@j4cobi/eslint-plugin-sort-imports
Version:
An ESLint rule that can auto fix import sorting issues.
243 lines • 11.6 kB
JavaScript
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator((name) => name);
var MemberSyntaxSortOrder = /* @__PURE__ */ ((MemberSyntaxSortOrder2) => {
MemberSyntaxSortOrder2["None"] = "none";
MemberSyntaxSortOrder2["All"] = "all";
MemberSyntaxSortOrder2["Multiple"] = "multiple";
MemberSyntaxSortOrder2["Single"] = "single";
return MemberSyntaxSortOrder2;
})(MemberSyntaxSortOrder || {});
const defaultOptions = [
{
ignoreCase: false,
ignoreMemberSort: false,
memberSyntaxSortOrder: ["none" /* None */, "all" /* All */, "multiple" /* Multiple */, "single" /* Single */],
typeSortStrategy: "after"
}
];
const rule = createRule({
name: "eslint-sort-imports",
defaultOptions,
meta: {
docs: { description: "enforce sorted import declarations within modules" },
messages: {
memberAlphabetical: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
typeOrder: "Expected type imports '{{typeSortStrategy}}' all other imports.",
wrongOrder: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.",
alphabeticalOrder: "Imports should be sorted alphabetically."
},
schema: [
{
type: "object",
properties: {
ignoreCase: { type: "boolean" },
memberSyntaxSortOrder: {
type: "array",
items: {
type: "string",
enum: ["none", "all", "multiple", "single"]
},
uniqueItems: true,
minItems: 4,
maxItems: 4
},
typeSortStrategy: {
type: "string",
enum: ["mixed", "before", "after"]
},
ignoreMemberSort: { type: "boolean" }
},
additionalProperties: false
}
],
type: "layout",
fixable: "code"
},
create(context) {
const config = context.options[0];
const ignoreCase = config?.ignoreCase ?? defaultOptions[0].ignoreCase;
const ignoreMemberSort = config?.ignoreMemberSort ?? defaultOptions[0].ignoreMemberSort;
const memberSyntaxSortOrder = config?.memberSyntaxSortOrder ?? defaultOptions[0].memberSyntaxSortOrder;
const typeSortStrategy = config?.typeSortStrategy ?? defaultOptions[0].typeSortStrategy;
const sourceCode = context.sourceCode;
let previousDeclaration = void 0;
let initialSource = void 0;
let allDeclarations = sourceCode.ast.body.filter((n) => n.type === "ImportDeclaration");
function sortAndFixAllNodes(initial, nodes) {
const rich = nodes.map((node) => [node, initial.substring(node.range[0], node.range[1])]);
const betweens = nodes.map((node, i) => i !== nodes.length - 1 ? initial.substring(node.range[1], nodes[i + 1].range[0]) : null).filter((n) => n !== null);
const fixed = rich.map((n) => {
const node = n[0];
if (!ignoreMemberSort) {
const importSpecifiers = node.specifiers.filter((specifier) => specifier.type === "ImportSpecifier");
const firstUnsortedIndex = importSpecifiers.map((s) => getSortableName(s, ignoreCase)).findIndex((name, index, array) => array[index - 1] > name);
if (firstUnsortedIndex !== -1) {
const before = initial.substring(node.range[0], importSpecifiers[0].range[0]);
const after = initial.substring(importSpecifiers[importSpecifiers.length - 1].range[1], node.range[1]);
const between = importSpecifiers.slice().sort((specifierA, specifierB) => {
const aName = getSortableName(specifierA, ignoreCase);
const bName = getSortableName(specifierB, ignoreCase);
return aName > bName ? 1 : -1;
}).reduce((sourceText, specifier, index) => {
const textAfterSpecifier = index === importSpecifiers.length - 1 ? "" : initial.slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
return sourceText + initial.substring.apply(initial, specifier.range) + textAfterSpecifier;
}, "");
return [node, `${before}${between}${after}`];
}
}
return n;
});
const sections = fixed.reduce(
(sections2, current) => {
const lastSection = sections2[sections2.length - 1];
if (lastSection.length === 0) lastSection.push(current);
else {
const lastFixed = lastSection[lastSection.length - 1];
if (isLineBetween(lastFixed[0], current[0])) sections2.push([current]);
else lastSection.push(current);
}
return sections2;
},
[[]]
);
const sorted = sections.map(
(section) => section.sort((a, b) => {
const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(b[0], memberSyntaxSortOrder);
const currentMemberIsType = b[0].importKind && b[0].importKind === "type" || false;
const previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(a[0], memberSyntaxSortOrder);
const previousMemberIsType = a[0].importKind && a[0].importKind === "type" || false;
let currentLocalMemberName = getFirstLocalMemberName(b[0]);
let previousLocalMemberName = getFirstLocalMemberName(a[0]);
if (ignoreCase) {
previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase();
currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase();
}
if (typeSortStrategy !== "mixed" && currentMemberIsType !== previousMemberIsType) {
return currentMemberIsType && typeSortStrategy === "before" || previousMemberIsType && typeSortStrategy === "after" ? 1 : -1;
}
if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) {
return currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex ? 1 : -1;
} else if (previousLocalMemberName && currentLocalMemberName) {
return currentLocalMemberName < previousLocalMemberName ? 1 : -1;
}
return 0;
})
).reduce((a, c) => a.concat(c), []);
return sorted.map((n) => n[1]).reduce((done, current, i) => `${done}${i !== 0 ? betweens[i - 1] : ""}${current}`, "");
}
return {
ImportDeclaration(node) {
if (!initialSource) initialSource = sourceCode.getText();
if (previousDeclaration && !isLineBetween(previousDeclaration, node)) {
const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node, memberSyntaxSortOrder);
const currentMemberIsType = node.importKind && node.importKind === "type" || false;
const previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration, memberSyntaxSortOrder);
const previousMemberIsType = previousDeclaration.importKind && previousDeclaration.importKind === "type" || false;
let currentLocalMemberName = getFirstLocalMemberName(node);
let previousLocalMemberName = getFirstLocalMemberName(previousDeclaration);
if (ignoreCase) {
previousLocalMemberName = previousLocalMemberName.toLowerCase();
currentLocalMemberName = currentLocalMemberName.toLowerCase();
}
if (typeSortStrategy !== "mixed" && currentMemberIsType !== previousMemberIsType) {
if (currentMemberIsType && typeSortStrategy === "before" || previousMemberIsType && typeSortStrategy === "after") {
context.report({
node,
messageId: "typeOrder",
data: { typeSortStrategy },
fix(fixer) {
return fixer.replaceTextRange(
[allDeclarations[0].range[0], allDeclarations[allDeclarations.length - 1].range[1]],
sortAndFixAllNodes(initialSource, allDeclarations)
);
}
});
}
} else if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) {
if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) {
context.report({
node,
messageId: "wrongOrder",
data: {
syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex],
syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex]
},
fix(fixer) {
return fixer.replaceTextRange(
[allDeclarations[0].range[0], allDeclarations[allDeclarations.length - 1].range[1]],
sortAndFixAllNodes(initialSource, allDeclarations)
);
}
});
}
} else {
if (previousLocalMemberName && currentLocalMemberName && currentLocalMemberName < previousLocalMemberName) {
context.report({
node,
messageId: "alphabeticalOrder",
fix(fixer) {
return fixer.replaceTextRange(
[allDeclarations[0].range[0], allDeclarations[allDeclarations.length - 1].range[1]],
sortAndFixAllNodes(initialSource, allDeclarations)
);
}
});
}
}
}
if (!ignoreMemberSort) {
const importSpecifiers = node.specifiers.filter((specifier) => specifier.type === AST_NODE_TYPES.ImportSpecifier);
const firstUnsortedIndex = importSpecifiers.map((s) => getSortableName(s, ignoreCase)).findIndex((name, index, array) => array[index - 1] > name);
if (firstUnsortedIndex !== -1) {
context.report({
node: importSpecifiers[firstUnsortedIndex],
messageId: "memberAlphabetical",
data: {
memberName: importSpecifiers[firstUnsortedIndex].local.name
},
fix(fixer) {
const hasComments = importSpecifiers.some((specifier) => sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length);
if (hasComments) return null;
return fixer.replaceTextRange(
[allDeclarations[0].range[0], allDeclarations[allDeclarations.length - 1].range[1]],
sortAndFixAllNodes(initialSource, allDeclarations)
);
}
});
}
}
previousDeclaration = node;
}
};
}
});
function getSortableName(specifier, ignoreCase) {
if (ignoreCase) return specifier.local.name.toLowerCase();
return specifier.local.name;
}
function usedMemberSyntax(node) {
switch (node.specifiers[0]?.type) {
case AST_NODE_TYPES.ImportNamespaceSpecifier:
return "all" /* All */;
case AST_NODE_TYPES.ImportDefaultSpecifier:
return "single" /* Single */;
case AST_NODE_TYPES.ImportSpecifier:
return "multiple" /* Multiple */;
default:
return "none" /* None */;
}
}
function getMemberParameterGroupIndex(node, memberSyntaxSortOrder) {
return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node));
}
function getFirstLocalMemberName(node) {
return node.specifiers.length ? node.specifiers[0].local.name : node.source.value;
}
function isLineBetween(firstNode, secondNode) {
return firstNode.loc.end.line < secondNode.loc.start.line - 1;
}
export {
MemberSyntaxSortOrder,
rule
};
//# sourceMappingURL=sort-imports.js.map