From ae020093dfeb27c7725d0868a9ec06cfebdd661a Mon Sep 17 00:00:00 2001 From: "Azat S." Date: Sat, 7 Sep 2024 21:07:16 +0700 Subject: [PATCH] feat: add ability to disable or enable side effect imports sorting --- docs/content/rules/sort-imports.mdx | 9 + rules/sort-imports.ts | 24 +- test/sort-imports.test.ts | 393 ++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+), 5 deletions(-) diff --git a/docs/content/rules/sort-imports.mdx b/docs/content/rules/sort-imports.mdx index 4955c78d..e452fb36 100644 --- a/docs/content/rules/sort-imports.mdx +++ b/docs/content/rules/sort-imports.mdx @@ -153,6 +153,15 @@ Allows you to specify a pattern for identifying internal imports. This is useful You can use glob patterns to define these internal imports. +### sortSideEffects + +default: `false` + +Specifies whether side effect imports should be sorted. By default, sorting side-effect imports is disabled for security reasons. + +- `true` — Sort side effect imports. +- `false` — Do not sort side effect imports. + ### newlinesBetween default: `'always'` diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts index fc9ace45..6a01b539 100644 --- a/rules/sort-imports.ts +++ b/rules/sort-imports.ts @@ -59,6 +59,7 @@ type Options = [ groups: (Group[] | Group)[] environment: 'node' | 'bun' internalPattern: string[] + sortSideEffects: boolean maxLineLength?: number order: 'desc' | 'asc' ignoreCase: boolean @@ -101,6 +102,11 @@ export default createEslintRule, MESSAGE_ID>({ }, type: 'array', }, + sortSideEffects: { + description: + 'Controls whether side-effect imports should be sorted.', + type: 'boolean', + }, newlinesBetween: { description: 'Specifies how new lines should be handled between import groups.', @@ -197,6 +203,7 @@ export default createEslintRule, MESSAGE_ID>({ order: 'asc', ignoreCase: true, internalPattern: ['~/**'], + sortSideEffects: false, newlinesBetween: 'always', maxLineLength: undefined, groups: [ @@ -230,6 +237,7 @@ export default createEslintRule, MESSAGE_ID>({ customGroups: { type: {}, value: {} }, internalPattern: ['~/**'], newlinesBetween: 'always', + sortSideEffects: false, type: 'alphabetical', environment: 'node', ignoreCase: true, @@ -524,10 +532,14 @@ export default createEslintRule, MESSAGE_ID>({ if (!(groupNum in grouped)) { grouped[groupNum] = [node] } else { - grouped[groupNum] = sortNodes( - [...grouped[groupNum], node], - options, - ) + if (!options.sortSideEffects && isSideEffectImport(node.node)) { + grouped[groupNum] = [...grouped[groupNum], node] + } else { + grouped[groupNum] = sortNodes( + [...grouped[groupNum], node], + options, + ) + } } } @@ -643,7 +655,9 @@ export default createEslintRule, MESSAGE_ID>({ if ( !( - isSideEffectImport(left.node) && isSideEffectImport(right.node) + !options.sortSideEffects && + isSideEffectImport(left.node) && + isSideEffectImport(right.node) ) && !hasContentBetweenNodes(left, right) && (leftNum > rightNum || diff --git a/test/sort-imports.test.ts b/test/sort-imports.test.ts index 53132be4..72a1ccbf 100644 --- a/test/sort-imports.test.ts +++ b/test/sort-imports.test.ts @@ -1370,6 +1370,137 @@ describe(ruleName, () => { ], }, ) + + ruleTester.run( + `${ruleName}(${type}): can enable or disable sorting side effect imports`, + rule, + { + valid: [ + { + code: dedent` + import a from 'aaaa' + + import 'bbb' + import './cc' + import '../d' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + }, + { + code: dedent` + import 'c' + import 'bb' + import 'aaa' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + }, + ], + invalid: [ + { + code: dedent` + import './cc' + import 'bbb' + import e from 'e' + import a from 'aaaa' + import '../d' + `, + output: dedent` + import a from 'aaaa' + import e from 'e' + + import './cc' + import 'bbb' + import '../d' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'bbb', + right: 'e', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'e', + right: 'aaaa', + }, + }, + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'aaaa', + right: '../d', + }, + }, + ], + }, + { + code: dedent` + import 'c' + import 'bb' + import 'aaa' + `, + output: dedent` + import 'aaa' + import 'bb' + import 'c' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: true, + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'c', + right: 'bb', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'bb', + right: 'aaa', + }, + }, + ], + }, + ], + }, + ) }) describe(`${ruleName}: sorting by natural order`, () => { @@ -2726,6 +2857,137 @@ describe(ruleName, () => { ], }, ) + + ruleTester.run( + `${ruleName}(${type}): can enable or disable sorting side effect imports`, + rule, + { + valid: [ + { + code: dedent` + import a from 'aaaa' + + import 'bbb' + import './cc' + import '../d' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + }, + { + code: dedent` + import 'c' + import 'bb' + import 'aaa' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + }, + ], + invalid: [ + { + code: dedent` + import './cc' + import 'bbb' + import e from 'e' + import a from 'aaaa' + import '../d' + `, + output: dedent` + import a from 'aaaa' + import e from 'e' + + import './cc' + import 'bbb' + import '../d' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'bbb', + right: 'e', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'e', + right: 'aaaa', + }, + }, + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'aaaa', + right: '../d', + }, + }, + ], + }, + { + code: dedent` + import 'c' + import 'bb' + import 'aaa' + `, + output: dedent` + import 'aaa' + import 'bb' + import 'c' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: true, + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'c', + right: 'bb', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'bb', + right: 'aaa', + }, + }, + ], + }, + ], + }, + ) }) describe(`${ruleName}: sorting by line length`, () => { @@ -4177,6 +4439,137 @@ describe(ruleName, () => { ], }, ) + + ruleTester.run( + `${ruleName}(${type}): can enable or disable sorting side effect imports`, + rule, + { + valid: [ + { + code: dedent` + import a from 'aaaa' + + import 'bbb' + import './cc' + import '../d' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + }, + { + code: dedent` + import 'c' + import 'bb' + import 'aaa' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + }, + ], + invalid: [ + { + code: dedent` + import './cc' + import 'bbb' + import e from 'e' + import a from 'aaaa' + import '../d' + `, + output: dedent` + import a from 'aaaa' + import e from 'e' + + import './cc' + import 'bbb' + import '../d' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'bbb', + right: 'e', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'e', + right: 'aaaa', + }, + }, + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'aaaa', + right: '../d', + }, + }, + ], + }, + { + code: dedent` + import 'c' + import 'bb' + import 'aaa' + `, + output: dedent` + import 'aaa' + import 'bb' + import 'c' + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: true, + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'c', + right: 'bb', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'bb', + right: 'aaa', + }, + }, + ], + }, + ], + }, + ) }) describe(`${ruleName}: validating group configuration`, () => {