Skip to content

Commit fd1ae63

Browse files
Xunnamiusljharb
authored andcommitted
[New] order: add sortTypesGroup option to allow intragroup sorting of type-only imports
Closes #2912 Closes #2347 Closes #2441 Subsumes #2615
1 parent 341178d commit fd1ae63

File tree

4 files changed

+274
-7
lines changed

4 files changed

+274
-7
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1010
- add [`enforce-node-protocol-usage`] rule and `import/node-version` setting ([#3024], thanks [@GoldStrikeArch] and [@sevenc-nanashi])
1111
- add TypeScript types ([#3097], thanks [@G-Rath])
1212
- [`extensions`]: add `pathGroupOverrides to allow enforcement decision overrides based on specifier ([#3105], thanks [@Xunnamius])
13+
- [`order`]: add `sortTypesGroup` option to allow intragroup sorting of type-only imports ([#3104], thanks [@Xunnamius])
1314

1415
### Fixed
1516
- [`no-unused-modules`]: provide more meaningful error message when no .eslintrc is present ([#3116], thanks [@michaelfaith])
@@ -1174,6 +1175,7 @@ for info on changes for earlier releases.
11741175
[#3116]: https://github.com/import-js/eslint-plugin-import/pull/3116
11751176
[#3106]: https://github.com/import-js/eslint-plugin-import/pull/3106
11761177
[#3105]: https://github.com/import-js/eslint-plugin-import/pull/3105
1178+
[#3104]: https://github.com/import-js/eslint-plugin-import/pull/3104
11771179
[#3097]: https://github.com/import-js/eslint-plugin-import/pull/3097
11781180
[#3073]: https://github.com/import-js/eslint-plugin-import/pull/3073
11791181
[#3072]: https://github.com/import-js/eslint-plugin-import/pull/3072

docs/rules/order.md

+62-2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ This rule supports the following options (none of which are required):
106106
- [`alphabetize`][30]
107107
- [`named`][33]
108108
- [`warnOnUnassignedImports`][5]
109+
- [`sortTypesGroup`][7]
109110

110111
---
111112

@@ -156,7 +157,7 @@ Roughly speaking, the grouping algorithm is as follows:
156157

157158
1. If the import has no corresponding identifiers (e.g. `import './my/thing.js'`), is otherwise "unassigned," or is an unsupported use of `require()`, and [`warnOnUnassignedImports`][5] is disabled, it will be ignored entirely since the order of these imports may be important for their [side-effects][31]
158159
2. If the import is part of an arcane TypeScript declaration (e.g. `import log = console.log`), it will be considered **object**. However, note that external module references (e.g. `import x = require('z')`) are treated as normal `require()`s and import-exports (e.g. `export import w = y;`) are ignored entirely
159-
3. If the import is [type-only][6], and `"type"` is in `groups`, it will be considered **type** (with additional implications if using [`pathGroups`][8] and `"type"` is in [`pathGroupsExcludedImportTypes`][9])
160+
3. If the import is [type-only][6], `"type"` is in `groups`, and [`sortTypesGroup`][7] is disabled, it will be considered **type** (with additional implications if using [`pathGroups`][8] and `"type"` is in [`pathGroupsExcludedImportTypes`][9])
160161
4. If the import's specifier matches [`import/internal-regex`][28], it will be considered **internal**
161162
5. If the import's specifier is an absolute path, it will be considered **unknown**
162163
6. If the import's specifier has the name of a Node.js core module (using [is-core-module][10]), it will be considered **builtin**
@@ -171,7 +172,7 @@ Roughly speaking, the grouping algorithm is as follows:
171172
15. If the import's specifier has a name that starts with a word character, it will be considered **external**
172173
16. If this point is reached, the import will be ignored entirely
173174

174-
At the end of the process, if they co-exist in the same file, all top-level `require()` statements that haven't been ignored are shifted (with respect to their order) below any ES6 `import` or similar declarations.
175+
At the end of the process, if they co-exist in the same file, all top-level `require()` statements that haven't been ignored are shifted (with respect to their order) below any ES6 `import` or similar declarations. Finally, any type-only declarations are potentially reorganized according to [`sortTypesGroup`][7].
175176

176177
### `pathGroups`
177178

@@ -533,6 +534,64 @@ import path from 'path';
533534
import './styles.css';
534535
```
535536

537+
### `sortTypesGroup`
538+
539+
Valid values: `boolean` \
540+
Default: `false`
541+
542+
> \[!NOTE]
543+
>
544+
> This setting is only meaningful when `"type"` is included in [`groups`][18].
545+
546+
Sort [type-only imports][6] separately from normal non-type imports.
547+
548+
When enabled, the intragroup sort order of [type-only imports][6] will mirror the intergroup ordering of normal imports as defined by [`groups`][18], [`pathGroups`][8], etc.
549+
550+
#### Example
551+
552+
Given the following settings:
553+
554+
```jsonc
555+
{
556+
"import/order": ["error", {
557+
"groups": ["type", "builtin", "parent", "sibling", "index"],
558+
"alphabetize": { "order": "asc" }
559+
}]
560+
}
561+
```
562+
563+
This will fail the rule check even though it's logically ordered as we expect (builtins come before parents, parents come before siblings, siblings come before indices), the only difference is we separated type-only imports from normal imports:
564+
565+
```ts
566+
import type A from "fs";
567+
import type B from "path";
568+
import type C from "../foo.js";
569+
import type D from "./bar.js";
570+
import type E from './';
571+
572+
import a from "fs";
573+
import b from "path";
574+
import c from "../foo.js";
575+
import d from "./bar.js";
576+
import e from "./";
577+
```
578+
579+
This happens because [type-only imports][6] are considered part of one global
580+
[`"type"` group](#how-imports-are-grouped) by default. However, if we set
581+
`sortTypesGroup` to `true`:
582+
583+
```jsonc
584+
{
585+
"import/order": ["error", {
586+
"groups": ["type", "builtin", "parent", "sibling", "index"],
587+
"alphabetize": { "order": "asc" },
588+
"sortTypesGroup": true
589+
}]
590+
}
591+
```
592+
593+
The same example will pass.
594+
536595
## Related
537596

538597
- [`import/external-module-folders`][29]
@@ -543,6 +602,7 @@ import './styles.css';
543602
[4]: https://nodejs.org/api/esm.html#terminology
544603
[5]: #warnonunassignedimports
545604
[6]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
605+
[7]: #sorttypesgroup
546606
[8]: #pathgroups
547607
[9]: #pathgroupsexcludedimporttypes
548608
[10]: https://www.npmjs.com/package/is-core-module

src/rules/order.js

+28-5
Original file line numberDiff line numberDiff line change
@@ -513,31 +513,43 @@ function computePathRank(ranks, pathGroups, path, maxPosition) {
513513
}
514514
}
515515

516-
function computeRank(context, ranks, importEntry, excludedImportTypes) {
516+
function computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesGroup) {
517517
let impType;
518518
let rank;
519+
520+
const isTypeGroupInGroups = ranks.omittedTypes.indexOf('type') === -1;
521+
const isTypeOnlyImport = importEntry.node.importKind === 'type';
522+
const isExcludedFromPathRank = isTypeOnlyImport && isTypeGroupInGroups && excludedImportTypes.has('type');
523+
519524
if (importEntry.type === 'import:object') {
520525
impType = 'object';
521-
} else if (importEntry.node.importKind === 'type' && ranks.omittedTypes.indexOf('type') === -1) {
526+
} else if (isTypeOnlyImport && isTypeGroupInGroups && !isSortingTypesGroup) {
522527
impType = 'type';
523528
} else {
524529
impType = importType(importEntry.value, context);
525530
}
526-
if (!excludedImportTypes.has(impType)) {
531+
532+
if (!excludedImportTypes.has(impType) && !isExcludedFromPathRank) {
527533
rank = computePathRank(ranks.groups, ranks.pathGroups, importEntry.value, ranks.maxPosition);
528534
}
535+
529536
if (typeof rank === 'undefined') {
530537
rank = ranks.groups[impType];
531538
}
539+
540+
if (isTypeOnlyImport && isSortingTypesGroup) {
541+
rank = ranks.groups.type + rank / 10;
542+
}
543+
532544
if (importEntry.type !== 'import' && !importEntry.type.startsWith('import:')) {
533545
rank += 100;
534546
}
535547

536548
return rank;
537549
}
538550

539-
function registerNode(context, importEntry, ranks, imported, excludedImportTypes) {
540-
const rank = computeRank(context, ranks, importEntry, excludedImportTypes);
551+
function registerNode(context, importEntry, ranks, imported, excludedImportTypes, isSortingTypesGroup) {
552+
const rank = computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesGroup);
541553
if (rank !== -1) {
542554
imported.push({ ...importEntry, rank });
543555
}
@@ -781,6 +793,10 @@ module.exports = {
781793
'never',
782794
],
783795
},
796+
sortTypesGroup: {
797+
type: 'boolean',
798+
default: false,
799+
},
784800
named: {
785801
default: false,
786802
oneOf: [{
@@ -837,6 +853,7 @@ module.exports = {
837853
const options = context.options[0] || {};
838854
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
839855
const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']);
856+
const sortTypesGroup = options.sortTypesGroup;
840857

841858
const named = {
842859
types: 'mixed',
@@ -879,6 +896,9 @@ module.exports = {
879896
const importMap = new Map();
880897
const exportMap = new Map();
881898

899+
const isTypeGroupInGroups = ranks.omittedTypes.indexOf('type') === -1;
900+
const isSortingTypesGroup = isTypeGroupInGroups && sortTypesGroup;
901+
882902
function getBlockImports(node) {
883903
if (!importMap.has(node)) {
884904
importMap.set(node, []);
@@ -932,6 +952,7 @@ module.exports = {
932952
ranks,
933953
getBlockImports(node.parent),
934954
pathGroupsExcludedImportTypes,
955+
isSortingTypesGroup,
935956
);
936957

937958
if (named.import) {
@@ -983,6 +1004,7 @@ module.exports = {
9831004
ranks,
9841005
getBlockImports(node.parent),
9851006
pathGroupsExcludedImportTypes,
1007+
isSortingTypesGroup,
9861008
);
9871009
},
9881010
CallExpression(node) {
@@ -1005,6 +1027,7 @@ module.exports = {
10051027
ranks,
10061028
getBlockImports(block),
10071029
pathGroupsExcludedImportTypes,
1030+
isSortingTypesGroup,
10081031
);
10091032
},
10101033
...named.require && {

0 commit comments

Comments
 (0)