From 6437ada96df40ac26a3caadb1877f99be7154bcb Mon Sep 17 00:00:00 2001 From: "Xunnamius (Romulus)" Date: Sat, 16 Nov 2024 13:35:59 -0800 Subject: [PATCH] feat: [import/order] allow intragroup sorting of type-only imports via `sortTypesAmongThemselves` Closes #2912 #2347 #2441 Subsumes #2615 --- src/rules/order.js | 33 +++++-- tests/src/rules/order.js | 182 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 5 deletions(-) diff --git a/src/rules/order.js b/src/rules/order.js index d6f25ddd3..c2791744f 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -513,22 +513,34 @@ function computePathRank(ranks, pathGroups, path, maxPosition) { } } -function computeRank(context, ranks, importEntry, excludedImportTypes) { +function computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesAmongThemselves) { let impType; let rank; + + const isTypeGroupInGroups = ranks.omittedTypes.indexOf('type') === -1; + const isTypeOnlyImport = importEntry.node.importKind === 'type'; + const isExcludedFromPathRank = isTypeOnlyImport && isTypeGroupInGroups && excludedImportTypes.has('type') + if (importEntry.type === 'import:object') { impType = 'object'; - } else if (importEntry.node.importKind === 'type' && ranks.omittedTypes.indexOf('type') === -1) { + } else if (isTypeOnlyImport && isTypeGroupInGroups && !isSortingTypesAmongThemselves) { impType = 'type'; } else { impType = importType(importEntry.value, context); } - if (!excludedImportTypes.has(impType)) { + + if (!excludedImportTypes.has(impType) && !isExcludedFromPathRank) { rank = computePathRank(ranks.groups, ranks.pathGroups, importEntry.value, ranks.maxPosition); } + if (typeof rank === 'undefined') { rank = ranks.groups[impType]; } + + if (isTypeOnlyImport && isSortingTypesAmongThemselves) { + rank = ranks.groups['type'] + rank / 10; + } + if (importEntry.type !== 'import' && !importEntry.type.startsWith('import:')) { rank += 100; } @@ -536,8 +548,8 @@ function computeRank(context, ranks, importEntry, excludedImportTypes) { return rank; } -function registerNode(context, importEntry, ranks, imported, excludedImportTypes) { - const rank = computeRank(context, ranks, importEntry, excludedImportTypes); +function registerNode(context, importEntry, ranks, imported, excludedImportTypes, isSortingTypesAmongThemselves) { + const rank = computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesAmongThemselves); if (rank !== -1) { imported.push({ ...importEntry, rank }); } @@ -781,6 +793,10 @@ module.exports = { 'never', ], }, + sortTypesAmongThemselves: { + type: 'boolean', + default: false, + }, named: { default: false, oneOf: [{ @@ -837,6 +853,7 @@ module.exports = { const options = context.options[0] || {}; const newlinesBetweenImports = options['newlines-between'] || 'ignore'; const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']); + const sortTypesAmongThemselves = options.sortTypesAmongThemselves; const named = { types: 'mixed', @@ -879,6 +896,9 @@ module.exports = { const importMap = new Map(); const exportMap = new Map(); + const isTypeGroupInGroups = ranks.omittedTypes.indexOf('type') === -1; + const isSortingTypesAmongThemselves = isTypeGroupInGroups && sortTypesAmongThemselves; + function getBlockImports(node) { if (!importMap.has(node)) { importMap.set(node, []); @@ -932,6 +952,7 @@ module.exports = { ranks, getBlockImports(node.parent), pathGroupsExcludedImportTypes, + isSortingTypesAmongThemselves ); if (named.import) { @@ -983,6 +1004,7 @@ module.exports = { ranks, getBlockImports(node.parent), pathGroupsExcludedImportTypes, + isSortingTypesAmongThemselves ); }, CallExpression(node) { @@ -1005,6 +1027,7 @@ module.exports = { ranks, getBlockImports(block), pathGroupsExcludedImportTypes, + isSortingTypesAmongThemselves ); }, ...named.require && { diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index ea62cec71..5036aba9e 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -3285,6 +3285,188 @@ context('TypeScript', function () { }], }), ] : [], + // Option sortTypesAmongThemselves: false (default) + test({ + code: ` + import c from 'Bar'; + import a from 'foo'; + + import type { C } from 'dirA/Bar'; + import b from 'dirA/bar'; + import type { D } from 'dirA/bar'; + + import index from './'; + + import type { AA } from 'abc'; + import type { A } from 'foo'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + }, + ], + 'newlines-between': 'always', + pathGroupsExcludedImportTypes: [], + }, + ], + }), + test({ + code: ` + import c from 'Bar'; + import a from 'foo'; + + import type { C } from 'dirA/Bar'; + import b from 'dirA/bar'; + import type { D } from 'dirA/bar'; + + import index from './'; + + import type { AA } from 'abc'; + import type { A } from 'foo'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + }, + ], + 'newlines-between': 'always', + pathGroupsExcludedImportTypes: [], + sortTypesAmongThemselves: false, + }, + ], + }), + // Option sortTypesAmongThemselves: true and 'type' in pathGroupsExcludedImportTypes + test({ + code: ` + import c from 'Bar'; + import a from 'foo'; + + import b from 'dirA/bar'; + + import index from './'; + + import type { AA } from 'abc'; + import type { C } from 'dirA/Bar'; + import type { D } from 'dirA/bar'; + import type { A } from 'foo'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + }, + ], + 'newlines-between': 'always', + pathGroupsExcludedImportTypes: ['type'], + sortTypesAmongThemselves: true, + }, + ], + }), + // Option sortTypesAmongThemselves: true and 'type' omitted from groups + test({ + code: ` + import c from 'Bar'; + import type { AA } from 'abc'; + import a from 'foo'; + import type { A } from 'foo'; + + import type { C } from 'dirA/Bar'; + import b from 'dirA/bar'; + import type { D } from 'dirA/bar'; + + import index from './'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + }, + ], + 'newlines-between': 'always', + pathGroupsExcludedImportTypes: [], + // Becomes a no-op without "type" in groups + sortTypesAmongThemselves: true, + }, + ], + }), + test({ + code: ` + import c from 'Bar'; + import type { AA } from 'abc'; + import a from 'foo'; + import type { A } from 'foo'; + + import type { C } from 'dirA/Bar'; + import b from 'dirA/bar'; + import type { D } from 'dirA/bar'; + + import index from './'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + }, + ], + 'newlines-between': 'always', + pathGroupsExcludedImportTypes: [], + }, + ], + }), + // Option: sortTypesAmongThemselves: true puts type imports in the same order as regular imports (from issue #2441, PR #2615) + test({ + code: ` + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + + import a from "fs"; + import b from "path"; + import c from "../foo.js"; + import d from "./bar.js"; + import e from "./"; + `, + ...parserConfig, + options: [ + { + groups: ['type', 'builtin', 'parent', 'sibling', 'index'], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + sortTypesAmongThemselves: true, + }, + ], + }), ), invalid: [].concat( // Option alphabetize: {order: 'asc'}