Skip to content

Commit 2cf2a68

Browse files
Xunnamiusljharb
authored andcommitted
[New] order: enable advanced spacing and sorting of type-only imports
1 parent f0727a6 commit 2cf2a68

File tree

2 files changed

+3050
-188
lines changed

2 files changed

+3050
-188
lines changed

src/rules/order.js

Lines changed: 182 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -513,33 +513,59 @@ function computePathRank(ranks, pathGroups, path, maxPosition) {
513513
}
514514
}
515515

516-
function computeRank(context, ranks, importEntry, excludedImportTypes) {
516+
function computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesAmongThemselves) {
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 && !isSortingTypesAmongThemselves) {
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
}
529-
if (typeof rank === 'undefined') {
535+
536+
if (rank === undefined) {
530537
rank = ranks.groups[impType];
538+
539+
if (rank === undefined) {
540+
return -1;
541+
}
531542
}
543+
544+
if (isTypeOnlyImport && isSortingTypesAmongThemselves) {
545+
rank = ranks.groups.type + rank / 10;
546+
}
547+
532548
if (importEntry.type !== 'import' && !importEntry.type.startsWith('import:')) {
533549
rank += 100;
534550
}
535551

536552
return rank;
537553
}
538554

539-
function registerNode(context, importEntry, ranks, imported, excludedImportTypes) {
540-
const rank = computeRank(context, ranks, importEntry, excludedImportTypes);
555+
function registerNode(context, importEntry, ranks, imported, excludedImportTypes, isSortingTypesAmongThemselves) {
556+
const rank = computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesAmongThemselves);
541557
if (rank !== -1) {
542-
imported.push({ ...importEntry, rank });
558+
let importNode = importEntry.node;
559+
560+
if (importEntry.type === 'require' && importNode.parent.parent.type === 'VariableDeclaration') {
561+
importNode = importNode.parent.parent;
562+
}
563+
564+
imported.push({
565+
...importEntry,
566+
rank,
567+
isMultiline: importNode.loc.end.line !== importNode.loc.start.line,
568+
});
543569
}
544570
}
545571

@@ -665,7 +691,7 @@ function removeNewLineAfterImport(context, currentImport, previousImport) {
665691
return undefined;
666692
}
667693

