Skip to content

Commit c5a0944

Browse files
committed
feat(import/order): enable advanced spacing and sorting of type-only imports
1 parent a20d843 commit c5a0944

File tree

2 files changed

+3049
-189
lines changed

2 files changed

+3049
-189
lines changed

src/rules/order.js

Lines changed: 181 additions & 29 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,124 @@ 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+
);
683716

684-
if (newlinesBetweenImports === 'always'
685-
|| newlinesBetweenImports === 'always-and-inside-groups') {
686-
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
687-
if (distinctGroup || !distinctGroup && isStartOfDistinctGroup) {
717+
const isTypeOnlyImport = currentImport.node.importKind === 'type';
718+
const isPreviousImportTypeOnlyImport = previousImport.node.importKind === 'type';
719+
720+
const isNormalImportNextToTypeOnlyImportAndRelevant =
721+
isTypeOnlyImport !== isPreviousImportTypeOnlyImport && isSortingTypesAmongThemselves;
722+
723+
const isTypeOnlyImportAndRelevant =
724+
isTypeOnlyImport && isSortingTypesAmongThemselves;
725+
726+
// In the special case where newlinesBetweenTypeOnlyImports and
727+
// consolidateIslands want the opposite thing, consolidateIslands wins
728+
const newlinesBetweenTypeOnlyImports =
729+
newlinesBetweenTypeOnlyImports_ === 'never' &&
730+
isConsolidatingSpaceBetweenImports &&
731+
isSortingTypesAmongThemselves &&
732+
(isNormalImportNextToTypeOnlyImportAndRelevant ||
733+
previousImport.isMultiline ||
734+
currentImport.isMultiline)
735+
? 'always-and-inside-groups'
736+
: newlinesBetweenTypeOnlyImports_;
737+
738+
const isNotIgnored =
739+
(isTypeOnlyImportAndRelevant &&
740+
newlinesBetweenTypeOnlyImports !== 'ignore') ||
741+
(!isTypeOnlyImportAndRelevant && newlinesBetweenImports !== 'ignore');
742+
743+
if(isNotIgnored) {
744+
const shouldAssertNewlineBetweenGroups =
745+
((isTypeOnlyImportAndRelevant || isNormalImportNextToTypeOnlyImportAndRelevant) &&
746+
(newlinesBetweenTypeOnlyImports === 'always' ||
747+
newlinesBetweenTypeOnlyImports === 'always-and-inside-groups')) ||
748+
((!isTypeOnlyImportAndRelevant && !isNormalImportNextToTypeOnlyImportAndRelevant) &&
749+
(newlinesBetweenImports === 'always' ||
750+
newlinesBetweenImports === 'always-and-inside-groups'));
751+
752+
const shouldAssertNoNewlineWithinGroup =
753+
((isTypeOnlyImportAndRelevant || isNormalImportNextToTypeOnlyImportAndRelevant) &&
754+
(newlinesBetweenTypeOnlyImports !== 'always-and-inside-groups')) ||
755+
((!isTypeOnlyImportAndRelevant && !isNormalImportNextToTypeOnlyImportAndRelevant) &&
756+
(newlinesBetweenImports !== 'always-and-inside-groups'));
757+
758+
const shouldAssertNoNewlineBetweenGroup =
759+
!isSortingTypesAmongThemselves ||
760+
!isNormalImportNextToTypeOnlyImportAndRelevant ||
761+
newlinesBetweenTypeOnlyImports === 'never';
762+
763+
const isTheNewlineBetweenImportsInTheSameGroup = (distinctGroup && currentImport.rank === previousImport.rank) ||
764+
(!distinctGroup && !isStartOfDistinctGroup);
765+
766+
// Let's try to cut down on linting errors sent to the user
767+
let alreadyReported = false;
768+
769+
if (shouldAssertNewlineBetweenGroups) {
770+
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
771+
if (distinctGroup || !distinctGroup && isStartOfDistinctGroup) {
772+
alreadyReported = true;
773+
context.report({
774+
node: previousImport.node,
775+
message: 'There should be at least one empty line between import groups',
776+
fix: fixNewLineAfterImport(context, previousImport),
777+
});
778+
}
779+
} else if (emptyLinesBetween > 0 && shouldAssertNoNewlineWithinGroup) {
780+
if (isTheNewlineBetweenImportsInTheSameGroup) {
781+
alreadyReported = true;
782+
context.report({
783+
node: previousImport.node,
784+
message: 'There should be no empty line within import group',
785+
fix: removeNewLineAfterImport(context, currentImport, previousImport)
786+
});
787+
}
788+
}
789+
} else if (emptyLinesBetween > 0 && shouldAssertNoNewlineBetweenGroup) {
790+
alreadyReported = true;
791+
context.report({
792+
node: previousImport.node,
793+
message: 'There should be no empty line between import groups',
794+
fix: removeNewLineAfterImport(context, currentImport, previousImport),
795+
});
796+
}
797+
798+
if(!alreadyReported && isConsolidatingSpaceBetweenImports) {
799+
if(emptyLinesBetween === 0 && currentImport.isMultiline) {
688800
context.report({
689801
node: previousImport.node,
690-
message: 'There should be at least one empty line between import groups',
802+
message: 'There should be at least one empty line between this import and the multi-line import that follows it',
691803
fix: fixNewLineAfterImport(context, previousImport),
692804
});
693-
}
694-
} else if (emptyLinesBetween > 0
695-
&& newlinesBetweenImports !== 'always-and-inside-groups') {
696-
if (distinctGroup && currentImport.rank === previousImport.rank || !distinctGroup && !isStartOfDistinctGroup) {
805+
} else if(emptyLinesBetween === 0 && previousImport.isMultiline) {
697806
context.report({
698807
node: previousImport.node,
699-
message: 'There should be no empty line within import group',
700-
fix: removeNewLineAfterImport(context, currentImport, previousImport),
808+
message: 'There should be at least one empty line between this multi-line import and the import that follows it',
809+
fix: fixNewLineAfterImport(context, previousImport),
810+
});
811+
} else if (
812+
emptyLinesBetween > 0 &&
813+
!previousImport.isMultiline &&
814+
!currentImport.isMultiline &&
815+
isTheNewlineBetweenImportsInTheSameGroup
816+
) {
817+
context.report({
818+
node: previousImport.node,
819+
message:
820+
'There should be no empty lines between this single-line import and the single-line import that follows it',
821+
fix: removeNewLineAfterImport(context, currentImport, previousImport)
701822
});
702823
}
703824
}
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-
});
710825
}
711826

712827
previousImport = currentImport;
@@ -781,6 +896,24 @@ module.exports = {
781896
'never',
782897
],
783898
},
899+
'newlines-between-types': {
900+
enum: [
901+
'ignore',
902+
'always',
903+
'always-and-inside-groups',
904+
'never',
905+
],
906+
},
907+
consolidateIslands: {
908+
enum: [
909+
'inside-groups',
910+
'never',
911+
],
912+
},
913+
sortTypesAmongThemselves: {
914+
type: 'boolean',
915+
default: false,
916+
},
784917
named: {
785918
default: false,
786919
oneOf: [{
@@ -836,7 +969,10 @@ module.exports = {
836969
create(context) {
837970
const options = context.options[0] || {};
838971
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
972+
const newlinesBetweenTypeOnlyImports = options['newlines-between-types'] || newlinesBetweenImports;
839973
const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']);
974+
const sortTypesAmongThemselves = options.sortTypesAmongThemselves;
975+
const consolidateIslands = options.consolidateIslands || 'never';
840976

841977
const named = {
842978
types: 'mixed',
@@ -879,6 +1015,9 @@ module.exports = {
8791015
const importMap = new Map();
8801016
const exportMap = new Map();
8811017

1018+
const isTypeGroupInGroups = ranks.omittedTypes.indexOf('type') === -1;
1019+
const isSortingTypesAmongThemselves = isTypeGroupInGroups && sortTypesAmongThemselves;
1020+
8821021
function getBlockImports(node) {
8831022
if (!importMap.has(node)) {
8841023
importMap.set(node, []);
@@ -932,6 +1071,7 @@ module.exports = {
9321071
ranks,
9331072
getBlockImports(node.parent),
9341073
pathGroupsExcludedImportTypes,
1074+
isSortingTypesAmongThemselves
9351075
);
9361076

9371077
if (named.import) {
@@ -983,6 +1123,7 @@ module.exports = {
9831123
ranks,
9841124
getBlockImports(node.parent),
9851125
pathGroupsExcludedImportTypes,
1126+
isSortingTypesAmongThemselves
9861127
);
9871128
},
9881129
CallExpression(node) {
@@ -1005,6 +1146,7 @@ module.exports = {
10051146
ranks,
10061147
getBlockImports(block),
10071148
pathGroupsExcludedImportTypes,
1149+
isSortingTypesAmongThemselves
10081150
);
10091151
},
10101152
...named.require && {
@@ -1092,8 +1234,18 @@ module.exports = {
10921234
},
10931235
'Program:exit'() {
10941236
importMap.forEach((imported) => {
1095-
if (newlinesBetweenImports !== 'ignore') {
1096-
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup);
1237+
if (newlinesBetweenImports !== 'ignore' || newlinesBetweenTypeOnlyImports !== 'ignore') {
1238+
makeNewlinesBetweenReport(
1239+
context,
1240+
imported,
1241+
newlinesBetweenImports,
1242+
newlinesBetweenTypeOnlyImports,
1243+
distinctGroup,
1244+
isSortingTypesAmongThemselves,
1245+
consolidateIslands === 'inside-groups' &&
1246+
(newlinesBetweenImports === 'always-and-inside-groups' ||
1247+
newlinesBetweenTypeOnlyImports === 'always-and-inside-groups')
1248+
);
10971249
}
10981250

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

0 commit comments

Comments
 (0)