diff --git a/docs/content/rules/sort-array-includes.mdx b/docs/content/rules/sort-array-includes.mdx index 3359ab5f..b17774f8 100644 --- a/docs/content/rules/sort-array-includes.mdx +++ b/docs/content/rules/sort-array-includes.mdx @@ -158,6 +158,44 @@ Allows you to group array elements by their kind, determining whether spread val - `literals-first` — Group all literal values before spread values. - `spreads-first` — Group all spread values before literal values. +### partitionByComment + +default: `false` + +Allows you to use comments to separate the members of arrays into logical groups. This can help in organizing and maintaining large enums by creating partitions within the enum based on comments. + +- `true` — All comments will be treated as delimiters, creating partitions. +- `false` — Comments will not be used as delimiters. +- `string` — A glob pattern to specify which comments should act as delimiters. +- `string[]` — A list of glob patterns to specify which comments should act as delimiters. + +### partitionByNewLine + +default: `false` + +When `true`, the rule will not sort the members of an array if there is an empty line between them. This can be useful for keeping logically separated groups of members in their defined order. + +```ts +if ([ + // Group 1 + 'Drone', + 'Keyboard', + 'Mouse', + 'Smartphone', + + // Group 2 + 'Laptop', + 'Monitor', + 'Smartwatch', + 'Tablet', + + // Group 3 + 'Headphones', + 'Router', + ].includes(product.name)) { + return 'Electronics' + } +``` ## Usage @@ -181,6 +219,7 @@ Allows you to group array elements by their kind, determining whether spread val order: 'asc', ignoreCase: true, groupKind: 'literals-first', + partitionByNewLine: false, }, ], }, @@ -205,6 +244,7 @@ Allows you to group array elements by their kind, determining whether spread val order: 'asc', ignoreCase: true, groupKind: 'literals-first', + partitionByNewLine: false, }, ], }, diff --git a/docs/content/rules/sort-sets.mdx b/docs/content/rules/sort-sets.mdx index b0e8090b..9777f665 100644 --- a/docs/content/rules/sort-sets.mdx +++ b/docs/content/rules/sort-sets.mdx @@ -164,6 +164,42 @@ Allows you to group set elements by their kind, determining whether spread value - `literals-first` — Group all literal values before spread values. - `spreads-first` — Group all spread values before literal values. +### partitionByComment + +default: `false` + +Allows you to use comments to separate the members of enums into logical groups. This can help in organizing and maintaining large enums by creating partitions within the enum based on comments. + +- `true` — All comments will be treated as delimiters, creating partitions. +- `false` — Comments will not be used as delimiters. +- `string` — A glob pattern to specify which comments should act as delimiters. +- `string[]` — A list of glob patterns to specify which comments should act as delimiters. + +### partitionByNewLine + +default: `false` + +When `true`, the rule will not sort the members of a set if there is an empty line between them. This can be useful for keeping logically separated groups of members in their defined order. + +```ts +let items = new Set([ + // Group 1 + 'Drone', + 'Keyboard', + 'Mouse', + 'Smartphone', + + // Group 2 + 'Laptop', + 'Monitor', + 'Smartwatch', + 'Tablet', + + // Group 3 + 'Headphones', + 'Router', + ]) +``` ## Usage diff --git a/rules/sort-array-includes.ts b/rules/sort-array-includes.ts index 76068206..edde6217 100644 --- a/rules/sort-array-includes.ts +++ b/rules/sort-array-includes.ts @@ -4,7 +4,10 @@ import type { TSESTree } from '@typescript-eslint/types' import type { SortingNode } from '../typings' +import { hasPartitionComment } from '../utils/is-partition-comment' +import { getCommentsBefore } from '../utils/get-comments-before' import { createEslintRule } from '../utils/create-eslint-rule' +import { getLinesBetween } from '../utils/get-lines-between' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' import { toSingleLine } from '../utils/to-single-line' @@ -23,6 +26,8 @@ export type Options = [ Partial<{ groupKind: 'literals-first' | 'spreads-first' | 'mixed' type: 'alphabetical' | 'line-length' | 'natural' + partitionByComment: string[] | boolean | string + partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean }>, @@ -51,6 +56,29 @@ export let jsonSchema: JSONSchema4 = { enum: ['mixed', 'literals-first', 'spreads-first'], type: 'string', }, + partitionByComment: { + description: + 'Allows you to use comments to separate the array members into logical groups.', + anyOf: [ + { + type: 'array', + items: { + type: 'string', + }, + }, + { + type: 'boolean', + }, + { + type: 'string', + }, + ], + }, + partitionByNewLine: { + description: + 'Allows to use spaces to separate the nodes into logical groups.', + type: 'boolean', + }, }, additionalProperties: false, } @@ -75,6 +103,8 @@ export default createEslintRule({ order: 'asc', ignoreCase: true, groupKind: 'literals-first', + partitionByComment: false, + partitionByNewLine: false, }, ], create: context => ({ @@ -108,76 +138,96 @@ export let sortArray = ( type: 'alphabetical', ignoreCase: true, order: 'asc', + partitionByComment: false, + partitionByNewLine: false, } as const) let sourceCode = getSourceCode(context) - let nodes: ({ type: string } & SortingNode)[] = elements - .reduce( - ( - accumulator: ({ type: string } & SortingNode)[][], - element: TSESTree.SpreadElement | TSESTree.Expression | null, - ) => { - if (element !== null) { - let group = 'unknown' - if (typeof options.groupKind === 'string') { - group = element.type === 'SpreadElement' ? 'spread' : 'literal' - } - accumulator.at(0)!.push({ - name: - element.type === 'Literal' - ? `${element.value}` - : sourceCode.text.slice(...element.range), - size: rangeToDiff(element.range), - type: element.type, - node: element, - group, - }) + let partitionComment = options.partitionByComment + + let formattedMembers: SortingNode[][] = elements.reduce( + ( + accumulator: SortingNode[][], + element: TSESTree.SpreadElement | TSESTree.Expression | null, + ) => { + if (element !== null) { + let group = 'unknown' + if (typeof options.groupKind === 'string') { + group = element.type === 'SpreadElement' ? 'spread' : 'literal' } - return accumulator - }, - [[], []], - ) - .flat() - - pairwise(nodes, (left, right) => { - let groupKindOrder = ['unknown'] - - if (typeof options.groupKind === 'string') { - groupKindOrder = - options.groupKind === 'literals-first' - ? ['literal', 'spread'] - : ['spread', 'literal'] - } + let lastSortingNode = accumulator.at(-1)?.at(-1) + let sortingNode: SortingNode = { + name: + element.type === 'Literal' + ? `${element.value}` + : sourceCode.text.slice(...element.range), + size: rangeToDiff(element.range), + node: element, + group, + } + if ( + (partitionComment && + hasPartitionComment( + partitionComment, + getCommentsBefore(element, sourceCode), + )) || + (options.partitionByNewLine && + lastSortingNode && + getLinesBetween(sourceCode, lastSortingNode, sortingNode)) + ) { + accumulator.push([]) + } - let leftNum = getGroupNumber(groupKindOrder, left) - let rightNum = getGroupNumber(groupKindOrder, right) + accumulator.at(-1)!.push(sortingNode) + } - if ( - (options.groupKind !== 'mixed' && leftNum > rightNum) || - ((options.groupKind === 'mixed' || leftNum === rightNum) && - isPositive(compare(left, right, options))) - ) { - context.report({ - messageId, - data: { - left: toSingleLine(left.name), - right: toSingleLine(right.name), - }, - node: right.node, - fix: fixer => { - let sortedNodes = - options.groupKind !== 'mixed' - ? groupKindOrder - .map(group => nodes.filter(n => n.group === group)) - .map(groupedNodes => sortNodes(groupedNodes, options)) - .flat() - : sortNodes(nodes, options) - - return makeFixes(fixer, nodes, sortedNodes, sourceCode) - }, - }) - } - }) + return accumulator + }, + [[]], + ) + + for (let nodes of formattedMembers) { + pairwise(nodes, (left, right) => { + let groupKindOrder = ['unknown'] + + if (typeof options.groupKind === 'string') { + groupKindOrder = + options.groupKind === 'literals-first' + ? ['literal', 'spread'] + : ['spread', 'literal'] + } + let leftNum = getGroupNumber(groupKindOrder, left) + let rightNum = getGroupNumber(groupKindOrder, right) + + if ( + (options.groupKind !== 'mixed' && leftNum > rightNum) || + ((options.groupKind === 'mixed' || leftNum === rightNum) && + isPositive(compare(left, right, options))) + ) { + context.report({ + messageId, + data: { + left: toSingleLine(left.name), + right: toSingleLine(right.name), + }, + node: right.node, + fix: fixer => { + let sortedNodes = + options.groupKind !== 'mixed' + ? groupKindOrder + .map(group => nodes.filter(n => n.group === group)) + .map(groupedNodes => sortNodes(groupedNodes, options)) + .flat() + : sortNodes(nodes, options) + + return makeFixes(fixer, nodes, sortedNodes, sourceCode, { + partitionComment, + }) + }, + }) + } + }) + } } } diff --git a/test/sort-array-includes.test.ts b/test/sort-array-includes.test.ts index 62945235..40fb92d8 100644 --- a/test/sort-array-includes.test.ts +++ b/test/sort-array-includes.test.ts @@ -305,6 +305,317 @@ describe(ruleName, () => { }, ], }) + + describe(`${ruleName}(${type}): partition by new line`, () => { + ruleTester.run( + `${ruleName}(${type}): allows to use new line as partition`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + [ + 'd', + 'a', + + 'c', + + 'e', + 'b', + ].includes(value) + `, + output: dedent` + [ + 'a', + 'd', + + 'c', + + 'b', + 'e', + ].includes(value) + `, + options: [ + { + ...options, + partitionByNewLine: true, + }, + ], + errors: [ + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'd', + right: 'a', + }, + }, + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'e', + right: 'b', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize partitions over group kind`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + [ + 'c', + ...d, + + 'a', + ...b, + ].includes(value) + `, + output: dedent` + [ + ...d, + 'c', + + ...b, + 'a', + ].includes(value) + `, + options: [ + { + ...options, + partitionByNewLine: true, + groupKind: 'spreads-first', + }, + ], + errors: [ + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'c', + right: '...d', + }, + }, + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'a', + right: '...b', + }, + }, + ], + }, + ], + }, + ) + }) + + describe(`${ruleName}(${type}): partition comments`, () => { + ruleTester.run( + `${ruleName}(${type}): allows to use partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + [ + // Part: A + 'cc', + 'd', + // Not partition comment + 'bbb', + // Part: B + 'aaaa', + 'e', + // Part: C + 'gg', + // Not partition comment + 'fff', + ].includes(value) + `, + output: dedent` + [ + // Part: A + // Not partition comment + 'bbb', + 'cc', + 'd', + // Part: B + 'aaaa', + 'e', + // Part: C + // Not partition comment + 'fff', + 'gg', + ].includes(value) + `, + options: [ + { + ...options, + partitionByComment: 'Part**', + }, + ], + errors: [ + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'd', + right: 'bbb', + }, + }, + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'gg', + right: 'fff', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allows to use all comments as parts`, + rule, + { + valid: [ + { + code: dedent` + [ + // Comment + 'bb', + // Other comment + 'a', + ].includes(value) + `, + options: [ + { + ...options, + partitionByComment: true, + }, + ], + }, + ], + invalid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allows to use multiple partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + [ + /* Partition Comment */ + // Part: A + 'd', + // Part: B + 'aaa', + 'c', + 'bb', + /* Other */ + 'e', + ].includes(value) + `, + output: dedent` + [ + /* Partition Comment */ + // Part: A + 'd', + // Part: B + 'aaa', + 'bb', + 'c', + /* Other */ + 'e', + ].includes(value) + `, + options: [ + { + ...options, + partitionByComment: ['Partition Comment', 'Part: *', 'Other'], + }, + ], + errors: [ + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'c', + right: 'bb', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize partitions over group kind`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + [ + 'c', + ...d, + // Part: 1 + 'a', + ...b, + ].includes(value) + `, + output: dedent` + [ + ...d, + 'c', + // Part: 1 + ...b, + 'a', + ].includes(value) + `, + options: [ + { + ...options, + partitionByComment: 'Part: *', + groupKind: 'spreads-first', + }, + ], + errors: [ + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'c', + right: '...d', + }, + }, + { + messageId: 'unexpectedArrayIncludesOrder', + data: { + left: 'a', + right: '...b', + }, + }, + ], + }, + ], + }, + ) + }) }) describe(`${ruleName}: sorting by natural order`, () => { diff --git a/test/sort-sets.test.ts b/test/sort-sets.test.ts index e342d4b1..5e55ed55 100644 --- a/test/sort-sets.test.ts +++ b/test/sort-sets.test.ts @@ -305,6 +305,264 @@ describe(ruleName, () => { }, ], }) + + describe(`${ruleName}(${type}): partition by new line`, () => { + ruleTester.run( + `${ruleName}(${type}): allows to use new line as partition`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + new Set([ + 'd', + 'a', + + 'c', + + 'e', + 'b', + ]) + `, + output: dedent` + new Set([ + 'a', + 'd', + + 'c', + + 'b', + 'e', + ]) + `, + options: [ + { + ...options, + partitionByNewLine: true, + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'd', + right: 'a', + }, + }, + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'e', + right: 'b', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize partitions over group kind`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + new Set([ + 'c', + ...d, + + 'a', + ...b, + ]) + `, + output: dedent` + new Set([ + ...d, + 'c', + + ...b, + 'a', + ]) + `, + options: [ + { + ...options, + partitionByNewLine: true, + groupKind: 'spreads-first', + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'c', + right: '...d', + }, + }, + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'a', + right: '...b', + }, + }, + ], + }, + ], + }, + ) + }) + + describe(`${ruleName}(${type}): partition comments`, () => { + ruleTester.run( + `${ruleName}(${type}): allows to use partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + new Set([ + // Part: A + 'cc', + 'd', + // Not partition comment + 'bbb', + // Part: B + 'aaaa', + 'e', + // Part: C + 'gg', + // Not partition comment + 'fff', + ]) + `, + output: dedent` + new Set([ + // Part: A + // Not partition comment + 'bbb', + 'cc', + 'd', + // Part: B + 'aaaa', + 'e', + // Part: C + // Not partition comment + 'fff', + 'gg', + ]) + `, + options: [ + { + ...options, + partitionByComment: 'Part**', + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'd', + right: 'bbb', + }, + }, + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'gg', + right: 'fff', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allows to use all comments as parts`, + rule, + { + valid: [ + { + code: dedent` + new Set([ + // Comment + 'bb', + // Other comment + 'a', + ]) + `, + options: [ + { + ...options, + partitionByComment: true, + }, + ], + }, + ], + invalid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allows to use multiple partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + new Set([ + /* Partition Comment */ + // Part: A + 'd', + // Part: B + 'aaa', + 'c', + 'bb', + /* Other */ + 'e', + ]) + `, + output: dedent` + new Set([ + /* Partition Comment */ + // Part: A + 'd', + // Part: B + 'aaa', + 'bb', + 'c', + /* Other */ + 'e', + ]) + `, + options: [ + { + ...options, + partitionByComment: ['Partition Comment', 'Part: *', 'Other'], + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'c', + right: 'bb', + }, + }, + ], + }, + ], + }, + ) + }) }) describe(`${ruleName}: sorting by natural order`, () => {