668-
function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup) {
694+
function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, newlinesBetweenTypeOnlyImports, distinctGroup, isSortingTypesAmongThemselves, isConsolidatingSpaceBetweenImports) {
669695
const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => {
670696
const linesBetweenImports = getSourceCode(context).lines.slice(
671697
previousImport.node.loc.end.line,
@@ -678,35 +704,126 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, di
678704
let previousImport = imported[0];
679705

680706
imported.slice(1).forEach(function (currentImport) {
681-
const emptyLinesBetween = getNumberOfEmptyLinesBetween(currentImport, previousImport);
682-
const isStartOfDistinctGroup = getIsStartOfDistinctGroup(currentImport, previousImport);
707+
const emptyLinesBetween = getNumberOfEmptyLinesBetween(
708+
currentImport,
709+
previousImport,
710+
);
711+
712+
const isStartOfDistinctGroup = getIsStartOfDistinctGroup(
713+
currentImport,
714+
previousImport,
715+
);
716+
717+
const isTypeOnlyImport = currentImport.node.importKind === 'type';
718+
const isPreviousImportTypeOnlyImport = previousImport.node.importKind === 'type';
719+
720+
const isNormalImportNextToTypeOnlyImportAndRelevant = isTypeOnlyImport !== isPreviousImportTypeOnlyImport && isSortingTypesAmongThemselves;
721+
722+
const isTypeOnlyImportAndRelevant = isTypeOnlyImport && isSortingTypesAmongThemselves;
723+
724+
// In the special case where newlinesBetweenTypeOnlyImports and
725+
// consolidateIslands want the opposite thing, consolidateIslands wins
726+
const newlinesBetweenTypeOnly = newlinesBetweenTypeOnlyImports === 'never'
727+
&& isConsolidatingSpaceBetweenImports
728+
&& isSortingTypesAmongThemselves
729+
&& (isNormalImportNextToTypeOnlyImportAndRelevant
730+
|| previousImport.isMultiline
731+
|| currentImport.isMultiline)
732+
? 'always-and-inside-groups'
733+
: newlinesBetweenTypeOnlyImports;
734+
735+
const isNotIgnored = isTypeOnlyImportAndRelevant
736+
&& newlinesBetweenTypeOnly !== 'ignore'
737+
|| !isTypeOnlyImportAndRelevant
738+
&& newlinesBetweenImports !== 'ignore';
739+
740+
if (isNotIgnored) {
741+
const shouldAssertNewlineBetweenGroups = (isTypeOnlyImportAndRelevant || isNormalImportNextToTypeOnlyImportAndRelevant)
742+
&& (
743+
newlinesBetweenTypeOnly === 'always'
744+
|| newlinesBetweenTypeOnly === 'always-and-inside-groups'
745+
)
746+
|| !isTypeOnlyImportAndRelevant
747+
&& !isNormalImportNextToTypeOnlyImportAndRelevant
748+
&& (
749+
newlinesBetweenImports === 'always'
750+
|| newlinesBetweenImports === 'always-and-inside-groups'
751+
);
752+
753+
const shouldAssertNoNewlineWithinGroup = (isTypeOnlyImportAndRelevant || isNormalImportNextToTypeOnlyImportAndRelevant)
754+
&& newlinesBetweenTypeOnly !== 'always-and-inside-groups'
755+
|| !isTypeOnlyImportAndRelevant
756+
&& !isNormalImportNextToTypeOnlyImportAndRelevant
757+
&& newlinesBetweenImports !== 'always-and-inside-groups';
758+
759+
const shouldAssertNoNewlineBetweenGroup = !isSortingTypesAmongThemselves
760+
|| !isNormalImportNextToTypeOnlyImportAndRelevant
761+
|| newlinesBetweenTypeOnly === 'never';
762+
763+
const isTheNewlineBetweenImportsInTheSameGroup = distinctGroup
764+
&& currentImport.rank === previousImport.rank
765+
|| !distinctGroup
766+
&& !isStartOfDistinctGroup;
767+
768+
// Let's try to cut down on linting errors sent to the user
769+
let alreadyReported = false;
770+
771+
if (shouldAssertNewlineBetweenGroups) {
772+
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
773+
if (distinctGroup || !distinctGroup && isStartOfDistinctGroup) {
774+
alreadyReported = true;
775+
context.report({
776+
node: previousImport.node,
777+
message: 'There should be at least one empty line between import groups',
778+
fix: fixNewLineAfterImport(context, previousImport),
779+
});
780+
}
781+
} else if (emptyLinesBetween > 0 && shouldAssertNoNewlineWithinGroup) {
782+
if (isTheNewlineBetweenImportsInTheSameGroup) {
783+
alreadyReported = true;
784+
context.report({
785+
node: previousImport.node,
786+
message: 'There should be no empty line within import group',
787+
fix: removeNewLineAfterImport(context, currentImport, previousImport),
788+
});
789+
}
790+
}
791+
} else if (emptyLinesBetween > 0 && shouldAssertNoNewlineBetweenGroup) {
792+
alreadyReported = true;
793+
context.report({
794+
node: previousImport.node,
795+
message: 'There should be no empty line between import groups',
796+
fix: removeNewLineAfterImport(context, currentImport, previousImport),
797+
});
798+
}
683799

684-
if (newlinesBetweenImports === 'always'
685-
|| newlinesBetweenImports === 'always-and-inside-groups') {
686-
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
687-
if (distinctGroup || !distinctGroup && isStartOfDistinctGroup) {
800+
if (!alreadyReported && isConsolidatingSpaceBetweenImports) {
801+
if (emptyLinesBetween === 0 && currentImport.isMultiline) {
688802
context.report({
689803
node: previousImport.node,
690-
message: 'There should be at least one empty line between import groups',
804+
message: 'There should be at least one empty line between this import and the multi-line import that follows it',
691805
fix: fixNewLineAfterImport(context, previousImport),
692806
});
693-
}
694-
} else if (emptyLinesBetween > 0
695-
&& newlinesBetweenImports !== 'always-and-inside-groups') {
696-
if (distinctGroup && currentImport.rank === previousImport.rank || !distinctGroup && !isStartOfDistinctGroup) {
807+
} else if (emptyLinesBetween === 0 && previousImport.isMultiline) {
808+
context.report({
809+
node: previousImport.node,
810+
message: 'There should be at least one empty line between this multi-line import and the import that follows it',
811+
fix: fixNewLineAfterImport(context, previousImport),
812+
});
813+
} else if (
814+
emptyLinesBetween > 0
815+
&& !previousImport.isMultiline
816+
&& !currentImport.isMultiline
817+
&& isTheNewlineBetweenImportsInTheSameGroup
818+
) {
697819
context.report({
698820
node: previousImport.node,
699-
message: 'There should be no empty line within import group',
821+
message:
822+
'There should be no empty lines between this single-line import and the single-line import that follows it',
700823
fix: removeNewLineAfterImport(context, currentImport, previousImport),
701824
});
702825
}
703826
}
704-
} else if (emptyLinesBetween > 0) {
705-
context.report({
706-
node: previousImport.node,
707-
message: 'There should be no empty line between import groups',
708-
fix: removeNewLineAfterImport(context, currentImport, previousImport),
709-
});
710827
}
711828

712829
previousImport = currentImport;
@@ -781,6 +898,24 @@ module.exports = {
781898
'never',
782899
],
783900
},
901+
'newlines-between-types': {
902+
enum: [
903+
'ignore',
904+
'always',
905+
'always-and-inside-groups',
906+
'never',
907+
],
908+
},
909+
consolidateIslands: {
910+
enum: [
911+
'inside-groups',
912+
'never',
913+
],
914+
},
915+
sortTypesAmongThemselves: {
916+
type: 'boolean',
917+
default: false,
918+
},
784919
named: {
785920
default: false,
786921
oneOf: [{
@@ -836,7 +971,10 @@ module.exports = {
836971
create(context) {
837972
const options = context.options[0] || {};
838973
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
974+
const newlinesBetweenTypeOnlyImports = options['newlines-between-types'] || newlinesBetweenImports;
839975
const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']);
976+
const sortTypesAmongThemselves = options.sortTypesAmongThemselves;
977+
const consolidateIslands = options.consolidateIslands || 'never';
840978

841979
const named = {
842980
types: 'mixed',
@@ -879,6 +1017,9 @@ module.exports = {
8791017
const importMap = new Map();
8801018
const exportMap = new Map();
8811019

1020+
const isTypeGroupInGroups = ranks.omittedTypes.indexOf('type') === -1;
1021+
const isSortingTypesAmongThemselves = isTypeGroupInGroups && sortTypesAmongThemselves;
1022+
8821023
function getBlockImports(node) {
8831024
if (!importMap.has(node)) {
8841025
importMap.set(node, []);
@@ -932,6 +1073,7 @@ module.exports = {
9321073
ranks,
9331074
getBlockImports(node.parent),
9341075
pathGroupsExcludedImportTypes,
1076+
isSortingTypesAmongThemselves,
9351077
);
9361078

9371079
if (named.import) {
@@ -983,6 +1125,7 @@ module.exports = {
9831125
ranks,
9841126
getBlockImports(node.parent),
9851127
pathGroupsExcludedImportTypes,
1128+
isSortingTypesAmongThemselves,
9861129
);
9871130
},
9881131
CallExpression(node) {
@@ -1005,6 +1148,7 @@ module.exports = {
10051148
ranks,
10061149
getBlockImports(block),
10071150
pathGroupsExcludedImportTypes,
1151+
isSortingTypesAmongThemselves,
10081152
);
10091153
},
10101154
...named.require && {
@@ -1092,8 +1236,18 @@ module.exports = {
10921236
},
10931237
'Program:exit'() {
10941238
importMap.forEach((imported) => {
1095-
if (newlinesBetweenImports !== 'ignore') {
1096-
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup);
1239+
if (newlinesBetweenImports !== 'ignore' || newlinesBetweenTypeOnlyImports !== 'ignore') {
1240+
makeNewlinesBetweenReport(
1241+
context,
1242+
imported,
1243+
newlinesBetweenImports,
1244+
newlinesBetweenTypeOnlyImports,
1245+
distinctGroup,
1246+
isSortingTypesAmongThemselves,
1247+
consolidateIslands === 'inside-groups'
1248+
&& (newlinesBetweenImports === 'always-and-inside-groups'
1249+
|| newlinesBetweenTypeOnlyImports === 'always-and-inside-groups'),
1250+
);
10971251
}
10981252

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

0 commit comments

Comments
 (0)