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
37 changes: 35 additions & 2 deletions 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 @@ -197,7 +198,7 @@ let obj = {
Examples of **correct** code for the `{minKeys: 4}` option:

```js
/*eslint sort-keys: ["error", "asc", {minKeys: 4}]*//
/*eslint sort-keys: ["error", "asc", {minKeys: 4}]*/
/*eslint-env es6*/

// 3 keys
Expand All @@ -214,6 +215,38 @@ let obj = {
};
```

### allowLineSeparatedGroups

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

```js
/*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
51 changes: 51 additions & 0 deletions lib/rules/sort-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,38 @@ 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.
* @param {Array<string>} sourceCodeLines use to check comment between groups.
* @returns {boolean} check group
* @private
*/
function checkLineSeparatedGroups(thisStartLine, prevEndLine, sourceCodeLines) {
let blockCommentFlag = false;
let commentOffset = 0;
Copy link
Author

Choose a reason for hiding this comment

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

@yeonjuan @mdjermanovic

I used the forEach function to check the comment line, but I'm not sure if it's okay.

Is there a better way?


sourceCodeLines.slice(prevEndLine, thisStartLine).forEach(value => {
if (value.includes("//")) {
commentOffset++;
} else if (value.includes("/*")) {
commentOffset++;
blockCommentFlag = true;
} else if (value.includes("*/")) {
commentOffset++;
blockCommentFlag = false;
} else if (blockCommentFlag) {
commentOffset++;
}
});

return !!Math.max(thisStartLine - prevEndLine - commentOffset - 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,6 +137,10 @@ module.exports = {
type: "integer",
minimum: 2,
default: 2
},
allowLineSeparatedGroups: {
type: "boolean",
default: false
}
},
additionalProperties: false
Expand All @@ -124,18 +160,23 @@ 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" : "")
];

// The stack to save the previous property's name for each object literals.
let stack = null;

// The sourceCode use to check comment between groups
const sourceCode = context.getSourceCode();

return {
ObjectExpression(node) {
stack = {
upper: stack,
prevName: null,
prevEndLine: null,
numKeys: node.properties.length
};
},
Expand All @@ -156,13 +197,23 @@ 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, sourceCode.lines);

stack.prevEndLine = node.loc.end.line;

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

if (allowLineSeparatedGroups && isLineSeparatedGroups) {
stack.prevName = null;
return;
}

if (prevName === null || thisName === null || numKeys < minKeys) {
return;
}
Expand Down
196 changes: 195 additions & 1 deletion tests/lib/rules/sort-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,89 @@ 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 }
},
{
code: `
var obj = {
b,

[foo + bar]: 1,
a // sort-keys: 'a' should be before 'b'
}
`,
options: ["asc", { allowLineSeparatedGroups: true }],
parserOptions: { ecmaVersion: 6 }
},
{
code: `
var obj = {
c: 1,
d: 2,

a() {

},
// abce
f: 3,

/*


*/
[a+b]: 1,
cc: 1,
e: 2
}
`,
options: ["asc", { allowLineSeparatedGroups: true }],
parserOptions: { ecmaVersion: 6 }
}
],
invalid: [

Expand Down Expand Up @@ -1761,6 +1843,118 @@ 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: "sortKeys",
data: {
natural: "",
insensitive: "",
order: "asc",
thisName: "a",
prevName: "c"
}
}
]
},
{
code: `
var obj = {
b: 1,
c () {

},
a: 3
}
`,
options: ["asc", { allowLineSeparatedGroups: true }],
parserOptions: { ecmaVersion: 6 },
errors: [
{
messageId: "sortKeys",
data: {
natural: "",
insensitive: "",
order: "asc",
thisName: "a",
prevName: "c"
}
}
]
},
{
code: `
var obj = {
b: 1,
c () {

},
// comment
a: 3
}
`,
options: ["asc", { allowLineSeparatedGroups: true }],
parserOptions: { ecmaVersion: 6 },
errors: [
{
messageId: "sortKeys",
data: {
natural: "",
insensitive: "",
order: "asc",
thisName: "a",
prevName: "c"
}
}
]
},
{
code: `
var obj = {
c: 1,
d: 2,

a() {

},
// abce
f: 3,

/*


*/
[a+b]: 1,
e: 4,
cc: 1
}
`,
options: ["asc", { allowLineSeparatedGroups: true }],
parserOptions: { ecmaVersion: 6 },
errors: [
{
messageId: "sortKeys",
data: {
natural: "",
insensitive: "",
order: "asc",
thisName: "cc",
prevName: "e"
}
}
]
}
]
});