From 41e18b9934af5ebdd1d201ee55fa06bb40cbe8ac Mon Sep 17 00:00:00 2001
From: Hugo <60015232+hugop95@users.noreply.github.com>
Date: Sun, 22 Sep 2024 16:06:37 +0200
Subject: [PATCH] feat: add partition by comment and partition by new line in
sort-named-imports
---
docs/content/rules/sort-named-imports.mdx | 42 +++++
rules/sort-named-imports.ts | 112 +++++++++---
test/sort-named-imports.test.ts | 204 ++++++++++++++++++++++
3 files changed, 329 insertions(+), 29 deletions(-)
diff --git a/docs/content/rules/sort-named-imports.mdx b/docs/content/rules/sort-named-imports.mdx
index 1caa4f49..96d17412 100644
--- a/docs/content/rules/sort-named-imports.mdx
+++ b/docs/content/rules/sort-named-imports.mdx
@@ -145,6 +145,44 @@ Allows you to group named imports by their kind, determining whether value impor
- `values-first` — Group all value imports before type imports.
- `types-first` — Group all type imports before value imports.
+### partitionByComment
+
+default: `false`
+
+Allows you to use comments to separate the members of named imports 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 named imports if there is an empty line between them. This can be useful for keeping logically separated groups of members in their defined order.
+
+```ts
+import {
+ // Group 1
+ Drone,
+ Keyboard,
+ Mouse,
+ Smartphone,
+
+ // Group 2
+ Laptop,
+ Monitor,
+ Smartwatch,
+ Tablet,
+
+ // Group 3
+ Headphones,
+ Router,
+} from './devices'
+```
+
+
## Usage
({
enum: ['mixed', 'values-first', 'types-first'],
type: 'string',
},
+ partitionByComment: {
+ description:
+ 'Allows you to use comments to separate the named imports 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,
},
@@ -76,6 +104,8 @@ export default createEslintRule({
order: 'asc',
ignoreAlias: false,
ignoreCase: true,
+ partitionByNewLine: false,
+ partitionByComment: false,
groupKind: 'mixed',
},
],
@@ -93,12 +123,16 @@ export default createEslintRule({
ignoreAlias: false,
groupKind: 'mixed',
ignoreCase: true,
+ partitionByNewLine: false,
+ partitionByComment: false,
order: 'asc',
} as const)
let sourceCode = getSourceCode(context)
+ let partitionComment = options.partitionByComment
- let nodes: SortingNode[] = specifiers.map(specifier => {
+ let formattedMembers: SortingNode[][] = [[]]
+ for (let specifier of specifiers) {
let group: undefined | 'value' | 'type'
let { name } = specifier.local
@@ -115,13 +149,29 @@ export default createEslintRule({
group = 'value'
}
- return {
+ let lastSortingNode = formattedMembers.at(-1)?.at(-1)
+ let sortingNode: SortingNode = {
size: rangeToDiff(specifier.range),
node: specifier,
group,
name,
}
- })
+
+ if (
+ (partitionComment &&
+ hasPartitionComment(
+ partitionComment,
+ getCommentsBefore(specifier, sourceCode),
+ )) ||
+ (options.partitionByNewLine &&
+ lastSortingNode &&
+ getLinesBetween(sourceCode, lastSortingNode, sortingNode))
+ ) {
+ formattedMembers.push([])
+ }
+
+ formattedMembers.at(-1)!.push(sortingNode)
+ }
let shouldGroupByKind = options.groupKind !== 'mixed'
let groupKindOrder =
@@ -129,33 +179,37 @@ export default createEslintRule({
? ['value', 'type']
: ['type', 'value']
- pairwise(nodes, (left, right) => {
- let leftNum = getGroupNumber(groupKindOrder, left)
- let rightNum = getGroupNumber(groupKindOrder, right)
+ for (let nodes of formattedMembers) {
+ pairwise(nodes, (left, right) => {
+ let leftNum = getGroupNumber(groupKindOrder, left)
+ let rightNum = getGroupNumber(groupKindOrder, right)
+ if (
+ (shouldGroupByKind && leftNum > rightNum) ||
+ ((!shouldGroupByKind || leftNum === rightNum) &&
+ isPositive(compare(left, right, options)))
+ ) {
+ let sortedNodes = shouldGroupByKind
+ ? groupKindOrder
+ .map(group => nodes.filter(n => n.group === group))
+ .map(groupedNodes => sortNodes(groupedNodes, options))
+ .flat()
+ : sortNodes(nodes, options)
- if (
- (shouldGroupByKind && leftNum > rightNum) ||
- ((!shouldGroupByKind || leftNum === rightNum) &&
- isPositive(compare(left, right, options)))
- ) {
- let sortedNodes = shouldGroupByKind
- ? groupKindOrder
- .map(group => nodes.filter(n => n.group === group))
- .map(groupedNodes => sortNodes(groupedNodes, options))
- .flat()
- : sortNodes(nodes, options)
-
- context.report({
- messageId: 'unexpectedNamedImportsOrder',
- data: {
- left: left.name,
- right: right.name,
- },
- node: right.node,
- fix: fixer => makeFixes(fixer, nodes, sortedNodes, sourceCode),
- })
- }
- })
+ context.report({
+ messageId: 'unexpectedNamedImportsOrder',
+ data: {
+ left: left.name,
+ right: right.name,
+ },
+ node: right.node,
+ fix: fixer =>
+ makeFixes(fixer, nodes, sortedNodes, sourceCode, {
+ partitionComment,
+ }),
+ })
+ }
+ })
+ }
}
},
}),
diff --git a/test/sort-named-imports.test.ts b/test/sort-named-imports.test.ts
index 2e3dc279..2fe505af 100644
--- a/test/sort-named-imports.test.ts
+++ b/test/sort-named-imports.test.ts
@@ -400,6 +400,210 @@ describe(ruleName, () => {
},
],
})
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to use new line as partition`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import {
+ D,
+ A,
+
+ C,
+
+ E,
+ B,
+ } from 'module'
+ `,
+ output: dedent`
+ import {
+ A,
+ D,
+
+ C,
+
+ B,
+ E,
+ } from 'module'
+ `,
+ options: [
+ {
+ ...options,
+ partitionByNewLine: true,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedNamedImportsOrder',
+ data: {
+ left: 'D',
+ right: 'A',
+ },
+ },
+ {
+ messageId: 'unexpectedNamedImportsOrder',
+ data: {
+ left: 'E',
+ right: 'B',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ describe('partition comments', () => {
+ ruleTester.run(
+ `${ruleName}(${type}): allows to use partition comments`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import {
+ // Part: A
+ CC,
+ type D,
+ // Not partition comment
+ BBB,
+ // Part: B
+ AAAA,
+ E,
+ // Part: C
+ GG,
+ // Not partition comment
+ FFF,
+ } from 'module'
+ `,
+ output: dedent`
+ import {
+ // Part: A
+ type D,
+ // Not partition comment
+ BBB,
+ CC,
+ // Part: B
+ AAAA,
+ E,
+ // Part: C
+ // Not partition comment
+ FFF,
+ GG,
+ } from 'module'
+ `,
+ options: [
+ {
+ ...options,
+ partitionByComment: 'Part**',
+ groupKind: 'types-first',
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedNamedImportsOrder',
+ data: {
+ left: 'CC',
+ right: 'D',
+ },
+ },
+ {
+ messageId: 'unexpectedNamedImportsOrder',
+ data: {
+ left: 'GG',
+ right: 'FFF',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to use all comments as parts`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ import {
+ // Comment
+ BB,
+ // Other comment
+ A,
+ } from 'module'
+ `,
+ options: [
+ {
+ ...options,
+ partitionByComment: true,
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to use multiple partition comments`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import {
+ /* Partition Comment */
+ // Part: A
+ D,
+ // Part: B
+ AAA,
+ C,
+ BB,
+ /* Other */
+ E,
+ } from 'module'
+ `,
+ output: dedent`
+ import {
+ /* Partition Comment */
+ // Part: A
+ D,
+ // Part: B
+ AAA,
+ BB,
+ C,
+ /* Other */
+ E,
+ } from 'module'
+ `,
+ options: [
+ {
+ ...options,
+ partitionByComment: ['Partition Comment', 'Part: *', 'Other'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedNamedImportsOrder',
+ data: {
+ left: 'C',
+ right: 'BB',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ })
})
describe(`${ruleName}: sorting by natural order`, () => {