eslint-plugin-import-x
Version:
Import with sanity.
1,186 lines • 49.2 kB
JavaScript
import debug from 'debug';
import { minimatch } from 'minimatch';
import { getValue, importType, isStaticRequire, createRule, } from '../utils/index.js';
const log = debug('eslint-plugin-import-x:rules:order');
const groupBy = (array, grouper) => array.reduce((acc, curr, index) => {
const key = grouper(curr, index);
(acc[key] ||= []).push(curr);
return acc;
}, {});
const categories = {
named: 'named',
import: 'import',
exports: 'exports',
};
const defaultGroups = [
'builtin',
'external',
'parent',
'sibling',
'index',
];
function reverse(array) {
return array.map(v => ({ ...v, rank: -v.rank })).reverse();
}
function getTokensOrCommentsAfter(sourceCode, node, count) {
let currentNodeOrToken = node;
const result = [];
for (let i = 0; i < count; i++) {
currentNodeOrToken = sourceCode.getTokenAfter(currentNodeOrToken, {
includeComments: true,
});
if (currentNodeOrToken == null) {
break;
}
result.push(currentNodeOrToken);
}
return result;
}
function getTokensOrCommentsBefore(sourceCode, node, count) {
let currentNodeOrToken = node;
const result = [];
for (let i = 0; i < count; i++) {
currentNodeOrToken = sourceCode.getTokenBefore(currentNodeOrToken, {
includeComments: true,
});
if (currentNodeOrToken == null) {
break;
}
result.push(currentNodeOrToken);
}
return result.reverse();
}
function takeTokensAfterWhile(sourceCode, node, condition) {
const tokens = getTokensOrCommentsAfter(sourceCode, node, 100);
const result = [];
for (const token of tokens) {
if (condition(token)) {
result.push(token);
}
else {
break;
}
}
return result;
}
function takeTokensBeforeWhile(sourceCode, node, condition) {
const tokens = getTokensOrCommentsBefore(sourceCode, node, 100);
const result = [];
for (let i = tokens.length - 1; i >= 0; i--) {
if (condition(tokens[i])) {
result.push(tokens[i]);
}
else {
break;
}
}
return result.reverse();
}
function findOutOfOrder(imported) {
if (imported.length === 0) {
return [];
}
let maxSeenRankNode = imported[0];
return imported.filter(function (importedModule) {
const res = importedModule.rank < maxSeenRankNode.rank;
if (maxSeenRankNode.rank < importedModule.rank) {
maxSeenRankNode = importedModule;
}
return res;
});
}
function findRootNode(node) {
let parent = node;
while (parent.parent != null &&
(!('body' in parent.parent) || parent.parent.body == null)) {
parent = parent.parent;
}
return parent;
}
function findEndOfLineWithComments(sourceCode, node) {
const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node));
const endOfTokens = tokensToEndOfLine.length > 0
? tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1]
: node.range[1];
let result = endOfTokens;
for (let i = endOfTokens; i < sourceCode.text.length; i++) {
if (sourceCode.text[i] === '\n') {
result = i + 1;
break;
}
if (sourceCode.text[i] !== ' ' &&
sourceCode.text[i] !== '\t' &&
sourceCode.text[i] !== '\r') {
break;
}
result = i + 1;
}
return result;
}
function commentOnSameLineAs(node) {
return (token) => (token.type === 'Block' || token.type === 'Line') &&
token.loc.start.line === token.loc.end.line &&
token.loc.end.line === node.loc.end.line;
}
function findStartOfLineWithComments(sourceCode, node) {
const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node));
const startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range[0] : node.range[0];
let result = startOfTokens;
for (let i = startOfTokens - 1; i > 0; i--) {
if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t') {
break;
}
result = i;
}
return result;
}
function findSpecifierStart(sourceCode, node) {
let token;
do {
token = sourceCode.getTokenBefore(node);
} while (token.value !== ',' && token.value !== '{');
return token.range[1];
}
function findSpecifierEnd(sourceCode, node) {
let token;
do {
token = sourceCode.getTokenAfter(node);
} while (token.value !== ',' && token.value !== '}');
return token.range[0];
}
function isRequireExpression(expr) {
return (expr != null &&
expr.type === 'CallExpression' &&
expr.callee != null &&
'name' in expr.callee &&
expr.callee.name === 'require' &&
expr.arguments != null &&
expr.arguments.length === 1 &&
expr.arguments[0].type === 'Literal');
}
function isSupportedRequireModule(node) {
if (node.type !== 'VariableDeclaration') {
return false;
}
if (node.declarations.length !== 1) {
return false;
}
const decl = node.declarations[0];
const isPlainRequire = decl.id &&
(decl.id.type === 'Identifier' || decl.id.type === 'ObjectPattern') &&
isRequireExpression(decl.init);
const isRequireWithMemberExpression = decl.id &&
(decl.id.type === 'Identifier' || decl.id.type === 'ObjectPattern') &&
decl.init != null &&
decl.init.type === 'CallExpression' &&
decl.init.callee != null &&
decl.init.callee.type === 'MemberExpression' &&
isRequireExpression(decl.init.callee.object);
return isPlainRequire || isRequireWithMemberExpression;
}
function isPlainImportModule(node) {
return (node.type === 'ImportDeclaration' &&
node.specifiers != null &&
node.specifiers.length > 0);
}
function isPlainImportEquals(node) {
return (node.type === 'TSImportEqualsDeclaration' &&
'expression' in node.moduleReference &&
!!node.moduleReference.expression);
}
function isCJSExports(context, node) {
if (node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.property.type === 'Identifier' &&
node.object.name === 'module' &&
node.property.name === 'exports') {
return !context.sourceCode
.getScope(node)
.variables.some(variable => variable.name === 'module');
}
if (node.type === 'Identifier' && node.name === 'exports') {
return !context.sourceCode
.getScope(node)
.variables.some(variable => variable.name === 'exports');
}
}
function getNamedCJSExports(context, node) {
if (node.type !== 'MemberExpression') {
return;
}
const result = [];
let root = node;
let parent;
while (root.type === 'MemberExpression') {
if (root.property.type !== 'Identifier') {
return;
}
result.unshift(root.property.name);
parent = root;
root = root.object;
}
if (isCJSExports(context, root)) {
return result;
}
if (isCJSExports(context, parent)) {
return result.slice(1);
}
}
function canCrossNodeWhileReorder(node) {
return (isSupportedRequireModule(node) ||
isPlainImportModule(node) ||
isPlainImportEquals(node));
}
function canReorderItems(firstNode, secondNode) {
const parent = firstNode.parent;
if (!parent || !('body' in parent) || !Array.isArray(parent.body)) {
return false;
}
const body = parent.body;
const [firstIndex, secondIndex] = [
body.indexOf(firstNode),
body.indexOf(secondNode),
].sort();
const nodesBetween = parent.body.slice(firstIndex, secondIndex + 1);
for (const nodeBetween of nodesBetween) {
if (!canCrossNodeWhileReorder(nodeBetween)) {
return false;
}
}
return true;
}
function makeImportDescription(node) {
if (node.type === 'export') {
if (node.node.exportKind === 'type') {
return 'type export';
}
return 'export';
}
if (node.node.importKind === 'type') {
return 'type import';
}
if (node.node.importKind === 'typeof') {
return 'typeof import';
}
return 'import';
}
function fixOutOfOrder(context, firstNode, secondNode, order, category) {
const isNamed = category === categories.named;
const isExports = category === categories.exports;
const { sourceCode } = context;
const { firstRoot, secondRoot } = isNamed
? { firstRoot: firstNode.node, secondRoot: secondNode.node }
: {
firstRoot: findRootNode(firstNode.node),
secondRoot: findRootNode(secondNode.node),
};
const { firstRootStart, firstRootEnd, secondRootStart, secondRootEnd } = isNamed
? {
firstRootStart: findSpecifierStart(sourceCode, firstRoot),
firstRootEnd: findSpecifierEnd(sourceCode, firstRoot),
secondRootStart: findSpecifierStart(sourceCode, secondRoot),
secondRootEnd: findSpecifierEnd(sourceCode, secondRoot),
}
: {
firstRootStart: findStartOfLineWithComments(sourceCode, firstRoot),
firstRootEnd: findEndOfLineWithComments(sourceCode, firstRoot),
secondRootStart: findStartOfLineWithComments(sourceCode, secondRoot),
secondRootEnd: findEndOfLineWithComments(sourceCode, secondRoot),
};
if (firstNode.displayName === secondNode.displayName) {
if (firstNode.alias) {
firstNode.displayName = `${firstNode.displayName} as ${firstNode.alias}`;
}
if (secondNode.alias) {
secondNode.displayName = `${secondNode.displayName} as ${secondNode.alias}`;
}
}
const firstDesc = makeImportDescription(firstNode);
const secondDesc = makeImportDescription(secondNode);
if (firstNode.displayName === secondNode.displayName &&
firstDesc === secondDesc) {
log(firstNode.displayName, firstNode.node.loc, secondNode.displayName, secondNode.node.loc);
return;
}
const firstImport = `${firstDesc} of \`${firstNode.displayName}\``;
const secondImport = `\`${secondNode.displayName}\` ${secondDesc}`;
const messageOptions = {
messageId: 'order',
data: { firstImport, secondImport, order },
};
if (isNamed) {
const firstCode = sourceCode.text.slice(firstRootStart, firstRoot.range[1]);
const firstTrivia = sourceCode.text.slice(firstRoot.range[1], firstRootEnd);
const secondCode = sourceCode.text.slice(secondRootStart, secondRoot.range[1]);
const secondTrivia = sourceCode.text.slice(secondRoot.range[1], secondRootEnd);
if (order === 'before') {
const trimmedTrivia = secondTrivia.trimEnd();
const gapCode = sourceCode.text.slice(firstRootEnd, secondRootStart - 1);
const whitespaces = secondTrivia.slice(trimmedTrivia.length);
context.report({
node: secondNode.node,
...messageOptions,
fix: fixer => fixer.replaceTextRange([firstRootStart, secondRootEnd], `${secondCode},${trimmedTrivia}${firstCode}${firstTrivia}${gapCode}${whitespaces}`),
});
}
else if (order === 'after') {
const trimmedTrivia = firstTrivia.trimEnd();
const gapCode = sourceCode.text.slice(secondRootEnd + 1, firstRootStart);
const whitespaces = firstTrivia.slice(trimmedTrivia.length);
context.report({
node: secondNode.node,
...messageOptions,
fix: fixes => fixes.replaceTextRange([secondRootStart, firstRootEnd], `${gapCode}${firstCode},${trimmedTrivia}${secondCode}${whitespaces}`),
});
}
}
else {
const canFix = isExports || canReorderItems(firstRoot, secondRoot);
let newCode = sourceCode.text.slice(secondRootStart, secondRootEnd);
if (newCode[newCode.length - 1] !== '\n') {
newCode = `${newCode}\n`;
}
if (order === 'before') {
context.report({
node: secondNode.node,
...messageOptions,
fix: canFix
? fixer => fixer.replaceTextRange([firstRootStart, secondRootEnd], newCode +
sourceCode.text.slice(firstRootStart, secondRootStart))
: null,
});
}
else if (order === 'after') {
context.report({
node: secondNode.node,
...messageOptions,
fix: canFix
? fixer => fixer.replaceTextRange([secondRootStart, firstRootEnd], sourceCode.text.slice(secondRootEnd, firstRootEnd) + newCode)
: null,
});
}
}
}
function reportOutOfOrder(context, imported, outOfOrder, order, category) {
for (const imp of outOfOrder) {
fixOutOfOrder(context, imported.find(importedItem => importedItem.rank > imp.rank), imp, order, category);
}
}
function makeOutOfOrderReport(context, imported, category) {
const outOfOrder = findOutOfOrder(imported);
if (outOfOrder.length === 0) {
return;
}
const reversedImported = reverse(imported);
const reversedOrder = findOutOfOrder(reversedImported);
if (reversedOrder.length < outOfOrder.length) {
reportOutOfOrder(context, reversedImported, reversedOrder, 'after', category);
return;
}
reportOutOfOrder(context, imported, outOfOrder, 'before', category);
}
const compareString = (a, b) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
};
const DEFAULT_IMPORT_KIND = 'value';
const getNormalizedValue = (node, toLowerCase) => {
const value = String(node.value);
return toLowerCase ? value.toLowerCase() : value;
};
const RELATIVE_DOTS = new Set(['.', '..']);
function getSorter(alphabetizeOptions) {
const multiplier = alphabetizeOptions.order === 'asc' ? 1 : -1;
const orderImportKind = alphabetizeOptions.orderImportKind;
const multiplierImportKind = orderImportKind !== 'ignore' &&
(alphabetizeOptions.orderImportKind === 'asc' ? 1 : -1);
return function importsSorter(nodeA, nodeB) {
const importA = getNormalizedValue(nodeA, alphabetizeOptions.caseInsensitive);
const importB = getNormalizedValue(nodeB, alphabetizeOptions.caseInsensitive);
let result = 0;
if (!importA.includes('/') && !importB.includes('/')) {
result = compareString(importA, importB);
}
else {
const A = importA.split('/');
const B = importB.split('/');
const a = A.length;
const b = B.length;
for (let i = 0; i < Math.min(a, b); i++) {
const x = A[i];
const y = B[i];
if (i === 0 && RELATIVE_DOTS.has(x) && RELATIVE_DOTS.has(y)) {
if (x !== y) {
break;
}
continue;
}
result = compareString(x, y);
if (result) {
break;
}
}
if (!result && a !== b) {
result = a < b ? -1 : 1;
}
}
result = result * multiplier;
if (!result && multiplierImportKind) {
result =
multiplierImportKind *
compareString(nodeA.node.importKind || DEFAULT_IMPORT_KIND, nodeB.node.importKind || DEFAULT_IMPORT_KIND);
}
return result;
};
}
function mutateRanksToAlphabetize(imported, alphabetizeOptions) {
const groupedByRanks = groupBy(imported, item => item.rank);
const sorterFn = getSorter(alphabetizeOptions);
const groupRanks = Object.keys(groupedByRanks).sort((a, b) => +a - +b);
for (const groupRank of groupRanks) {
groupedByRanks[groupRank].sort(sorterFn);
}
let newRank = 0;
const alphabetizedRanks = groupRanks.reduce((acc, groupRank) => {
for (const importedItem of groupedByRanks[groupRank]) {
acc[`${importedItem.value}|${importedItem.node.importKind}`] =
Number.parseInt(groupRank, 10) + newRank;
newRank += 1;
}
return acc;
}, {});
for (const importedItem of imported) {
importedItem.rank =
alphabetizedRanks[`${importedItem.value}|${importedItem.node.importKind}`];
}
}
function computePathRank(ranks, pathGroups, path, maxPosition) {
for (const { pattern, patternOptions, group, position = 1 } of pathGroups) {
if (minimatch(path, pattern, patternOptions || { nocomment: true })) {
return ranks[group] + position / maxPosition;
}
}
}
function computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesGroup) {
let impType;
let rank;
const isTypeGroupInGroups = !ranks.omittedTypes.includes('type');
const isTypeOnlyImport = importEntry.node.importKind === 'type';
const isExcludedFromPathRank = isTypeOnlyImport && isTypeGroupInGroups && excludedImportTypes.has('type');
if (importEntry.type === 'import:object') {
impType = 'object';
}
else if (isTypeOnlyImport && isTypeGroupInGroups && !isSortingTypesGroup) {
impType = 'type';
}
else {
impType = importType(importEntry.value, context);
}
if (!excludedImportTypes.has(impType) && !isExcludedFromPathRank) {
rank =
typeof importEntry.value === 'string'
? computePathRank(ranks.groups, ranks.pathGroups, importEntry.value, ranks.maxPosition)
: undefined;
}
if (rank === undefined) {
rank = ranks.groups[impType];
if (rank === undefined) {
return -1;
}
}
if (isTypeOnlyImport && isSortingTypesGroup) {
rank = ranks.groups.type + rank / 10;
}
if (importEntry.type !== 'import' &&
!importEntry.type.startsWith('import:')) {
rank += 100;
}
return rank;
}
function registerNode(context, importEntry, ranks, imported, excludedImportTypes, isSortingTypesGroup) {
const rank = computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesGroup);
if (rank !== -1) {
let importNode = importEntry.node;
if (importEntry.type === 'require' &&
importNode.parent?.parent?.type === 'VariableDeclaration') {
importNode = importNode.parent.parent;
}
imported.push({
...importEntry,
rank,
isMultiline: importNode.loc.end.line !== importNode.loc.start.line,
});
}
}
function getRequireBlock(node) {
let n = node;
while ((n.parent?.type === 'MemberExpression' && n.parent.object === n) ||
(n.parent?.type === 'CallExpression' && n.parent.callee === n)) {
n = n.parent;
}
if (n.parent?.type === 'VariableDeclarator' &&
n.parent.parent.type === 'VariableDeclaration' &&
n.parent.parent.parent.type === 'Program') {
return n.parent.parent.parent;
}
}
const types = [
'builtin',
'external',
'internal',
'unknown',
'parent',
'sibling',
'index',
'object',
'type',
];
function convertGroupsToRanks(groups) {
const rankObject = groups.reduce((res, group, index) => {
for (const groupItem of [group].flat()) {
if (!types.includes(groupItem)) {
throw new Error(`Incorrect configuration of the rule: Unknown type \`${JSON.stringify(groupItem)}\``);
}
if (res[groupItem] !== undefined) {
throw new Error(`Incorrect configuration of the rule: \`${groupItem}\` is duplicated`);
}
res[groupItem] = index * 2;
}
return res;
}, {});
const omittedTypes = types.filter(type => rankObject[type] === undefined);
const ranks = omittedTypes.reduce(function (res, type) {
res[type] = groups.length * 2;
return res;
}, rankObject);
return { groups: ranks, omittedTypes };
}
function convertPathGroupsForRanks(pathGroups) {
const after = {};
const before = {};
const transformed = pathGroups.map((pathGroup, index) => {
const { group, position: positionString } = pathGroup;
let position = 0;
if (positionString === 'after') {
if (!after[group]) {
after[group] = 1;
}
position = after[group]++;
}
else if (positionString === 'before') {
if (!before[group]) {
before[group] = [];
}
before[group].push(index);
}
return { ...pathGroup, position };
});
let maxPosition = 1;
for (const group of Object.keys(before)) {
const groupLength = before[group].length;
for (const [index, groupIndex] of before[group].entries()) {
transformed[groupIndex].position = -1 * (groupLength - index);
}
maxPosition = Math.max(maxPosition, groupLength);
}
for (const key of Object.keys(after)) {
const groupNextPosition = after[key];
maxPosition = Math.max(maxPosition, groupNextPosition - 1);
}
return {
pathGroups: transformed,
maxPosition: maxPosition > 10 ? Math.pow(10, Math.ceil(Math.log10(maxPosition))) : 10,
};
}
function fixNewLineAfterImport(context, previousImport) {
const prevRoot = findRootNode(previousImport.node);
const tokensToEndOfLine = takeTokensAfterWhile(context.sourceCode, prevRoot, commentOnSameLineAs(prevRoot));
let endOfLine = prevRoot.range[1];
if (tokensToEndOfLine.length > 0) {
endOfLine = tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1];
}
return (fixer) => fixer.insertTextAfterRange([prevRoot.range[0], endOfLine], '\n');
}
function removeNewLineAfterImport(context, currentImport, previousImport) {
const { sourceCode } = context;
const prevRoot = findRootNode(previousImport.node);
const currRoot = findRootNode(currentImport.node);
const rangeToRemove = [
findEndOfLineWithComments(sourceCode, prevRoot),
findStartOfLineWithComments(sourceCode, currRoot),
];
if (/^\s*$/.test(sourceCode.text.slice(rangeToRemove[0], rangeToRemove[1]))) {
return (fixer) => fixer.removeRange(rangeToRemove);
}
return;
}
function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports_, newlinesBetweenTypeOnlyImports_, distinctGroup, isSortingTypesGroup, isConsolidatingSpaceBetweenImports) {
const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => {
return context.sourceCode.lines
.slice(previousImport.node.loc.end.line, currentImport.node.loc.start.line - 1)
.filter(line => line.trim().length === 0).length;
};
const getIsStartOfDistinctGroup = (currentImport, previousImport) => currentImport.rank - 1 >= previousImport.rank;
let previousImport = imported[0];
for (const currentImport of imported.slice(1)) {
const emptyLinesBetween = getNumberOfEmptyLinesBetween(currentImport, previousImport);
const isStartOfDistinctGroup = getIsStartOfDistinctGroup(currentImport, previousImport);
const isTypeOnlyImport = currentImport.node.importKind === 'type';
const isPreviousImportTypeOnlyImport = previousImport.node.importKind === 'type';
const isNormalImportNextToTypeOnlyImportAndRelevant = isTypeOnlyImport !== isPreviousImportTypeOnlyImport && isSortingTypesGroup;
const isTypeOnlyImportAndRelevant = isTypeOnlyImport && isSortingTypesGroup;
const newlinesBetweenImports = isSortingTypesGroup &&
isConsolidatingSpaceBetweenImports &&
(previousImport.isMultiline || currentImport.isMultiline) &&
newlinesBetweenImports_ === 'never'
? 'always-and-inside-groups'
: newlinesBetweenImports_;
const newlinesBetweenTypeOnlyImports = isSortingTypesGroup &&
isConsolidatingSpaceBetweenImports &&
(isNormalImportNextToTypeOnlyImportAndRelevant ||
previousImport.isMultiline ||
currentImport.isMultiline) &&
newlinesBetweenTypeOnlyImports_ === 'never'
? 'always-and-inside-groups'
: newlinesBetweenTypeOnlyImports_;
const isNotIgnored = (isTypeOnlyImportAndRelevant &&
newlinesBetweenTypeOnlyImports !== 'ignore') ||
(!isTypeOnlyImportAndRelevant && newlinesBetweenImports !== 'ignore');
if (isNotIgnored) {
const shouldAssertNewlineBetweenGroups = ((isTypeOnlyImportAndRelevant ||
isNormalImportNextToTypeOnlyImportAndRelevant) &&
(newlinesBetweenTypeOnlyImports === 'always' ||
newlinesBetweenTypeOnlyImports === 'always-and-inside-groups')) ||
(!isTypeOnlyImportAndRelevant &&
!isNormalImportNextToTypeOnlyImportAndRelevant &&
(newlinesBetweenImports === 'always' ||
newlinesBetweenImports === 'always-and-inside-groups'));
const shouldAssertNoNewlineWithinGroup = ((isTypeOnlyImportAndRelevant ||
isNormalImportNextToTypeOnlyImportAndRelevant) &&
newlinesBetweenTypeOnlyImports !== 'always-and-inside-groups') ||
(!isTypeOnlyImportAndRelevant &&
!isNormalImportNextToTypeOnlyImportAndRelevant &&
newlinesBetweenImports !== 'always-and-inside-groups');
const shouldAssertNoNewlineBetweenGroup = !isSortingTypesGroup ||
!isNormalImportNextToTypeOnlyImportAndRelevant ||
newlinesBetweenTypeOnlyImports === 'never';
const isTheNewlineBetweenImportsInTheSameGroup = (distinctGroup && currentImport.rank === previousImport.rank) ||
(!distinctGroup && !isStartOfDistinctGroup);
let alreadyReported = false;
if (shouldAssertNewlineBetweenGroups) {
if (currentImport.rank !== previousImport.rank &&
emptyLinesBetween === 0) {
if (distinctGroup || isStartOfDistinctGroup) {
alreadyReported = true;
context.report({
node: previousImport.node,
messageId: 'oneLineBetweenGroups',
fix: fixNewLineAfterImport(context, previousImport),
});
}
}
else if (emptyLinesBetween > 0 &&
shouldAssertNoNewlineWithinGroup &&
isTheNewlineBetweenImportsInTheSameGroup) {
alreadyReported = true;
context.report({
node: previousImport.node,
messageId: 'noLineWithinGroup',
fix: removeNewLineAfterImport(context, currentImport, previousImport),
});
}
}
else if (emptyLinesBetween > 0 && shouldAssertNoNewlineBetweenGroup) {
alreadyReported = true;
context.report({
node: previousImport.node,
messageId: 'noLineBetweenGroups',
fix: removeNewLineAfterImport(context, currentImport, previousImport),
});
}
if (!alreadyReported && isConsolidatingSpaceBetweenImports) {
if (emptyLinesBetween === 0 && currentImport.isMultiline) {
context.report({
node: previousImport.node,
messageId: 'oneLineBetweenTheMultiLineImport',
fix: fixNewLineAfterImport(context, previousImport),
});
}
else if (emptyLinesBetween === 0 && previousImport.isMultiline) {
context.report({
node: previousImport.node,
messageId: 'oneLineBetweenThisMultiLineImport',
fix: fixNewLineAfterImport(context, previousImport),
});
}
else if (emptyLinesBetween > 0 &&
!previousImport.isMultiline &&
!currentImport.isMultiline &&
isTheNewlineBetweenImportsInTheSameGroup) {
context.report({
node: previousImport.node,
messageId: 'noLineBetweenSingleLineImport',
fix: removeNewLineAfterImport(context, currentImport, previousImport),
});
}
}
}
previousImport = currentImport;
}
}
function getAlphabetizeConfig(options) {
const alphabetize = options.alphabetize || {};
const order = alphabetize.order || 'ignore';
const orderImportKind = alphabetize.orderImportKind || 'ignore';
const caseInsensitive = alphabetize.caseInsensitive || false;
return { order, orderImportKind, caseInsensitive };
}
const defaultDistinctGroup = true;
export default createRule({
name: 'order',
meta: {
type: 'suggestion',
docs: {
category: 'Style guide',
description: 'Enforce a convention in module import order.',
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
groups: {
type: 'array',
},
pathGroupsExcludedImportTypes: {
type: 'array',
},
distinctGroup: {
type: 'boolean',
default: defaultDistinctGroup,
},
pathGroups: {
type: 'array',
items: {
type: 'object',
properties: {
pattern: {
type: 'string',
},
patternOptions: {
type: 'object',
},
group: {
type: 'string',
enum: types,
},
position: {
type: 'string',
enum: ['after', 'before'],
},
},
additionalProperties: false,
required: ['pattern', 'group'],
},
},
'newlines-between': {
type: 'string',
enum: ['ignore', 'always', 'always-and-inside-groups', 'never'],
},
'newlines-between-types': {
type: 'string',
enum: ['ignore', 'always', 'always-and-inside-groups', 'never'],
},
consolidateIslands: {
type: 'string',
enum: ['inside-groups', 'never'],
},
sortTypesGroup: {
type: 'boolean',
default: false,
},
named: {
default: false,
oneOf: [
{
type: 'boolean',
},
{
type: 'object',
properties: {
enabled: { type: 'boolean' },
import: { type: 'boolean' },
export: { type: 'boolean' },
require: { type: 'boolean' },
cjsExports: { type: 'boolean' },
types: {
type: 'string',
enum: ['mixed', 'types-first', 'types-last'],
},
},
additionalProperties: false,
},
],
},
alphabetize: {
type: 'object',
properties: {
caseInsensitive: {
type: 'boolean',
default: false,
},
order: {
type: 'string',
enum: ['ignore', 'asc', 'desc'],
default: 'ignore',
},
orderImportKind: {
type: 'string',
enum: ['ignore', 'asc', 'desc'],
default: 'ignore',
},
},
additionalProperties: false,
},
warnOnUnassignedImports: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
dependencies: {
'newlines-between-types': {
type: 'object',
properties: {
sortTypesGroup: {
type: 'boolean',
enum: [true],
},
},
required: ['sortTypesGroup'],
},
consolidateIslands: {
anyOf: [
{
type: 'object',
properties: {
'newlines-between': {
type: 'string',
enum: ['always-and-inside-groups'],
},
},
required: ['newlines-between'],
},
{
type: 'object',
properties: {
'newlines-between-types': {
type: 'string',
enum: ['always-and-inside-groups'],
},
},
required: ['newlines-between-types'],
},
],
},
},
},
],
messages: {
error: '{{error}}',
noLineWithinGroup: 'There should be no empty line within import group',
noLineBetweenGroups: 'There should be no empty line between import groups',
oneLineBetweenGroups: 'There should be at least one empty line between import groups',
order: '{{secondImport}} should occur {{order}} {{firstImport}}',
oneLineBetweenTheMultiLineImport: 'There should be at least one empty line between this import and the multi-line import that follows it',
oneLineBetweenThisMultiLineImport: 'There should be at least one empty line between this multi-line import and the import that follows it',
noLineBetweenSingleLineImport: 'There should be no empty lines between this single-line import and the single-line import that follows it',
},
},
defaultOptions: [],
create(context) {
const options = context.options[0] || {};
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
const newlinesBetweenTypeOnlyImports = options['newlines-between-types'] || newlinesBetweenImports;
const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes ||
['builtin', 'external', 'object']);
const sortTypesGroup = options.sortTypesGroup;
const consolidateIslands = options.consolidateIslands || 'never';
const named = {
types: 'mixed',
...(typeof options.named === 'object'
? {
...options.named,
import: 'import' in options.named
? options.named.import
: options.named.enabled,
export: 'export' in options.named
? options.named.export
: options.named.enabled,
require: 'require' in options.named
? options.named.require
: options.named.enabled,
cjsExports: 'cjsExports' in options.named
? options.named.cjsExports
: options.named.enabled,
}
: {
import: options.named,
export: options.named,
require: options.named,
cjsExports: options.named,
}),
};
const namedGroups = named.types === 'mixed'
? []
: named.types === 'types-last'
? ['value']
: ['type'];
const alphabetize = getAlphabetizeConfig(options);
const distinctGroup = options.distinctGroup == null
? defaultDistinctGroup
: !!options.distinctGroup;
let ranks;
try {
const { pathGroups, maxPosition } = convertPathGroupsForRanks(options.pathGroups || []);
const { groups, omittedTypes } = convertGroupsToRanks(options.groups || defaultGroups);
ranks = {
groups,
omittedTypes,
pathGroups,
maxPosition,
};
}
catch (error) {
return {
Program(node) {
context.report({
node,
messageId: 'error',
data: {
error: error.message,
},
});
},
};
}
const importMap = new Map();
const exportMap = new Map();
const isTypeGroupInGroups = !ranks.omittedTypes.includes('type');
const isSortingTypesGroup = isTypeGroupInGroups && sortTypesGroup;
function getBlockImports(node) {
let blockImports = importMap.get(node);
if (!blockImports) {
importMap.set(node, (blockImports = []));
}
return blockImports;
}
function getBlockExports(node) {
let blockExports = exportMap.get(node);
if (!blockExports) {
exportMap.set(node, (blockExports = []));
}
return blockExports;
}
function makeNamedOrderReport(context, namedImports) {
if (namedImports.length > 1) {
const imports = namedImports.map(namedImport => {
const kind = namedImport.kind || 'value';
const rank = namedGroups.indexOf(kind);
return {
displayName: namedImport.value,
rank: rank === -1 ? namedGroups.length : rank,
...namedImport,
value: `${namedImport.value}:${namedImport.alias || ''}`,
};
});
if (alphabetize.order !== 'ignore') {
mutateRanksToAlphabetize(imports, alphabetize);
}
makeOutOfOrderReport(context, imports, categories.named);
}
}
return {
ImportDeclaration(node) {
if (node.specifiers.length > 0 || options.warnOnUnassignedImports) {
const name = node.source.value;
registerNode(context, {
node,
value: name,
displayName: name,
type: 'import',
}, ranks, getBlockImports(node.parent), pathGroupsExcludedImportTypes, isSortingTypesGroup);
if (named.import) {
makeNamedOrderReport(context, node.specifiers
.filter(specifier => specifier.type === 'ImportSpecifier')
.map(specifier => ({
node: specifier,
value: getValue(specifier.imported),
type: 'import',
kind: specifier.importKind,
...(specifier.local.range[0] !==
specifier.imported.range[0] && {
alias: specifier.local.name,
}),
})));
}
}
},
TSImportEqualsDeclaration(node) {
if (node.isExport) {
return;
}
let displayName;
let value;
let type;
if (node.moduleReference.type === 'TSExternalModuleReference') {
value = node.moduleReference.expression.value;
displayName = value;
type = 'import';
}
else {
value = '';
displayName = context.sourceCode.getText(node.moduleReference);
type = 'import:object';
}
registerNode(context, {
node,
value,
displayName,
type,
}, ranks, getBlockImports(node.parent), pathGroupsExcludedImportTypes, isSortingTypesGroup);
},
CallExpression(node) {
if (!isStaticRequire(node)) {
return;
}
const block = getRequireBlock(node);
const firstArg = node.arguments[0];
if (!block || !('value' in firstArg)) {
return;
}
const { value } = firstArg;
registerNode(context, {
node,
value,
displayName: value,
type: 'require',
}, ranks, getBlockImports(block), pathGroupsExcludedImportTypes, isSortingTypesGroup);
},
...(named.require && {
VariableDeclarator(node) {
if (node.id.type === 'ObjectPattern' &&
isRequireExpression(node.init)) {
const { properties } = node.id;
for (const p of properties) {
if (!('key' in p) ||
p.key.type !== 'Identifier' ||
p.value.type !== 'Identifier') {
return;
}
}
makeNamedOrderReport(context, node.id.properties.map(prop_ => {
const prop = prop_;
const key = prop.key;
const value = prop.value;
return {
node: prop,
value: key.name,
type: 'require',
...(key.range[0] !== value.range[0] && {
alias: value.name,
}),
};
}));
}
},
}),
...(named.export && {
ExportNamedDeclaration(node) {
makeNamedOrderReport(context, node.specifiers.map(specifier => ({
node: specifier,
value: getValue(specifier.local),
type: 'export',
kind: specifier.exportKind,
...(specifier.local.range[0] !== specifier.exported.range[0] && {
alias: getValue(specifier.exported),
}),
})));
},
}),
...(named.cjsExports && {
AssignmentExpression(node) {
if (node.parent.type === 'ExpressionStatement') {
if (isCJSExports(context, node.left)) {
if (node.right.type === 'ObjectExpression') {
const { properties } = node.right;
for (const p of properties) {
if (!('key' in p) ||
p.key.type !== 'Identifier' ||
p.value.type !== 'Identifier') {
return;
}
}
makeNamedOrderReport(context, properties.map(prop_ => {
const prop = prop_;
const key = prop.key;
const value = prop.value;
return {
node: prop,
value: key.name,
type: 'export',
...(key.range[0] !== value.range[0] && {
alias: value.name,
}),
};
}));
}
}
else {
const nameParts = getNamedCJSExports(context, node.left);
if (nameParts && nameParts.length > 0) {
const name = nameParts.join('.');
getBlockExports(node.parent.parent).push({
node,
value: name,
displayName: name,
type: 'export',
rank: 0,
});
}
}
}
},
}),
'Program:exit'() {
for (const imported of importMap.values()) {
if (newlinesBetweenImports !== 'ignore' ||
newlinesBetweenTypeOnlyImports !== 'ignore') {
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, newlinesBetweenTypeOnlyImports, distinctGroup, isSortingTypesGroup, consolidateIslands === 'inside-groups' &&
(newlinesBetweenImports === 'always-and-inside-groups' ||
newlinesBetweenTypeOnlyImports ===
'always-and-inside-groups'));
}
if (alphabetize.order !== 'ignore') {
mutateRanksToAlphabetize(imported, alphabetize);
}
makeOutOfOrderReport(context, imported, categories.import);
}
for (const exported of exportMap.values()) {
if (alphabetize.order !== 'ignore') {
mutateRanksToAlphabetize(exported, alphabetize);
makeOutOfOrderReport(context, exported, categories.exports);
}
}
importMap.clear();
exportMap.clear();
},
};
},
});
//# sourceMappingURL=order.js.map