From 31a942fbdb1dc985d270f536c5e25bb2e6454bbb Mon Sep 17 00:00:00 2001 From: chirokas Date: Mon, 11 Mar 2024 16:20:58 +0800 Subject: [PATCH 1/2] feat(sort-enums): add `partition-by-comment` option --- docs/rules/sort-enums.md | 1 + rules/sort-enums.ts | 88 +++++--- test/sort-enums.test.ts | 454 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 515 insertions(+), 28 deletions(-) diff --git a/docs/rules/sort-enums.md b/docs/rules/sort-enums.md index af4fdc696..3d3e22b3a 100644 --- a/docs/rules/sort-enums.md +++ b/docs/rules/sort-enums.md @@ -71,6 +71,7 @@ interface Options { type?: 'alphabetical' | 'natural' | 'line-length' order?: 'asc' | 'desc' 'ignore-case'?: boolean + 'partition-by-comment'?: string[] | string | boolean } ``` diff --git a/rules/sort-enums.ts b/rules/sort-enums.ts index a49b5cb0a..9174f1d4d 100644 --- a/rules/sort-enums.ts +++ b/rules/sort-enums.ts @@ -1,6 +1,8 @@ -import type { SortingNode } from '../typings' +import type { PartitionComment, SortingNode } from '../typings' +import { isPartitionComment } from '../utils/is-partition-comment' import { createEslintRule } from '../utils/create-eslint-rule' +import { getCommentBefore } from '../utils/get-comment-before' import { toSingleLine } from '../utils/to-single-line' import { rangeToDiff } from '../utils/range-to-diff' import { isPositive } from '../utils/is-positive' @@ -15,6 +17,7 @@ type MESSAGE_ID = 'unexpectedEnumsOrder' type Options = [ Partial<{ + 'partition-by-comment': PartitionComment 'ignore-case': boolean order: SortOrder type: SortType @@ -35,6 +38,10 @@ export default createEslintRule({ { type: 'object', properties: { + 'partition-by-comment': { + default: false, + type: ['boolean', 'string', 'array'], + }, type: { enum: [ SortType.alphabetical, @@ -77,36 +84,61 @@ export default createEslintRule({ type: SortType.alphabetical, order: SortOrder.asc, 'ignore-case': false, + 'partition-by-comment': false, }) - let nodes: SortingNode[] = node.members.map(member => ({ - name: - member.id.type === 'Literal' - ? `${member.id.value}` - : `${context.sourceCode.text.slice(...member.id.range)}`, - size: rangeToDiff(member.range), - node: member, - })) + let partitionComment = options['partition-by-comment'] - pairwise(nodes, (left, right) => { - if (isPositive(compare(left, right, options))) { - context.report({ - messageId: 'unexpectedEnumsOrder', - data: { - left: toSingleLine(left.name), - right: toSingleLine(right.name), - }, - node: right.node, - fix: fixer => - makeFixes( - fixer, - nodes, - sortNodes(nodes, options), - context.sourceCode, - ), - }) - } - }) + let formattedMembers: SortingNode[][] = node.members.reduce( + (accumulator: SortingNode[][], member) => { + let comment = getCommentBefore(member, context.sourceCode) + + if ( + partitionComment && + comment && + isPartitionComment(partitionComment, comment.value) + ) { + accumulator.push([]) + } + + let name = + member.id.type === 'Literal' + ? `${member.id.value}` + : `${context.sourceCode.text.slice(...member.id.range)}` + + let sortingNode = { + name, + node: member, + size: rangeToDiff(member.range), + } + accumulator.at(-1)!.push(sortingNode) + return accumulator + }, + [[]], + ) + + for (let nodes of formattedMembers) { + pairwise(nodes, (left, right) => { + if (isPositive(compare(left, right, options))) { + context.report({ + messageId: 'unexpectedEnumsOrder', + data: { + left: toSingleLine(left.name), + right: toSingleLine(right.name), + }, + node: right.node, + fix: fixer => + makeFixes( + fixer, + nodes, + sortNodes(nodes, options), + context.sourceCode, + { partitionComment }, + ), + }) + } + }) + } } }, }), diff --git a/test/sort-enums.test.ts b/test/sort-enums.test.ts index ed6ba3c60..efa2f201c 100644 --- a/test/sort-enums.test.ts +++ b/test/sort-enums.test.ts @@ -252,6 +252,162 @@ describe(RULE_NAME, () => { invalid: [], }, ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + enum HeroAssociation { + // Part: S-Class + Blast = 'Blast', + Tatsumaki = 'Tatsumaki', + // Atomic Samurai + Kamikaze = 'Kamikaze', + // Part: A-Class + Sweet = 'Sweet Mask', + Iaian = 'Iaian', + // Part: B-Class + 'Mountain-Ape' = 'Mountain Ape', + // Member of the Blizzard Group + Eyelashes = 'Eyelashes', + } + `, + output: dedent` + enum HeroAssociation { + // Part: S-Class + Blast = 'Blast', + // Atomic Samurai + Kamikaze = 'Kamikaze', + Tatsumaki = 'Tatsumaki', + // Part: A-Class + Iaian = 'Iaian', + Sweet = 'Sweet Mask', + // Part: B-Class + // Member of the Blizzard Group + Eyelashes = 'Eyelashes', + 'Mountain-Ape' = 'Mountain Ape', + } + `, + options: [ + { + ...options, + 'partition-by-comment': 'Part**', + }, + ], + errors: [ + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Tatsumaki', + right: 'Kamikaze', + }, + }, + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Sweet', + right: 'Iaian', + }, + }, + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Mountain-Ape', + right: 'Eyelashes', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use all comments as parts`, + rule, + { + valid: [ + { + code: dedent` + enum Brothers { + // Older brother + Edward = 'Edward Elric', + // Younger brother + Alphonse = 'Alphonse Elric', + } + `, + options: [ + { + ...options, + 'partition-by-comment': true, + }, + ], + }, + ], + invalid: [], + }, + ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use multiple partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + enum PsychoPass { + /* Public Safety Bureau */ + // Crime Coefficient: Low + Tsunemori = 'Akane Tsunemori', + // Crime Coefficient: High + Kogami = 'Shinya Kogami', + Ginoza = 'Nobuchika Ginoza', + Masaoka = 'Tomomi Masaoka', + /* Victims */ + Makishima = 'Shogo Makishima', + } + `, + output: dedent` + enum PsychoPass { + /* Public Safety Bureau */ + // Crime Coefficient: Low + Tsunemori = 'Akane Tsunemori', + // Crime Coefficient: High + Ginoza = 'Nobuchika Ginoza', + Kogami = 'Shinya Kogami', + Masaoka = 'Tomomi Masaoka', + /* Victims */ + Makishima = 'Shogo Makishima', + } + `, + options: [ + { + ...options, + 'partition-by-comment': [ + 'Public Safety Bureau', + 'Crime Coefficient: *', + 'Victims', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Kogami', + right: 'Ginoza', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: sorting by natural order`, () => { @@ -489,6 +645,162 @@ describe(RULE_NAME, () => { invalid: [], }, ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + enum HeroAssociation { + // Part: S-Class + Blast = 'Blast', + Tatsumaki = 'Tatsumaki', + // Atomic Samurai + Kamikaze = 'Kamikaze', + // Part: A-Class + Sweet = 'Sweet Mask', + Iaian = 'Iaian', + // Part: B-Class + 'Mountain-Ape' = 'Mountain Ape', + // Member of the Blizzard Group + Eyelashes = 'Eyelashes', + } + `, + output: dedent` + enum HeroAssociation { + // Part: S-Class + Blast = 'Blast', + // Atomic Samurai + Kamikaze = 'Kamikaze', + Tatsumaki = 'Tatsumaki', + // Part: A-Class + Iaian = 'Iaian', + Sweet = 'Sweet Mask', + // Part: B-Class + // Member of the Blizzard Group + Eyelashes = 'Eyelashes', + 'Mountain-Ape' = 'Mountain Ape', + } + `, + options: [ + { + ...options, + 'partition-by-comment': 'Part**', + }, + ], + errors: [ + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Tatsumaki', + right: 'Kamikaze', + }, + }, + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Sweet', + right: 'Iaian', + }, + }, + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Mountain-Ape', + right: 'Eyelashes', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use all comments as parts`, + rule, + { + valid: [ + { + code: dedent` + enum Brothers { + // Older brother + Edward = 'Edward Elric', + // Younger brother + Alphonse = 'Alphonse Elric', + } + `, + options: [ + { + ...options, + 'partition-by-comment': true, + }, + ], + }, + ], + invalid: [], + }, + ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use multiple partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + enum PsychoPass { + /* Public Safety Bureau */ + // Crime Coefficient: Low + Tsunemori = 'Akane Tsunemori', + // Crime Coefficient: High + Kogami = 'Shinya Kogami', + Ginoza = 'Nobuchika Ginoza', + Masaoka = 'Tomomi Masaoka', + /* Victims */ + Makishima = 'Shogo Makishima', + } + `, + output: dedent` + enum PsychoPass { + /* Public Safety Bureau */ + // Crime Coefficient: Low + Tsunemori = 'Akane Tsunemori', + // Crime Coefficient: High + Ginoza = 'Nobuchika Ginoza', + Kogami = 'Shinya Kogami', + Masaoka = 'Tomomi Masaoka', + /* Victims */ + Makishima = 'Shogo Makishima', + } + `, + options: [ + { + ...options, + 'partition-by-comment': [ + 'Public Safety Bureau', + 'Crime Coefficient: *', + 'Victims', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Kogami', + right: 'Ginoza', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: sorting by line length`, () => { @@ -732,6 +1044,148 @@ describe(RULE_NAME, () => { invalid: [], }, ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + enum HeroAssociation { + // Part: S-Class + Blast = 'Blast', + Tatsumaki = 'Tatsumaki', + // Atomic Samurai + Kamikaze = 'Kamikaze', + // Part: A-Class + Sweet = 'Sweet Mask', + Iaian = 'Iaian', + // Part: B-Class + 'Mountain-Ape' = 'Mountain Ape', + // Member of the Blizzard Group + Eyelashes = 'Eyelashes', + } + `, + output: dedent` + enum HeroAssociation { + // Part: S-Class + Tatsumaki = 'Tatsumaki', + // Atomic Samurai + Kamikaze = 'Kamikaze', + Blast = 'Blast', + // Part: A-Class + Sweet = 'Sweet Mask', + Iaian = 'Iaian', + // Part: B-Class + 'Mountain-Ape' = 'Mountain Ape', + // Member of the Blizzard Group + Eyelashes = 'Eyelashes', + } + `, + options: [ + { + ...options, + 'partition-by-comment': 'Part**', + }, + ], + errors: [ + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Blast', + right: 'Tatsumaki', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use all comments as parts`, + rule, + { + valid: [ + { + code: dedent` + enum Brothers { + // Older brother + Edward = 'Edward Elric', + // Younger brother + Alphonse = 'Alphonse Elric', + } + `, + options: [ + { + ...options, + 'partition-by-comment': true, + }, + ], + }, + ], + invalid: [], + }, + ) + + ruleTester.run( + `${RULE_NAME}(${type}): allows to use multiple partition comments`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + enum PsychoPass { + /* Public Safety Bureau */ + // Crime Coefficient: Low + Tsunemori = 'Akane Tsunemori', + // Crime Coefficient: High + Kogami = 'Shinya Kogami', + Ginoza = 'Nobuchika Ginoza', + Masaoka = 'Tomomi Masaoka', + /* Victims */ + Makishima = 'Shogo Makishima', + } + `, + output: dedent` + enum PsychoPass { + /* Public Safety Bureau */ + // Crime Coefficient: Low + Tsunemori = 'Akane Tsunemori', + // Crime Coefficient: High + Ginoza = 'Nobuchika Ginoza', + Masaoka = 'Tomomi Masaoka', + Kogami = 'Shinya Kogami', + /* Victims */ + Makishima = 'Shogo Makishima', + } + `, + options: [ + { + ...options, + 'partition-by-comment': [ + 'Public Safety Bureau', + 'Crime Coefficient: *', + 'Victims', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedEnumsOrder', + data: { + left: 'Kogami', + right: 'Ginoza', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: misc`, () => { From a1a2ef69fa5a6f2436f89cc3927609cdad059d18 Mon Sep 17 00:00:00 2001 From: chirokas Date: Wed, 13 Mar 2024 15:27:31 +0800 Subject: [PATCH 2/2] docs: update `sort-enums.md` --- docs/rules/sort-enums.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/rules/sort-enums.md b/docs/rules/sort-enums.md index 3d3e22b3a..840447eea 100644 --- a/docs/rules/sort-enums.md +++ b/docs/rules/sort-enums.md @@ -96,6 +96,14 @@ interface Options { Only affects alphabetical and natural sorting. When `true` the rule ignores the case-sensitivity of the order. +### partition-by-comment + +(default: `false`) + +You can set comments that would separate the members of enums into logical parts. If set to `true`, all enum member comments will be treated as delimiters. + +The [minimatch](https://github.com/isaacs/minimatch) library is used for pattern matching. + ## ⚙️ Usage ::: code-group