190 lines
No EOL
9.5 KiB
JavaScript
190 lines
No EOL
9.5 KiB
JavaScript
import { a as createRule, t as resolve } from "../utils.mjs";
|
|
|
|
//#region src/rules/no-duplicates/no-duplicates.ts
|
|
function checkImports(imported, context) {
|
|
imported.forEach((nodes, module) => {
|
|
if (nodes.length <= 1) return;
|
|
for (let i = 0, len = nodes.length; i < len; i++) {
|
|
const node = nodes[i];
|
|
context.report({
|
|
node: node.source,
|
|
messageId: "duplicate",
|
|
data: { module },
|
|
fix: i === 0 ? getFix(nodes, context.sourceCode, context) : null
|
|
});
|
|
}
|
|
});
|
|
}
|
|
function getFix(nodes, sourceCode, context) {
|
|
const first = nodes[0];
|
|
if (hasProblematicComments(first, sourceCode) || hasNamespace(first)) return null;
|
|
const defaultImportNames = new Set(nodes.flatMap((x) => getDefaultImportName(x) || []));
|
|
if (defaultImportNames.size > 1) return null;
|
|
const restWithoutCommentsAndNamespaces = nodes.slice(1).filter((node) => !hasProblematicComments(node, sourceCode) && !hasNamespace(node));
|
|
const restWithoutCommentsAndNamespacesHasSpecifiers = restWithoutCommentsAndNamespaces.map(hasSpecifiers);
|
|
const specifiers = restWithoutCommentsAndNamespaces.reduce((acc, node, nodeIndex) => {
|
|
const tokens = sourceCode.getTokens(node);
|
|
const openBrace = tokens.find((token) => isPunctuator(token, "{"));
|
|
const closeBrace = tokens.find((token) => isPunctuator(token, "}"));
|
|
if (openBrace == null || closeBrace == null) return acc;
|
|
acc.push({
|
|
importNode: node,
|
|
identifiers: sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]).split(","),
|
|
isEmpty: !restWithoutCommentsAndNamespacesHasSpecifiers[nodeIndex]
|
|
});
|
|
return acc;
|
|
}, []);
|
|
const unnecessaryImports = restWithoutCommentsAndNamespaces.filter((node, nodeIndex) => !restWithoutCommentsAndNamespacesHasSpecifiers[nodeIndex] && !specifiers.some((specifier) => specifier.importNode === node));
|
|
const shouldAddSpecifiers = specifiers.length > 0;
|
|
const shouldRemoveUnnecessary = unnecessaryImports.length > 0;
|
|
const shouldAddDefault = getDefaultImportName(first) == null && defaultImportNames.size === 1;
|
|
if (!shouldAddSpecifiers && !shouldRemoveUnnecessary && !shouldAddDefault) return null;
|
|
const preferInline = context.options[0] && context.options[0]["prefer-inline"];
|
|
return (fixer) => {
|
|
const tokens = sourceCode.getTokens(first);
|
|
const openBrace = tokens.find((token) => isPunctuator(token, "{"));
|
|
const closeBrace = tokens.find((token) => isPunctuator(token, "}"));
|
|
const firstToken = sourceCode.getFirstToken(first);
|
|
const [defaultImportName] = defaultImportNames;
|
|
const firstHasTrailingComma = closeBrace != null && isPunctuator(sourceCode.getTokenBefore(closeBrace), ",");
|
|
const firstIsEmpty = !hasSpecifiers(first);
|
|
const firstExistingIdentifiers = firstIsEmpty ? /* @__PURE__ */ new Set() : new Set(sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]).split(",").map((x) => x.split(" as ")[0].trim()));
|
|
const [specifiersText] = specifiers.reduce(([result, needsComma, existingIdentifiers], specifier) => {
|
|
const isTypeSpecifier = "importNode" in specifier && specifier.importNode.importKind === "type";
|
|
const [specifierText, updatedExistingIdentifiers] = specifier.identifiers.reduce(([text, set], cur) => {
|
|
const trimmed = cur.trim();
|
|
if (trimmed.length === 0 || existingIdentifiers.has(trimmed)) return [text, set];
|
|
const curWithType = preferInline && isTypeSpecifier ? cur.replace(/^(\s*)/, "$1type ") : cur;
|
|
return [text.length > 0 ? `${text},${curWithType}` : curWithType, set.add(trimmed)];
|
|
}, ["", existingIdentifiers]);
|
|
return [
|
|
needsComma && !specifier.isEmpty && specifierText.length > 0 ? `${result},${specifierText}` : `${result}${specifierText}`,
|
|
specifier.isEmpty ? needsComma : true,
|
|
updatedExistingIdentifiers
|
|
];
|
|
}, [
|
|
"",
|
|
!firstHasTrailingComma && !firstIsEmpty,
|
|
firstExistingIdentifiers
|
|
]);
|
|
const fixes = [];
|
|
if (shouldAddSpecifiers && preferInline && first.importKind === "type") {
|
|
const typeIdentifierToken = tokens.find((token) => token.type === "Identifier" && token.value === "type");
|
|
if (typeIdentifierToken) fixes.push(fixer.removeRange([typeIdentifierToken.range[0], typeIdentifierToken.range[1] + 1]));
|
|
for (const identifier of tokens.filter((token) => firstExistingIdentifiers.has(token.value))) fixes.push(fixer.replaceTextRange([identifier.range[0], identifier.range[1]], `type ${identifier.value}`));
|
|
}
|
|
if (openBrace == null && shouldAddSpecifiers && shouldAddDefault) fixes.push(fixer.insertTextAfter(firstToken, ` ${defaultImportName}, {${specifiersText}} from`));
|
|
else if (openBrace == null && !shouldAddSpecifiers && shouldAddDefault) fixes.push(fixer.insertTextAfter(firstToken, ` ${defaultImportName} from`));
|
|
else if (openBrace != null && closeBrace != null && shouldAddDefault) {
|
|
fixes.push(fixer.insertTextAfter(firstToken, ` ${defaultImportName},`));
|
|
if (shouldAddSpecifiers) fixes.push(fixer.insertTextBefore(closeBrace, specifiersText));
|
|
} else if (openBrace == null && shouldAddSpecifiers && !shouldAddDefault) if (first.specifiers.length === 0) fixes.push(fixer.insertTextAfter(firstToken, ` {${specifiersText}} from`));
|
|
else fixes.push(fixer.insertTextAfter(first.specifiers[0], `, {${specifiersText}}`));
|
|
else if (openBrace != null && closeBrace != null && !shouldAddDefault) {
|
|
const tokenBefore = sourceCode.getTokenBefore(closeBrace);
|
|
fixes.push(fixer.insertTextAfter(tokenBefore, specifiersText));
|
|
}
|
|
for (const specifier of specifiers) {
|
|
const importNode = specifier.importNode;
|
|
fixes.push(fixer.remove(importNode));
|
|
const charAfterImportRange = [importNode.range[1], importNode.range[1] + 1];
|
|
if (sourceCode.text.slice(charAfterImportRange[0], charAfterImportRange[1]) === "\n") fixes.push(fixer.removeRange(charAfterImportRange));
|
|
}
|
|
for (const node of unnecessaryImports) {
|
|
fixes.push(fixer.remove(node));
|
|
const charAfterImportRange = [node.range[1], node.range[1] + 1];
|
|
if (sourceCode.text.slice(charAfterImportRange[0], charAfterImportRange[1]) === "\n") fixes.push(fixer.removeRange(charAfterImportRange));
|
|
}
|
|
return fixes;
|
|
};
|
|
}
|
|
function isPunctuator(node, value) {
|
|
return node.type === "Punctuator" && node.value === value;
|
|
}
|
|
function getDefaultImportName(node) {
|
|
return node.specifiers.find((specifier) => specifier.type === "ImportDefaultSpecifier")?.local.name;
|
|
}
|
|
function hasNamespace(node) {
|
|
return node.specifiers.some((specifier) => specifier.type === "ImportNamespaceSpecifier");
|
|
}
|
|
function hasSpecifiers(node) {
|
|
return node.specifiers.some((specifier) => specifier.type === "ImportSpecifier");
|
|
}
|
|
function hasProblematicComments(node, sourceCode) {
|
|
return hasCommentBefore(node, sourceCode) || hasCommentAfter(node, sourceCode) || hasCommentInsideNonSpecifiers(node, sourceCode);
|
|
}
|
|
function hasCommentBefore(node, sourceCode) {
|
|
return sourceCode.getCommentsBefore(node).some((comment) => comment.loc.end.line >= node.loc.start.line - 1);
|
|
}
|
|
function hasCommentAfter(node, sourceCode) {
|
|
return sourceCode.getCommentsAfter(node).some((comment) => comment.loc.start.line === node.loc.end.line);
|
|
}
|
|
function hasCommentInsideNonSpecifiers(node, sourceCode) {
|
|
const tokens = sourceCode.getTokens(node);
|
|
const openBraceIndex = tokens.findIndex((token) => isPunctuator(token, "{"));
|
|
const closeBraceIndex = tokens.findIndex((token) => isPunctuator(token, "}"));
|
|
return (openBraceIndex !== -1 && closeBraceIndex !== -1 ? [...tokens.slice(1, openBraceIndex + 1), ...tokens.slice(closeBraceIndex + 1)] : tokens.slice(1)).some((token) => sourceCode.getCommentsBefore(token).length > 0);
|
|
}
|
|
var no_duplicates_default = createRule({
|
|
name: "no-duplicates",
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
recommended: true,
|
|
description: "Forbid repeated import of the same module in multiple places."
|
|
},
|
|
fixable: "code",
|
|
schema: [{
|
|
type: "object",
|
|
properties: { "prefer-inline": { type: "boolean" } },
|
|
additionalProperties: false
|
|
}],
|
|
messages: { duplicate: "'{{module}}' imported multiple times." }
|
|
},
|
|
defaultOptions: [],
|
|
create(context) {
|
|
const preferInline = context.options[0]?.["prefer-inline"];
|
|
const moduleMaps = /* @__PURE__ */ new Map();
|
|
function getImportMap(n) {
|
|
const parent = n.parent;
|
|
let map;
|
|
if (moduleMaps.has(parent)) map = moduleMaps.get(parent);
|
|
else {
|
|
map = {
|
|
imported: /* @__PURE__ */ new Map(),
|
|
nsImported: /* @__PURE__ */ new Map(),
|
|
defaultTypesImported: /* @__PURE__ */ new Map(),
|
|
namespaceTypesImported: /* @__PURE__ */ new Map(),
|
|
namedTypesImported: /* @__PURE__ */ new Map()
|
|
};
|
|
moduleMaps.set(parent, map);
|
|
}
|
|
if (n.importKind === "type") {
|
|
if (n.specifiers.length > 0 && n.specifiers[0].type === "ImportDefaultSpecifier") return map.defaultTypesImported;
|
|
if (n.specifiers.length > 0 && n.specifiers[0].type === "ImportNamespaceSpecifier") return map.namespaceTypesImported;
|
|
if (!preferInline) return map.namedTypesImported;
|
|
}
|
|
if (!preferInline && n.specifiers.some((spec) => "importKind" in spec && spec.importKind === "type")) return map.namedTypesImported;
|
|
return hasNamespace(n) ? map.nsImported : map.imported;
|
|
}
|
|
return {
|
|
ImportDeclaration(n) {
|
|
const resolvedPath = resolve(n.source.value);
|
|
const importMap = getImportMap(n);
|
|
if (importMap.has(resolvedPath)) importMap.get(resolvedPath).push(n);
|
|
else importMap.set(resolvedPath, [n]);
|
|
},
|
|
"Program:exit": function() {
|
|
for (const map of moduleMaps.values()) {
|
|
checkImports(map.imported, context);
|
|
checkImports(map.nsImported, context);
|
|
checkImports(map.defaultTypesImported, context);
|
|
checkImports(map.namedTypesImported, context);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
//#endregion
|
|
export { no_duplicates_default as t }; |