Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add allowLineSeparatedGroups option to sort-keys (fixes #12759) #13573

Closed
wants to merge 13 commits into from
35 changes: 34 additions & 1 deletion docs/rules/sort-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ let obj = {b: 1, ...c, a: 2};

```json
{
"sort-keys": ["error", "asc", {"caseSensitive": true, "natural": false, "minKeys": 2}]
"sort-keys": ["error", "asc", {"caseSensitive": true, "natural": false, "minKeys": 2, "allowLineSeparatedGroups": false}]
}
```

Expand All @@ -75,6 +75,7 @@ The 2nd option is an object which has 3 properties.
* `caseSensitive` - if `true`, enforce properties to be in case-sensitive order. Default is `true`.
* `minKeys` - Specifies the minimum number of keys that an object should have in order for the object's unsorted keys to produce an error. Default is `2`, which means by default all objects with unsorted keys will result in lint errors.
* `natural` - if `true`, enforce properties to be in natural order. Default is `false`. Natural Order compares strings containing combination of letters and numbers in the way a human being would sort. It basically sorts numerically, instead of sorting alphabetically. So the number 10 comes after the number 3 in Natural Sorting.
* `allowLineSeparatedGroups` - if `true`, Group object keys through line break. Default is `false`.

Example for a list:

Expand Down Expand Up @@ -214,6 +215,38 @@ let obj = {
};
```

### allowLineSeparatedGroups

Examples of **incorrect** code for the `{allowLineSeparatedGroups: true}` option:

```js
/*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*//
/*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*/

/*eslint-env es6*/

// Each group follows the sort-keys rule.
let obj = {
c: 2,
b: 3, // 'b' should be before 'c'.

a: 3
};
```

Examples of **correct** code for the `{allowLineSeparatedGroups: true}` option:

```js
/*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*//
/*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*/

/*eslint-env es6*/

// Divide groups through line breaks.
let obj = {
b: 2,
c: 3,

a: 3
};
```

## When Not To Use It

If you don't want to notify about properties' order, then it's safe to disable this rule.
Expand Down
34 changes: 33 additions & 1 deletion lib/rules/sort-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ function getPropertyName(node) {
return node.key.name || null;
}

/**
* Check the Line Separated Groups
*
* - Compare the start line of the current node with the last line of the previous node.
* If the difference between the two values is greater than 0, Not the same group.
* @param {number} thisStartLine is start line of the current node.
* @param {number} prevEndLine is end line of the previous node.
* @returns {number} number of lines between nodes.
* @private
*/
function checkLineSeparatedGroups(thisStartLine, prevEndLine) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change the function name to getLinesBetween or getNumberOfLinesBetween?
IMO check prefixed name seems to represent return boolean or call context.report() but actually it returns the number of lines between two.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I change it to !!Math.max(thisStartLine - prevEndLine - 1, 0); ?
In this function, distances the value of the two is not important but whether they are separated from each other is important.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I change it to !!Math.max(thisStartLine - prevEndLine - 1, 0); ?

sure :). it makes sense to me

return Math.max(thisStartLine - prevEndLine - 1, 0);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has some false positives when we use /*, */, // as a value in code.

        {
            code: `
                var obj = {
                    c: "/*",

                    a: "*/", // Error. Expected object keys to be in ascending order. 'a' should be before 'c'.
                }
            `,
            options: ["asc", { allowLineSeparatedGroups: true }]
        }

Is there a better way?

🤔 IMO, we can use SourceCode methods for it. there are some methods for treating comment as a node or token

getTokenAfter(nodeOrToken, {includeComments: true})
getTokenBefore(nodeOrToken, {includeComments: true}); 

Copy link
Author

@JJoGeon JJoGeon Sep 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @yeonjuan !!

in this case,

var sampleObject1 = {
  a: 1,
  b: 2,
  // comment 1
  // comment 2
  c: 3
}

I used getTokensBefore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a complicated way to find out if there's a blank line.

For example, this is a false positive:

/* eslint sort-keys: ["error", "asc", { allowLineSeparatedGroups: true }] */

var obj = {
  b,
  /*
  */ //

  a
}

comments offset is calculated as 3, but it doesn't take into account if there are lines between comments or not.

Could we maybe simplify this? We could take tokens between properties and then see if there's a blank line between any two successive tokens.

More precisely, if we want to check if there's a blank line between two properties (property1 and property2), than we could check if there's a blank line between any two of the following items:

  • property1
  • comma after property1
  • comment1
  • comment2
  • ....
  • commentN
  • property2


/**
* Functions which check that the given 2 names are in specific order.
*
Expand Down Expand Up @@ -105,13 +119,18 @@ module.exports = {
type: "integer",
minimum: 2,
default: 2
},
allowLineSeparatedGroups: {
type: "boolean",
default: false
}
},
additionalProperties: false
}
],

messages: {
allowLineSeparatedGroups: "'{{thisName}}' should be before '{{prevName}}'. To separate groups by line break, apply the allowLineSeparatedGroups option.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's good to give option information to those who don't use this option.
I want to hear other member's opinion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this message, and just use the existing one. I believe we don't have any error messages that advise users to reconfigure the rule.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the advice !!
I'll remove it !!

sortKeys: "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'."
}
},
Expand All @@ -124,6 +143,7 @@ module.exports = {
const insensitive = options && options.caseSensitive === false;
const natural = options && options.natural;
const minKeys = options && options.minKeys;
const allowLineSeparatedGroups = options && options.allowLineSeparatedGroups || false;
const isValidOrder = isValidOrders[
order + (insensitive ? "I" : "") + (natural ? "N" : "")
];
Expand All @@ -136,6 +156,7 @@ module.exports = {
stack = {
upper: stack,
prevName: null,
prevEndLine: null,
numKeys: node.properties.length
};
},
Expand All @@ -156,13 +177,24 @@ module.exports = {
}

const prevName = stack.prevName;
const prevEndLine = stack.prevEndLine;
const numKeys = stack.numKeys;
const thisName = getPropertyName(node);
const thisStartLine = node.loc.start.line;
const isLineSeparatedGroups = prevEndLine && checkLineSeparatedGroups(thisStartLine, prevEndLine);

if (thisStartLine !== null) {
Copy link
Member

@yeonjuan yeonjuan Aug 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 const thisStartLine = node.loc.start.line;

//...

if (thisStartLine !== null) { 

I think it's unneeded condition since thisStartLine seems equals with node.loc.start.line (number).
so thisStartLine !== null seems always true

stack.prevEndLine = thisStartLine;
Copy link
Member

@yeonjuan yeonjuan Aug 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to treat the multiline property as a group separator because it compares the start line of the previous property and the start line of the current property. So the below case has no lint error.

{
   code: `
     var obj = {
        b: 1, // 1st Group
        c () { // 1st Group 

        },
        a: 3 // 2nd Group ?? No Error
      }
     `,
    options: ["asc", { allowLineSeparatedGroups: true }],
    parserOptions: { ecmaVersion: 6 }
},

Maybe, we should compare the end line of the previous property with the start line of the current property as what "sort-import" does

/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/

import b from 'foo.js';
import d 
   from 'bar.js';
import a from 'baz.js'; // Lint error, cause it's not separated from previous

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry !!!!
I made a mistake while clean the code.

It was stack.prevEndLine = node.loc.end.line; before PR.

Thank you !

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry !!!!
I made a mistake while clean the code.

There's no need to be sorry :)
Can we add a test case for ensuring it? (maybe in invalid cases?)

{
   code: `
     var obj = {
        b: 1, 
        c () { 

        },
        a: 3
      }
     `,
    options: ["asc", { allowLineSeparatedGroups: true }],
    parserOptions: { ecmaVersion: 6 }
},

}

if (thisName !== null) {
stack.prevName = thisName;
}

if (allowLineSeparatedGroups && isLineSeparatedGroups) {
return;
}

if (prevName === null || thisName === null || numKeys < minKeys) {
return;
}
Expand All @@ -171,7 +203,7 @@ module.exports = {
context.report({
node,
loc: node.key.loc,
messageId: "sortKeys",
messageId: isLineSeparatedGroups ? "allowLineSeparatedGroups" : "sortKeys",
data: {
thisName,
prevName,
Expand Down
70 changes: 69 additions & 1 deletion tests/lib/rules/sort-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,53 @@ ruleTester.run("sort-keys", rule, {
{ code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ["desc", { natural: true, caseSensitive: false }] },

// desc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
{ code: "var obj = {a:1, _:2, b:3}", options: ["desc", { natural: true, caseSensitive: false, minKeys: 4 }] }
{ code: "var obj = {a:1, _:2, b:3}", options: ["desc", { natural: true, caseSensitive: false, minKeys: 4 }] },

// allowLineSeparatedGroups, default false
{
code: `
var obj = {
f: 1,
g: 2,


a: 3,
b: 4,
c: 5,

d: 6,
e: 7
}
`,
options: ["asc", { allowLineSeparatedGroups: true }]
},
{
code: `
var obj = {
b: 1,
c: 2,

a: 3
}
`,
options: ["asc", { allowLineSeparatedGroups: true }]
},
{
code: `
var obj = {
c: 1,
d: 2,

a: 3,
b() {

},
e: 4
}
`,
options: ["asc", { allowLineSeparatedGroups: true }],
parserOptions: { ecmaVersion: 6 }
}
],
invalid: [

Expand Down Expand Up @@ -1761,6 +1807,28 @@ ruleTester.run("sort-keys", rule, {
}
}
]
},

// If allowLineSeparatedGroups option is false, Groups are not divided by line breaks.
{
code: `
var obj = {
b: 1,
c: 2,

a: 3
}
`,
options: ["asc", { allowLineSeparatedGroups: false }],
errors: [
{
messageId: "allowLineSeparatedGroups",
data: {
thisName: "a",
prevName: "c"
}
}
]
}
]
});