Skip to content

Commit 2f2ea6d

Browse files
Xunnamiusljharb
authored andcommitted
[New] order: add newlines-between-types option to control intragroup sorting of type-only imports
Closes #2912 Closes #2347 Closes #2441 Subsumes #2615
1 parent fd1ae63 commit 2f2ea6d

File tree

4 files changed

+595
-33
lines changed

4 files changed

+595
-33
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1111
- add TypeScript types ([#3097], thanks [@G-Rath])
1212
- [`extensions`]: add `pathGroupOverrides to allow enforcement decision overrides based on specifier ([#3105], thanks [@Xunnamius])
1313
- [`order`]: add `sortTypesGroup` option to allow intragroup sorting of type-only imports ([#3104], thanks [@Xunnamius])
14+
- [`order`]: add `newlines-between-types` option to control intragroup sorting of type-only imports ([#3127], thanks [@Xunnamius])
1415

1516
### Fixed
1617
- [`no-unused-modules`]: provide more meaningful error message when no .eslintrc is present ([#3116], thanks [@michaelfaith])
@@ -1170,6 +1171,7 @@ for info on changes for earlier releases.
11701171
[`memo-parser`]: ./memo-parser/README.md
11711172

11721173
[#3138]: https://github.com/import-js/eslint-plugin-import/pull/3138
1174+
[#3127]: https://github.com/import-js/eslint-plugin-import/pull/3127
11731175
[#3125]: https://github.com/import-js/eslint-plugin-import/pull/3125
11741176
[#3122]: https://github.com/import-js/eslint-plugin-import/pull/3122
11751177
[#3116]: https://github.com/import-js/eslint-plugin-import/pull/3116

docs/rules/order.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ This rule supports the following options (none of which are required):
107107
- [`named`][33]
108108
- [`warnOnUnassignedImports`][5]
109109
- [`sortTypesGroup`][7]
110+
- [`newlines-between-types`][27]
110111

111112
---
112113

@@ -592,6 +593,135 @@ This happens because [type-only imports][6] are considered part of one global
592593

593594
The same example will pass.
594595

596+
### `newlines-between-types`
597+
598+
Valid values: `"ignore" | "always" | "always-and-inside-groups" | "never"` \
599+
Default: the value of [`newlines-between`][20]
600+
601+
> \[!NOTE]
602+
>
603+
> This setting is only meaningful when [`sortTypesGroup`][7] is enabled.
604+
605+
`newlines-between-types` is functionally identical to [`newlines-between`][20] except it only enforces or forbids new lines between _[type-only][6] import groups_, which exist only when [`sortTypesGroup`][7] is enabled.
606+
607+
In addition, when determining if a new line is enforceable or forbidden between the type-only imports and the normal imports, `newlines-between-types` takes precedence over [`newlines-between`][20].
608+
609+
#### Example
610+
611+
Given the following settings:
612+
613+
```jsonc
614+
{
615+
"import/order": ["error", {
616+
"groups": ["type", "builtin", "parent", "sibling", "index"],
617+
"sortTypesGroup": true,
618+
"newlines-between": "always"
619+
}]
620+
}
621+
```
622+
623+
This will fail the rule check:
624+
625+
```ts
626+
import type A from "fs";
627+
import type B from "path";
628+
import type C from "../foo.js";
629+
import type D from "./bar.js";
630+
import type E from './';
631+
632+
import a from "fs";
633+
import b from "path";
634+
635+
import c from "../foo.js";
636+
637+
import d from "./bar.js";
638+
639+
import e from "./";
640+
```
641+
642+
However, if we set `newlines-between-types` to `"ignore"`:
643+
644+
```jsonc
645+
{
646+
"import/order": ["error", {
647+
"groups": ["type", "builtin", "parent", "sibling", "index"],
648+
"sortTypesGroup": true,
649+
"newlines-between": "always",
650+
"newlines-between-types": "ignore"
651+
}]
652+
}
653+
```
654+
655+
The same example will pass.
656+
657+
Note the new line after `import type E from './';` but before `import a from "fs";`. This new line separates the type-only imports from the normal imports. Its existence is governed by [`newlines-between-types`][27] and _not `newlines-between`_.
658+
659+
> \[!IMPORTANT]
660+
>
661+
> In certain situations, `consolidateIslands: true` will take precedence over `newlines-between-types: "never"`, if used, when it comes to the new line separating type-only imports from normal imports.
662+
663+
The next example will pass even though there's a new line preceding the normal import and [`newlines-between`][20] is set to `"never"`:
664+
665+
```jsonc
666+
{
667+
"import/order": ["error", {
668+
"groups": ["type", "builtin", "parent", "sibling", "index"],
669+
"sortTypesGroup": true,
670+
"newlines-between": "never",
671+
"newlines-between-types": "always"
672+
}]
673+
}
674+
```
675+
676+
```ts
677+
import type A from "fs";
678+
679+
import type B from "path";
680+
681+
import type C from "../foo.js";
682+
683+
import type D from "./bar.js";
684+
685+
import type E from './';
686+
687+
import a from "fs";
688+
import b from "path";
689+
import c from "../foo.js";
690+
import d from "./bar.js";
691+
import e from "./";
692+
```
693+
694+
While the following fails due to the new line between the last type import and the first normal import:
695+
696+
```jsonc
697+
{
698+
"import/order": ["error", {
699+
"groups": ["type", "builtin", "parent", "sibling", "index"],
700+
"sortTypesGroup": true,
701+
"newlines-between": "always",
702+
"newlines-between-types": "never"
703+
}]
704+
}
705+
```
706+
707+
```ts
708+
import type A from "fs";
709+
import type B from "path";
710+
import type C from "../foo.js";
711+
import type D from "./bar.js";
712+
import type E from './';
713+
714+
import a from "fs";
715+
716+
import b from "path";
717+
718+
import c from "../foo.js";
719+
720+
import d from "./bar.js";
721+
722+
import e from "./";
723+
```
724+
595725
## Related
596726

597727
- [`import/external-module-folders`][29]
@@ -617,6 +747,7 @@ The same example will pass.
617747
[21]: https://eslint.org/docs/latest/rules/no-multiple-empty-lines
618748
[22]: https://prettier.io
619749
[23]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#type-modifiers-on-import-names
750+
[27]: #newlines-between-types
620751
[28]: ../../README.md#importinternal-regex
621752
[29]: ../../README.md#importexternal-module-folders
622753
[30]: #alphabetize

src/rules/order.js

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ const compareString = (a, b) => {
416416
};
417417

418418
/** Some parsers (languages without types) don't provide ImportKind */
419-
const DEAFULT_IMPORT_KIND = 'value';
419+
const DEFAULT_IMPORT_KIND = 'value';
420420
const getNormalizedValue = (node, toLowerCase) => {
421421
const value = node.value;
422422
return toLowerCase ? String(value).toLowerCase() : value;
@@ -462,8 +462,8 @@ function getSorter(alphabetizeOptions) {
462462
// In case the paths are equal (result === 0), sort them by importKind
463463
if (!result && multiplierImportKind) {
464464
result = multiplierImportKind * compareString(
465-
nodeA.node.importKind || DEAFULT_IMPORT_KIND,
466-
nodeB.node.importKind || DEAFULT_IMPORT_KIND,
465+
nodeA.node.importKind || DEFAULT_IMPORT_KIND,
466+
nodeB.node.importKind || DEFAULT_IMPORT_KIND,
467467
);
468468
}
469469

@@ -677,7 +677,7 @@ function removeNewLineAfterImport(context, currentImport, previousImport) {
677677
return undefined;
678678
}
679679

680-
function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup) {
680+
function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, newlinesBetweenTypeOnlyImports, distinctGroup, isSortingTypesGroup) {
681681
const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => {
682682
const linesBetweenImports = getSourceCode(context).lines.slice(
683683
previousImport.node.loc.end.line,
@@ -690,35 +690,72 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, di
690690
let previousImport = imported[0];
691691

692692
imported.slice(1).forEach(function (currentImport) {
693-
const emptyLinesBetween = getNumberOfEmptyLinesBetween(currentImport, previousImport);
694-
const isStartOfDistinctGroup = getIsStartOfDistinctGroup(currentImport, previousImport);
695-
696-
if (newlinesBetweenImports === 'always'
697-
|| newlinesBetweenImports === 'always-and-inside-groups') {
698-
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
699-
if (distinctGroup || !distinctGroup && isStartOfDistinctGroup) {
700-
context.report({
701-
node: previousImport.node,
702-
message: 'There should be at least one empty line between import groups',
703-
fix: fixNewLineAfterImport(context, previousImport),
704-
});
705-
}
706-
} else if (emptyLinesBetween > 0
707-
&& newlinesBetweenImports !== 'always-and-inside-groups') {
708-
if (distinctGroup && currentImport.rank === previousImport.rank || !distinctGroup && !isStartOfDistinctGroup) {
709-
context.report({
710-
node: previousImport.node,
711-
message: 'There should be no empty line within import group',
712-
fix: removeNewLineAfterImport(context, currentImport, previousImport),
713-
});
693+
const emptyLinesBetween = getNumberOfEmptyLinesBetween(
694+
currentImport,
695+
previousImport,
696+
);
697+
698+
const isStartOfDistinctGroup = getIsStartOfDistinctGroup(
699+
currentImport,
700+
previousImport,
701+
);
702+
703+
const isTypeOnlyImport = currentImport.node.importKind === 'type';
704+
const isPreviousImportTypeOnlyImport = previousImport.node.importKind === 'type';
705+
706+
const isNormalImportFollowingTypeOnlyImportAndRelevant = !isTypeOnlyImport && isPreviousImportTypeOnlyImport && isSortingTypesGroup;
707+
708+
const isTypeOnlyImportAndRelevant = isTypeOnlyImport && isSortingTypesGroup;
709+
710+
const isNotIgnored = isTypeOnlyImportAndRelevant
711+
&& newlinesBetweenTypeOnlyImports !== 'ignore'
712+
|| !isTypeOnlyImportAndRelevant && newlinesBetweenImports !== 'ignore';
713+
714+
if (isNotIgnored) {
715+
const shouldAssertNewlineBetweenGroups = (isTypeOnlyImportAndRelevant || isNormalImportFollowingTypeOnlyImportAndRelevant)
716+
&& (newlinesBetweenTypeOnlyImports === 'always'
717+
|| newlinesBetweenTypeOnlyImports === 'always-and-inside-groups')
718+
|| !isTypeOnlyImportAndRelevant && !isNormalImportFollowingTypeOnlyImportAndRelevant
719+
&& (newlinesBetweenImports === 'always'
720+
|| newlinesBetweenImports === 'always-and-inside-groups');
721+
722+
const shouldAssertNoNewlineWithinGroup = (isTypeOnlyImportAndRelevant || isNormalImportFollowingTypeOnlyImportAndRelevant)
723+
&& newlinesBetweenTypeOnlyImports !== 'always-and-inside-groups'
724+
|| !isTypeOnlyImportAndRelevant && !isNormalImportFollowingTypeOnlyImportAndRelevant
725+
&& newlinesBetweenImports !== 'always-and-inside-groups';
726+
727+
const shouldAssertNoNewlineBetweenGroup = !isSortingTypesGroup
728+
|| !isNormalImportFollowingTypeOnlyImportAndRelevant
729+
|| newlinesBetweenTypeOnlyImports === 'never';
730+
731+
if (shouldAssertNewlineBetweenGroups) {
732+
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
733+
if (distinctGroup || !distinctGroup && isStartOfDistinctGroup) {
734+
context.report({
735+
node: previousImport.node,
736+
message: 'There should be at least one empty line between import groups',
737+
fix: fixNewLineAfterImport(context, previousImport),
738+
});
739+
}
740+
} else if (emptyLinesBetween > 0 && shouldAssertNoNewlineWithinGroup) {
741+
if (
742+
distinctGroup && currentImport.rank === previousImport.rank
743+
|| !distinctGroup && !isStartOfDistinctGroup
744+
) {
745+
context.report({
746+
node: previousImport.node,
747+
message: 'There should be no empty line within import group',
748+
fix: removeNewLineAfterImport(context, currentImport, previousImport),
749+
});
750+
}
714751
}
752+
} else if (emptyLinesBetween > 0 && shouldAssertNoNewlineBetweenGroup) {
753+
context.report({
754+
node: previousImport.node,
755+
message: 'There should be no empty line between import groups',
756+
fix: removeNewLineAfterImport(context, currentImport, previousImport),
757+
});
715758
}
716-
} else if (emptyLinesBetween > 0) {
717-
context.report({
718-
node: previousImport.node,
719-
message: 'There should be no empty line between import groups',
720-
fix: removeNewLineAfterImport(context, currentImport, previousImport),
721-
});
722759
}
723760

724761
previousImport = currentImport;
@@ -793,6 +830,14 @@ module.exports = {
793830
'never',
794831
],
795832
},
833+
'newlines-between-types': {
834+
enum: [
835+
'ignore',
836+
'always',
837+
'always-and-inside-groups',
838+
'never',
839+
],
840+
},
796841
sortTypesGroup: {
797842
type: 'boolean',
798843
default: false,
@@ -852,6 +897,7 @@ module.exports = {
852897
create(context) {
853898
const options = context.options[0] || {};
854899
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
900+
const newlinesBetweenTypeOnlyImports = options['newlines-between-types'] || newlinesBetweenImports;
855901
const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']);
856902
const sortTypesGroup = options.sortTypesGroup;
857903

@@ -1115,8 +1161,8 @@ module.exports = {
11151161
},
11161162
'Program:exit'() {
11171163
importMap.forEach((imported) => {
1118-
if (newlinesBetweenImports !== 'ignore') {
1119-
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup);
1164+
if (newlinesBetweenImports !== 'ignore' || newlinesBetweenTypeOnlyImports !== 'ignore') {
1165+
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, newlinesBetweenTypeOnlyImports, distinctGroup, isSortingTypesGroup);
11201166
}
11211167

11221168
if (alphabetize.order !== 'ignore') {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy