From 099af7836169255d424361aaec46eab10ebfaebf Mon Sep 17 00:00:00 2001 From: Emily M Klassen Date: Mon, 24 Oct 2022 12:36:37 -0700 Subject: [PATCH] feat: add new `allowLineSeparatedGroups` option Adapted from https://github.com/eslint/eslint/pull/16138 --- __tests__/lib/rules/sort-keys.js | 196 +++++++++++++++++++++++++++++++ lib/rules/sort-keys.js | 83 +++++++++++-- package.json | 3 +- 3 files changed, 274 insertions(+), 8 deletions(-) diff --git a/__tests__/lib/rules/sort-keys.js b/__tests__/lib/rules/sort-keys.js index 6499227..c26b9f8 100644 --- a/__tests__/lib/rules/sort-keys.js +++ b/__tests__/lib/rules/sort-keys.js @@ -172,6 +172,202 @@ const test = { // 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 }] }, + // 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 }, + }, + { code: 'var obj = {a:1, _:2, b:3}', options: ['desc', { natural: true, caseSensitive: false, minKeys: 4 }] }, + // ALL_CAPS first { code: 'var obj = {CA: 0, b_:1, Ca:3, ca:2}', options: ['asc', { caseSensitive: false, allCaps: 'first' }] }, { code: 'var obj = {CA: 0, b_:1, ca:3, Ca:2}', options: ['asc', { caseSensitive: false, allCaps: 'first' }] }, diff --git a/lib/rules/sort-keys.js b/lib/rules/sort-keys.js index 875e7d0..f13ab83 100644 --- a/lib/rules/sort-keys.js +++ b/lib/rules/sort-keys.js @@ -30,9 +30,22 @@ const astUtils = require('./utils/ast-utils'), * @property {boolean} [caseSensitive] Use case sensitive sorting * @property {boolean} [natural] Use natural sorting * @property {number} [minKeys] Minimum Keys + * @property {boolean} [allowLineSeparatedGroups] Allow Line Separated Groups * @property {'first' | 'last' | 'ignore'} [allCaps] All Caps option * @property {Override[]} [overrides] Overrides options */ +/** @typedef {(a: string, b: string) => boolean | null} IsValidOrder */ +/** + * @typedef Stack + * @property {Stack | null} upper + * @property {import('estree').Property & import('eslint').Rule.NodeParentExtension | null} prevNode + * @property {boolean} prevBlankLine + * @property {string | null} prevName + * @property {number} numKeys + * @property {string} parentName + * @property {Override} override + * @property {IsValidOrder} isValidOrder + */ /** * Combinatorial combination fn @@ -74,6 +87,45 @@ function getPropertyName(node) { return (key && key.name) || null } +/** + * test blank lines + * @param {import('eslint').Rule.RuleContext} context context + * @param {import('eslint').Rule.Node} node node + * @param {import('eslint').Rule.Node} prevNode prevNode + * @returns {boolean} if there is a blank line between the prevNode and current node + */ +function hasBlankLineBetweenNodes(context, node, prevNode) { + const sourceCode = context.getSourceCode() + + // Get tokens between current node and previous node + const tokens = prevNode && sourceCode.getTokensBetween(prevNode, node, { includeComments: true }) + + if (!tokens) { + return false + } + let previousToken + + // check blank line between tokens + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + + if (previousToken && token.loc.start.line - previousToken.loc.end.line > 1) { + return true + } + previousToken = token + } + + // check blank line between the current node and the last token + if (node.loc.start.line - tokens[tokens.length - 1].loc.end.line > 1) { + return true + } + + // check blank line between the first token and the previous node + if (tokens[0].loc.start.line - prevNode.loc.end.line > 1) { + return true + } +} + /** * Function to check that 2 names are proper all caps order * @param {string} a first value @@ -93,8 +145,7 @@ function isValidAllCapsTest(a, b) { * * Postfix `I` is meant insensitive. * Postfix `N` is meant natural. - * Postfix `C` is all caps/constants first - * @type {Record<`${'asc'|'desc'}${''|'I'}${''|'N'}`, (a: string, b: string) => boolean>} + * @type {Record<`${'asc'|'desc'}${''|'I'}${''|'N'}`, IsValidOrder>} * @private */ // @ts-ignore @@ -214,6 +265,10 @@ module.exports = { minimum: 2, default: 2, }, + allowLineSeparatedGroups: { + type: 'boolean', + default: false, + }, allCaps: { enum: ['first', 'last', 'ignore'], default: 'ignore', @@ -261,6 +316,7 @@ module.exports = { const insensitive = options && options.caseSensitive === false const natural = options && options.natural const minKeys = (options && options.minKeys) || 2 + const allowLineSeparatedGroups = (options && options.allowLineSeparatedGroups) || false const allCaps = (options && options.allCaps) || 'ignore' /** @type {Override[]} */ const overrides = (options && options.overrides) || [] @@ -288,11 +344,15 @@ module.exports = { }) }) const isValidOrderBase = isValidOrders[`${order}${insensitive ? 'I' : ''}${natural ? 'N' : ''}`] + /** @type {IsValidOrder} */ const isValidOrderAllCaps = // eslint-disable-next-line no-nested-ternary allCaps === 'ignore' ? () => null : allCaps === 'last' ? (a, b) => isValidAllCapsTest(b, a) : isValidAllCapsTest - // The stack to save the previous property's name for each object literals. + /** + * The stack to save the previous property's name for each object literals. + * @type {Stack | null} + */ let stack = null /** @@ -302,8 +362,8 @@ module.exports = { */ function SpreadElement(node) { if (node.parent.type === 'ObjectExpression') { - stack.prevName = null stack.prevNode = null + stack.prevName = null } } @@ -335,8 +395,9 @@ module.exports = { stack = { upper: stack, - prevName: null, prevNode: null, + prevBlankLine: false, + prevName: null, numKeys: node.properties.length, parentName, override, @@ -355,14 +416,22 @@ module.exports = { return } - const prevName = stack.prevName const prevNode = stack.prevNode + const prevName = stack.prevName const numKeys = stack.numKeys const thisName = getPropertyName(node) + const isBlankLineBetweenNodes = + stack.prevBlankLine || (allowLineSeparatedGroups && hasBlankLineBetweenNodes(context, node, prevNode)) + + stack.prevNode = node if (thisName !== null) { stack.prevName = thisName - stack.prevNode = node || prevNode + } + + if (allowLineSeparatedGroups && isBlankLineBetweenNodes) { + stack.prevBlankLine = thisName === null + return } if (prevName === null || thisName === null || numKeys < minKeys) { diff --git a/package.json b/package.json index ea39ae4..d2f8800 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ ], "author": "Emily M Klassen ", "contributors": [ - "Leonid Buneev " + "Leonid Buneev ", + "ESLint Contributors " ], "main": "lib/index.js", "scripts": {