diff --git a/docs/src/rules/sort-keys.md b/docs/src/rules/sort-keys.md index a551f4f7ab2..f4047cde3e5 100644 --- a/docs/src/rules/sort-keys.md +++ b/docs/src/rules/sort-keys.md @@ -92,6 +92,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`, the rule allows to group object keys through line breaks. In other words, a blank line after a property will reset the sorting of keys. Default is `false`. Example for a list: @@ -263,6 +264,128 @@ let obj = { ::: +### allowLineSeparatedGroups + +Examples of **incorrect** code for the `{allowLineSeparatedGroups: true}` option: + +::: incorrect + +```js +/*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*/ +/*eslint-env es6*/ + +let obj1 = { + b: 1, + c () { + + }, + a: 3 +} + +let obj2 = { + b: 1, + c: 2, + + z () { + + }, + y: 3 +} + +let obj3 = { + b: 1, + c: 2, + + z () { + + }, + // comment + y: 3, +} + +let obj4 = { + b: 1 + // comment before comma + , a: 2 +}; +``` + +::: + +Examples of **correct** code for the `{allowLineSeparatedGroups: true}` option: + +::: correct + +```js +/*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*/ +/*eslint-env es6*/ + +let obj = { + e: 1, + f: 2, + g: 3, + + a: 4, + b: 5, + c: 6 +} + +let obj = { + b: 1, + + // comment + a: 4, + c: 5, +} + +let obj = { + c: 1, + d: 2, + + b () { + + }, + e: 3, +} + +let obj = { + c: 1, + d: 2, + // comment + + // comment + b() { + + }, + e: 4 +} + +let obj = { + b, + + [foo + bar]: 1, + a +} + +let obj = { + b: 1 + // comment before comma + + , + a: 2 +}; + +var obj = { + b: 1, + + a: 2, + ...z, + c: 3 +} +``` + +::: + ## When Not To Use It If you don't want to notify about properties' order, then it's safe to disable this rule. diff --git a/lib/rules/sort-keys.js b/lib/rules/sort-keys.js index 5cda81d0a7d..1523ab751a0 100644 --- a/lib/rules/sort-keys.js +++ b/lib/rules/sort-keys.js @@ -105,6 +105,10 @@ module.exports = { type: "integer", minimum: 2, default: 2 + }, + allowLineSeparatedGroups: { + type: "boolean", + default: false } }, additionalProperties: false @@ -124,17 +128,21 @@ 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; + const sourceCode = context.getSourceCode(); return { ObjectExpression(node) { stack = { upper: stack, + prevNode: null, + prevBlankLine: false, prevName: null, numKeys: node.properties.length }; @@ -159,10 +167,45 @@ module.exports = { const numKeys = stack.numKeys; const thisName = getPropertyName(node); + // Get tokens between current node and previous node + const tokens = stack.prevNode && sourceCode + .getTokensBetween(stack.prevNode, node, { includeComments: true }); + + let isBlankLineBetweenNodes = stack.prevBlankLine; + + if (tokens) { + + // check blank line between tokens + tokens.forEach((token, index) => { + const previousToken = tokens[index - 1]; + + if (previousToken && (token.loc.start.line - previousToken.loc.end.line > 1)) { + isBlankLineBetweenNodes = true; + } + }); + + // check blank line between the current node and the last token + if (!isBlankLineBetweenNodes && (node.loc.start.line - tokens[tokens.length - 1].loc.end.line > 1)) { + isBlankLineBetweenNodes = true; + } + + // check blank line between the first token and the previous node + if (!isBlankLineBetweenNodes && (tokens[0].loc.start.line - stack.prevNode.loc.end.line > 1)) { + isBlankLineBetweenNodes = true; + } + } + + stack.prevNode = node; + if (thisName !== null) { stack.prevName = thisName; } + if (allowLineSeparatedGroups && isBlankLineBetweenNodes) { + stack.prevBlankLine = thisName === null; + return; + } + if (prevName === null || thisName === null || numKeys < minKeys) { return; } diff --git a/tests/lib/rules/sort-keys.js b/tests/lib/rules/sort-keys.js index ff6482747ad..5c9523ab68c 100644 --- a/tests/lib/rules/sort-keys.js +++ b/tests/lib/rules/sort-keys.js @@ -163,7 +163,202 @@ 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 option + { + code: ` + var obj = { + e: 1, + f: 2, + g: 3, + + a: 4, + b: 5, + c: 6 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }] + }, + { + code: ` + var obj = { + b: 1, + + // comment + a: 2, + c: 3 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }] + }, + { + code: ` + var obj = { + b: 1 + + , + + // comment + a: 2, + c: 3 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }] + }, + { + code: ` + var obj = { + c: 1, + d: 2, + + b() { + }, + e: 4 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: ` + var obj = { + c: 1, + d: 2, + // comment + + // comment + b() { + }, + e: 4 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: ` + var obj = { + b, + + [a+b]: 1, + a + } + `, + 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 } + }, + { + code: ` + var obj = { + b: "/*", + + a: "*/", + } + `, + options: ["asc", { allowLineSeparatedGroups: true }] + }, + { + code: ` + var obj = { + b, + /* + */ // + + a + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: ` + var obj = { + b, + + /* + */ // + a + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: ` + var obj = { + b: 1 + + ,a: 2 + }; + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: ` + var obj = { + b: 1 + // comment before comma + + , + a: 2 + }; + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: ` + var obj = { + b, + + a, + ...z, + c + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 2018 } + }, + { + code: ` + var obj = { + b, + + [foo()]: [ + + ], + a + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 2018 } + } ], invalid: [ @@ -1761,6 +1956,296 @@ ruleTester.run("sort-keys", rule, { } } ] + }, + + // When allowLineSeparatedGroups option is false + { + 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: ` + let obj = { + b + + ,a + } + `, + options: ["asc", { allowLineSeparatedGroups: false }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "a", + prevName: "b" + } + } + ] + }, + + // When allowLineSeparatedGroups option is true + { + 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 = { + a: 1, + b: 2, + + z () { + + }, + y: 3 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "y", + prevName: "z" + } + } + ] + }, + { + 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 = { + b, + [a+b]: 1, + a // sort-keys: 'a' should be before 'b' + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "a", + prevName: "b" + } + } + ] + }, + { + code: ` + var obj = { + c: 1, + d: 2, + // comment + // comment + b() { + }, + e: 4 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "b", + prevName: "d" + } + } + ] + }, + { + code: ` + var obj = { + c: 1, + d: 2, + + z() { + + }, + f: 3, + /* + + + */ + [a+b]: 1, + b: 1, + e: 2 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "f", + prevName: "z" + } + }, + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "b", + prevName: "f" + } + } + ] + }, + { + code: ` + var obj = { + b: "/*", + a: "*/", + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + errors: [ + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "a", + prevName: "b" + } + } + ] + }, + { + code: ` + var obj = { + b: 1 + // comment before comma + , a: 2 + }; + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "a", + prevName: "b" + } + } + ] + }, + { + code: ` + let obj = { + b, + [foo()]: [ + // ↓ this blank is inside a property and therefore should not count + + ], + a + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + messageId: "sortKeys", + data: { + natural: "", + insensitive: "", + order: "asc", + thisName: "a", + prevName: "b" + } + } + ] } ] });