Skip to content

Commit 998655b

Browse files
hyperupcallljharb
authored andcommitted
[New] order: Add distinctGroup option
Fixes #2292.
1 parent a8781f7 commit 998655b

File tree

4 files changed

+268
-14
lines changed

4 files changed

+268
-14
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
- [`no-cycle`]: add `allowUnsafeDynamicCyclicDependency` option ([#2387], thanks [@GerkinDev])
1212
- [`no-restricted-paths`]: support arrays for `from` and `target` options ([#2466], thanks [@AdriAt360])
1313
- [`no-anonymous-default-export`]: add `allowNew` option ([#2505], thanks [@DamienCassou])
14+
- [`order`]: Add `distinctGroup` option ([#2395], thanks [@hyperupcall])
1415

1516
### Fixed
1617
- [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311])
@@ -1014,6 +1015,7 @@ for info on changes for earlier releases.
10141015
[#2411]: https://github.com/import-js/eslint-plugin-import/pull/2411
10151016
[#2399]: https://github.com/import-js/eslint-plugin-import/pull/2399
10161017
[#2396]: https://github.com/import-js/eslint-plugin-import/pull/2396
1018+
[#2395]: https://github.com/import-js/eslint-plugin-import/pull/2395
10171019
[#2393]: https://github.com/import-js/eslint-plugin-import/pull/2393
10181020
[#2388]: https://github.com/import-js/eslint-plugin-import/pull/2388
10191021
[#2387]: https://github.com/import-js/eslint-plugin-import/pull/2387

docs/rules/order.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,31 @@ Properties of the objects
128128
}
129129
```
130130

131+
### `distinctGroup: [boolean]`:
132+
133+
This changes how `pathGroups[].position` affects grouping. The property is most useful when `newlines-between` is set to `always` and at least 1 `pathGroups` entry has a `position` property set.
134+
135+
By default, in the context of a particular `pathGroup` entry, when setting `position`, a new "group" will silently be created. That is, even if the `group` is specified, a newline will still separate imports that match that `pattern` with the rest of the group (assuming `newlines-between` is `always`). This is undesirable if your intentions are to use `position` to position _within_ the group (and not create a new one). Override this behavior by setting `distinctGroup` to `false`; this will keep imports within the same group as intended.
136+
137+
Note that currently, `distinctGroup` defaults to `true`. However, in a later update, the default will change to `false`
138+
139+
Example:
140+
```json
141+
{
142+
"import/order": ["error", {
143+
"newlines-between": "always",
144+
"pathGroups": [
145+
{
146+
"pattern": "@app/**",
147+
"group": "external",
148+
"position": "after"
149+
}
150+
],
151+
"distinctGroup": false
152+
}]
153+
}
154+
```
155+
131156
### `pathGroupsExcludedImportTypes: [array]`:
132157

133158
This defines import types that are not handled by configured pathGroups.

src/rules/order.js

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ function removeNewLineAfterImport(context, currentImport, previousImport) {
493493
return undefined;
494494
}
495495

496-
function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports) {
496+
function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup) {
497497
const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => {
498498
const linesBetweenImports = context.getSourceCode().lines.slice(
499499
previousImport.node.loc.end.line,
@@ -502,27 +502,34 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports) {
502502

503503
return linesBetweenImports.filter((line) => !line.trim().length).length;
504504
};
505+
const getIsStartOfDistinctGroup = (currentImport, previousImport) => {
506+
return currentImport.rank - 1 >= previousImport.rank;
507+
};
505508
let previousImport = imported[0];
506509

507510
imported.slice(1).forEach(function (currentImport) {
508511
const emptyLinesBetween = getNumberOfEmptyLinesBetween(currentImport, previousImport);
512+
const isStartOfDistinctGroup = getIsStartOfDistinctGroup(currentImport, previousImport);
509513

510514
if (newlinesBetweenImports === 'always'
511515
|| newlinesBetweenImports === 'always-and-inside-groups') {
512516
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
513-
context.report({
514-
node: previousImport.node,
515-
message: 'There should be at least one empty line between import groups',
516-
fix: fixNewLineAfterImport(context, previousImport),
517-
});
518-
} else if (currentImport.rank === previousImport.rank
519-
&& emptyLinesBetween > 0
517+
if (distinctGroup || (!distinctGroup && isStartOfDistinctGroup)) {
518+
context.report({
519+
node: previousImport.node,
520+
message: 'There should be at least one empty line between import groups',
521+
fix: fixNewLineAfterImport(context, previousImport),
522+
});
523+
}
524+
} else if (emptyLinesBetween > 0
520525
&& newlinesBetweenImports !== 'always-and-inside-groups') {
521-
context.report({
522-
node: previousImport.node,
523-
message: 'There should be no empty line within import group',
524-
fix: removeNewLineAfterImport(context, currentImport, previousImport),
525-
});
526+
if ((distinctGroup && currentImport.rank === previousImport.rank) || (!distinctGroup && !isStartOfDistinctGroup)) {
527+
context.report({
528+
node: previousImport.node,
529+
message: 'There should be no empty line within import group',
530+
fix: removeNewLineAfterImport(context, currentImport, previousImport),
531+
});
532+
}
526533
}
527534
} else if (emptyLinesBetween > 0) {
528535
context.report({
@@ -544,6 +551,9 @@ function getAlphabetizeConfig(options) {
544551
return { order, caseInsensitive };
545552
}
546553

554+
// TODO, semver-major: Change the default of "distinctGroup" from true to false
555+
const defaultDistinctGroup = true;
556+
547557
module.exports = {
548558
meta: {
549559
type: 'suggestion',
@@ -562,6 +572,10 @@ module.exports = {
562572
pathGroupsExcludedImportTypes: {
563573
type: 'array',
564574
},
575+
distinctGroup: {
576+
type: 'boolean',
577+
default: defaultDistinctGroup,
578+
},
565579
pathGroups: {
566580
type: 'array',
567581
items: {
@@ -582,6 +596,7 @@ module.exports = {
582596
enum: ['after', 'before'],
583597
},
584598
},
599+
additionalProperties: false,
585600
required: ['pattern', 'group'],
586601
},
587602
},
@@ -622,6 +637,7 @@ module.exports = {
622637
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
623638
const pathGroupsExcludedImportTypes = new Set(options['pathGroupsExcludedImportTypes'] || ['builtin', 'external', 'object']);
624639
const alphabetize = getAlphabetizeConfig(options);
640+
const distinctGroup = options.distinctGroup == null ? defaultDistinctGroup : !!options.distinctGroup;
625641
let ranks;
626642

627643
try {
@@ -724,7 +740,7 @@ module.exports = {
724740
'Program:exit': function reportAndReset() {
725741
importMap.forEach((imported) => {
726742
if (newlinesBetweenImports !== 'ignore') {
727-
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports);
743+
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup);
728744
}
729745

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

tests/src/rules/order.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,161 @@ ruleTester.run('order', rule, {
925925
},
926926
},
927927
}),
928+
// Option pathGroup[].distinctGroup: 'true' does not prevent 'position' properties from affecting the visible grouping
929+
test({
930+
code: `
931+
import A from 'a';
932+
933+
import C from 'c';
934+
935+
import B from 'b';
936+
`,
937+
options: [
938+
{
939+
'newlines-between': 'always',
940+
'distinctGroup': true,
941+
'pathGroupsExcludedImportTypes': [],
942+
'pathGroups': [
943+
{
944+
'pattern': 'a',
945+
'group': 'external',
946+
'position': 'before',
947+
},
948+
{
949+
'pattern': 'b',
950+
'group': 'external',
951+
'position': 'after',
952+
},
953+
],
954+
},
955+
],
956+
}),
957+
// Option pathGroup[].distinctGroup: 'false' should prevent 'position' properties from affecting the visible grouping
958+
test({
959+
code: `
960+
import A from 'a';
961+
import C from 'c';
962+
import B from 'b';
963+
`,
964+
options: [
965+
{
966+
'newlines-between': 'always',
967+
'distinctGroup': false,
968+
'pathGroupsExcludedImportTypes': [],
969+
'pathGroups': [
970+
{
971+
'pattern': 'a',
972+
'group': 'external',
973+
'position': 'before',
974+
},
975+
{
976+
'pattern': 'b',
977+
'group': 'external',
978+
'position': 'after',
979+
},
980+
],
981+
},
982+
],
983+
}),
984+
// Option pathGroup[].distinctGroup: 'false' should prevent 'position' properties from affecting the visible grouping 2
985+
test({
986+
code: `
987+
import A from 'a';
988+
989+
import b from './b';
990+
import B from './B';
991+
`,
992+
options: [
993+
{
994+
'newlines-between': 'always',
995+
'distinctGroup': false,
996+
'pathGroupsExcludedImportTypes': [],
997+
'pathGroups': [
998+
{
999+
'pattern': 'a',
1000+
'group': 'external',
1001+
},
1002+
{
1003+
'pattern': 'b',
1004+
'group': 'internal',
1005+
'position': 'before',
1006+
},
1007+
],
1008+
},
1009+
],
1010+
}),
1011+
// Option pathGroup[].distinctGroup: 'false' should prevent 'position' properties from affecting the visible grouping 3
1012+
test({
1013+
code: `
1014+
import A from "baz";
1015+
import B from "Bar";
1016+
import C from "Foo";
1017+
1018+
import D from "..";
1019+
import E from "../";
1020+
import F from "../baz";
1021+
import G from "../Bar";
1022+
import H from "../Foo";
1023+
1024+
import I from ".";
1025+
import J from "./baz";
1026+
import K from "./Bar";
1027+
import L from "./Foo";
1028+
`,
1029+
options: [
1030+
{
1031+
'alphabetize': {
1032+
'caseInsensitive': false,
1033+
'order': 'asc',
1034+
},
1035+
'newlines-between': 'always',
1036+
'groups': [
1037+
['builtin', 'external', 'internal', 'unknown', 'object', 'type'],
1038+
'parent',
1039+
['sibling', 'index'],
1040+
],
1041+
'distinctGroup': false,
1042+
'pathGroupsExcludedImportTypes': [],
1043+
'pathGroups': [
1044+
{
1045+
'pattern': './',
1046+
'group': 'sibling',
1047+
'position': 'before',
1048+
},
1049+
{
1050+
'pattern': '.',
1051+
'group': 'sibling',
1052+
'position': 'before',
1053+
},
1054+
{
1055+
'pattern': '..',
1056+
'group': 'parent',
1057+
'position': 'before',
1058+
},
1059+
{
1060+
'pattern': '../',
1061+
'group': 'parent',
1062+
'position': 'before',
1063+
},
1064+
{
1065+
'pattern': '[a-z]*',
1066+
'group': 'external',
1067+
'position': 'before',
1068+
},
1069+
{
1070+
'pattern': '../[a-z]*',
1071+
'group': 'parent',
1072+
'position': 'before',
1073+
},
1074+
{
1075+
'pattern': './[a-z]*',
1076+
'group': 'sibling',
1077+
'position': 'before',
1078+
},
1079+
],
1080+
},
1081+
],
1082+
}),
9281083
],
9291084
invalid: [
9301085
// builtin before external module (require)
@@ -2439,6 +2594,62 @@ ruleTester.run('order', rule, {
24392594
message: '`..` import should occur before import of `../a`',
24402595
}],
24412596
}),
2597+
// Option pathGroup[].distinctGroup: 'false' should error when newlines are incorrect 2
2598+
test({
2599+
code: `
2600+
import A from 'a';
2601+
import C from './c';
2602+
`,
2603+
output: `
2604+
import A from 'a';
2605+
2606+
import C from './c';
2607+
`,
2608+
options: [
2609+
{
2610+
'newlines-between': 'always',
2611+
'distinctGroup': false,
2612+
'pathGroupsExcludedImportTypes': [],
2613+
},
2614+
],
2615+
errors: [{
2616+
message: 'There should be at least one empty line between import groups',
2617+
}],
2618+
}),
2619+
// Option pathGroup[].distinctGroup: 'false' should error when newlines are incorrect 2
2620+
test({
2621+
code: `
2622+
import A from 'a';
2623+
2624+
import C from 'c';
2625+
`,
2626+
output: `
2627+
import A from 'a';
2628+
import C from 'c';
2629+
`,
2630+
options: [
2631+
{
2632+
'newlines-between': 'always',
2633+
'distinctGroup': false,
2634+
'pathGroupsExcludedImportTypes': [],
2635+
'pathGroups': [
2636+
{
2637+
'pattern': 'a',
2638+
'group': 'external',
2639+
'position': 'before',
2640+
},
2641+
{
2642+
'pattern': 'c',
2643+
'group': 'external',
2644+
'position': 'after',
2645+
},
2646+
],
2647+
},
2648+
],
2649+
errors: [{
2650+
message: 'There should be no empty line within import group',
2651+
}],
2652+
}),
24422653
// Alphabetize with require
24432654
...semver.satisfies(eslintPkg.version, '< 3.0.0') ? [] : [
24442655
test({

0 commit comments

Comments
 (0)