From 4a0931b1131a8b18beaee4a709fa010150f7119b Mon Sep 17 00:00:00 2001 From: Hugo <60015232+hugop95@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:05:55 +0200 Subject: [PATCH] feat: add partition by comment and partition by new line in sort-union-types --- docs/content/rules/sort-union-types.mdx | 35 +++ rules/sort-union-types.ts | 279 ++++++++++++++---------- test/sort-union-types.test.ts | 196 +++++++++++++++++ 3 files changed, 399 insertions(+), 111 deletions(-) diff --git a/docs/content/rules/sort-union-types.mdx b/docs/content/rules/sort-union-types.mdx index 2e64fbf59..11ca5df6b 100644 --- a/docs/content/rules/sort-union-types.mdx +++ b/docs/content/rules/sort-union-types.mdx @@ -122,6 +122,37 @@ Controls whether sorting should be case-sensitive or not. - `true` — Ignore case when sorting alphabetically or naturally (e.g., “A” and “a” are the same). - `false` — Consider case when sorting (e.g., “A” comes before “a”). +### partitionByComment + +default: `false` + +Allows you to use comments to separate the members of union types 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 union type if there is an empty line between them. This can be useful for keeping logically separated groups of members in their defined order. + +```ts +type CarBrand = + // Group 1 + Fiat | + Honda | + + // Group 2 + Ferrari | + + // Group 3 + Chevrolet | + Ford +``` + ### groups default: `[]` @@ -243,6 +274,8 @@ groups: [ type: 'alphabetical', order: 'asc', ignoreCase: true, + partitionByNewLine: false, + partitionByComment: false, groups: [], }, ], @@ -267,6 +300,8 @@ groups: [ type: 'alphabetical', order: 'asc', ignoreCase: true, + partitionByNewLine: false, + partitionByComment: false, groups: [], }, ], diff --git a/rules/sort-union-types.ts b/rules/sort-union-types.ts index befd28981..56a9eca0d 100644 --- a/rules/sort-union-types.ts +++ b/rules/sort-union-types.ts @@ -1,7 +1,10 @@ import type { SortingNode } from '../typings' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' +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' @@ -35,7 +38,9 @@ type Group = type Options = [ Partial<{ type: 'alphabetical' | 'line-length' | 'natural' + partitionByComment: string[] | boolean | string groups: (Group[] | Group)[] + partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean }>, @@ -86,6 +91,29 @@ export default createEslintRule({ ], }, }, + partitionByComment: { + description: + 'Allows you to use comments to separate the union types 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, }, @@ -102,6 +130,8 @@ export default createEslintRule({ type: 'alphabetical', order: 'asc', ignoreCase: true, + partitionByNewLine: false, + partitionByComment: false, groups: [], }, ], @@ -114,6 +144,8 @@ export default createEslintRule({ ignoreCase: true, order: 'asc', groups: [], + partitionByNewLine: false, + partitionByComment: false, } as const) validateGroupsConfiguration( @@ -137,128 +169,153 @@ export default createEslintRule({ ) let sourceCode = getSourceCode(context) + let partitionComment = options.partitionByComment - let nodes: SortingNode[] = node.types.map(type => { - let { getGroup, defineGroup } = useGroups(options.groups) + let formattedMembers: SortingNode[][] = node.types.reduce( + (accumulator: SortingNode[][], type) => { + let { getGroup, defineGroup } = useGroups(options.groups) - switch (type.type) { - case 'TSConditionalType': - defineGroup('conditional') - break - case 'TSConstructorType': - case 'TSFunctionType': - defineGroup('function') - break - case 'TSImportType': - defineGroup('import') - break - case 'TSIntersectionType': - defineGroup('intersection') - break - case 'TSAnyKeyword': - case 'TSBigIntKeyword': - case 'TSBooleanKeyword': - case 'TSNeverKeyword': - case 'TSNumberKeyword': - case 'TSObjectKeyword': - case 'TSStringKeyword': - case 'TSSymbolKeyword': - case 'TSThisType': - case 'TSUnknownKeyword': - case 'TSIntrinsicKeyword': - defineGroup('keyword') - break - case 'TSLiteralType': - case 'TSTemplateLiteralType': - defineGroup('literal') - break - case 'TSArrayType': - case 'TSIndexedAccessType': - case 'TSInferType': - case 'TSTypeReference': - case 'TSQualifiedName': - defineGroup('named') - break - case 'TSMappedType': - case 'TSTypeLiteral': - defineGroup('object') - break - case 'TSTypeQuery': - case 'TSTypeOperator': - defineGroup('operator') - break - case 'TSTupleType': - defineGroup('tuple') - break - case 'TSUnionType': - defineGroup('union') - break - case 'TSNullKeyword': - case 'TSUndefinedKeyword': - case 'TSVoidKeyword': - defineGroup('nullish') - break - } + switch (type.type) { + case 'TSConditionalType': + defineGroup('conditional') + break + case 'TSConstructorType': + case 'TSFunctionType': + defineGroup('function') + break + case 'TSImportType': + defineGroup('import') + break + case 'TSIntersectionType': + defineGroup('intersection') + break + case 'TSAnyKeyword': + case 'TSBigIntKeyword': + case 'TSBooleanKeyword': + case 'TSNeverKeyword': + case 'TSNumberKeyword': + case 'TSObjectKeyword': + case 'TSStringKeyword': + case 'TSSymbolKeyword': + case 'TSThisType': + case 'TSUnknownKeyword': + case 'TSIntrinsicKeyword': + defineGroup('keyword') + break + case 'TSLiteralType': + case 'TSTemplateLiteralType': + defineGroup('literal') + break + case 'TSArrayType': + case 'TSIndexedAccessType': + case 'TSInferType': + case 'TSTypeReference': + case 'TSQualifiedName': + defineGroup('named') + break + case 'TSMappedType': + case 'TSTypeLiteral': + defineGroup('object') + break + case 'TSTypeQuery': + case 'TSTypeOperator': + defineGroup('operator') + break + case 'TSTupleType': + defineGroup('tuple') + break + case 'TSUnionType': + defineGroup('union') + break + case 'TSNullKeyword': + case 'TSUndefinedKeyword': + case 'TSVoidKeyword': + defineGroup('nullish') + break + } - return { - name: sourceCode.text.slice(...type.range), - size: rangeToDiff(type.range), - group: getGroup(), - node: type, - } - }) + let lastSortingNode = accumulator.at(-1)?.at(-1) + let sortingNode: SortingNode = { + name: sourceCode.text.slice(...type.range), + size: rangeToDiff(type.range), + group: getGroup(), + node: type, + } + if ( + (partitionComment && + hasPartitionComment( + partitionComment, + getCommentsBefore(type, sourceCode), + )) || + (options.partitionByNewLine && + lastSortingNode && + getLinesBetween(sourceCode, lastSortingNode, sortingNode)) + ) { + accumulator.push([]) + } - pairwise(nodes, (left, right) => { - let leftNum = getGroupNumber(options.groups, left) - let rightNum = getGroupNumber(options.groups, right) + accumulator.at(-1)?.push(sortingNode) - if ( - leftNum > rightNum || - (leftNum === rightNum && isPositive(compare(left, right, options))) - ) { - context.report({ - messageId: - leftNum !== rightNum - ? 'unexpectedUnionTypesGroupOrder' - : 'unexpectedUnionTypesOrder', - data: { - left: toSingleLine(left.name), - leftGroup: left.group, - right: toSingleLine(right.name), - rightGroup: right.group, - }, - node: right.node, - fix: fixer => { - let grouped: { - [key: string]: SortingNode[] - } = {} + return accumulator + }, + [[]], + ) + + for (let nodes of formattedMembers) { + pairwise(nodes, (left, right) => { + let leftNum = getGroupNumber(options.groups, left) + let rightNum = getGroupNumber(options.groups, right) - for (let currentNode of nodes) { - let groupNum = getGroupNumber(options.groups, currentNode) + if ( + leftNum > rightNum || + (leftNum === rightNum && isPositive(compare(left, right, options))) + ) { + context.report({ + messageId: + leftNum !== rightNum + ? 'unexpectedUnionTypesGroupOrder' + : 'unexpectedUnionTypesOrder', + data: { + left: toSingleLine(left.name), + leftGroup: left.group, + right: toSingleLine(right.name), + rightGroup: right.group, + }, + node: right.node, + fix: fixer => { + let grouped: { + [key: string]: SortingNode[] + } = {} - if (!(groupNum in grouped)) { - grouped[groupNum] = [currentNode] - } else { - grouped[groupNum] = sortNodes( - [...grouped[groupNum], currentNode], - options, - ) + for (let currentNode of nodes) { + let groupNum = getGroupNumber(options.groups, currentNode) + + if (!(groupNum in grouped)) { + grouped[groupNum] = [currentNode] + } else { + grouped[groupNum] = sortNodes( + [...grouped[groupNum], currentNode], + options, + ) + } } - } - let sortedNodes: SortingNode[] = [] + let sortedNodes: SortingNode[] = [] - for (let group of Object.keys(grouped).sort( - (a, b) => Number(a) - Number(b), - )) { - sortedNodes.push(...sortNodes(grouped[group], options)) - } + for (let group of Object.keys(grouped).sort( + (a, b) => Number(a) - Number(b), + )) { + sortedNodes.push(...sortNodes(grouped[group], options)) + } - return makeFixes(fixer, nodes, sortedNodes, sourceCode) - }, - }) - } - }) + return makeFixes(fixer, nodes, sortedNodes, sourceCode, { + partitionComment, + }) + }, + }) + } + }) + } }, }), }) diff --git a/test/sort-union-types.test.ts b/test/sort-union-types.test.ts index 0b3e78b52..73c808e2c 100644 --- a/test/sort-union-types.test.ts +++ b/test/sort-union-types.test.ts @@ -418,6 +418,202 @@ describe(ruleName, () => { }, ], }) + + ruleTester.run( + `${ruleName}(${type}): allows to use new line as partition`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + type Type = + D | + A | + + C | + + E | + B + `, + output: dedent` + type Type = + A | + D | + + C | + + B | + E + `, + options: [ + { + type: 'alphabetical', + partitionByNewLine: true, + }, + ], + errors: [ + { + messageId: 'unexpectedUnionTypesOrder', + data: { + left: 'D', + right: 'A', + }, + }, + { + messageId: 'unexpectedUnionTypesOrder', + data: { + left: 'E', + right: 'B', + }, + }, + ], + }, + ], + }, + ) + + describe('partition comments', () => { + ruleTester.run( + `${ruleName}(${type}): allows to use partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + type T = + // Part: A + CC | + D | + // Not partition comment + BBB | + // Part: B + AAA | + E | + // Part: C + GG | + // Not partition comment + FFF + `, + output: dedent` + type T = + // Part: A + // Not partition comment + BBB | + CC | + D | + // Part: B + AAA | + E | + // Part: C + // Not partition comment + FFF | + GG + `, + options: [ + { + ...options, + partitionByComment: 'Part**', + }, + ], + errors: [ + { + messageId: 'unexpectedUnionTypesOrder', + data: { + left: 'D', + right: 'BBB', + }, + }, + { + messageId: 'unexpectedUnionTypesOrder', + data: { + left: 'GG', + right: 'FFF', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allows to use all comments as parts`, + rule, + { + valid: [ + { + code: dedent` + type T = + // 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` + type T = + /* Partition Comment */ + // Part: A + D | + // Part: B + AAA | + C | + BB | + /* Other */ + E + `, + output: dedent` + type T = + /* Partition Comment */ + // Part: A + D | + // Part: B + AAA | + BB | + C | + /* Other */ + E + `, + options: [ + { + ...options, + partitionByComment: ['Partition Comment', 'Part: *', 'Other'], + }, + ], + errors: [ + { + messageId: 'unexpectedUnionTypesOrder', + data: { + left: 'C', + right: 'BB', + }, + }, + ], + }, + ], + }, + ) + }) }) describe(`${ruleName}: sorting by natural order`, () => {