Files

242 lines
11 KiB
JavaScript

import { a as createRule, t as resolve } from "../utils.mjs";
//#region src/rules/no-duplicates/no-duplicates.ts
const LEADING_WHITESPACE_PATTERN = /^(\s*)/;
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 getSpecifiersByKind(node) {
const typeSpecs = [];
const valueSpecs = [];
for (const spec of node.specifiers) {
if (spec.type !== "ImportSpecifier") continue;
const name = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
const localName = spec.local.name;
("importKind" in spec && spec.importKind === "type" ? typeSpecs : valueSpecs).push({
name,
localName
});
}
return {
typeSpecs,
valueSpecs
};
}
function formatSpecifier(s) {
return s.name !== s.localName ? `${s.name} as ${s.localName}` : s.name;
}
function getFix(nodes, sourceCode, context) {
const first = nodes[0];
const isTypeOnlyImport = first.importKind === "type";
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;
const entry = {
importNode: node,
identifiers: sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]).split(","),
isEmpty: !restWithoutCommentsAndNamespacesHasSpecifiers[nodeIndex]
};
if (isTypeOnlyImport && node.importKind !== "type") {
const { typeSpecs, valueSpecs } = getSpecifiersByKind(node);
if (typeSpecs.length > 0 && valueSpecs.length > 0) {
entry.typeSpecs = typeSpecs;
entry.valueSpecs = valueSpecs;
}
}
acc.push(entry);
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) => {
if (specifier.typeSpecs) {
const newSpecs = specifier.typeSpecs.filter((ts) => !existingIdentifiers.has(ts.name));
if (newSpecs.length === 0) return [
result,
needsComma,
existingIdentifiers
];
const text = newSpecs.map(formatSpecifier).join(", ");
const updatedSet = new Set(existingIdentifiers);
newSpecs.forEach((ts) => updatedSet.add(ts.name));
return [
needsComma ? `${result}, ${text}` : `${result}${text}`,
true,
updatedSet
];
}
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(LEADING_WHITESPACE_PATTERN, "$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;
if (specifier.valueSpecs) {
const nodeTokens = sourceCode.getTokens(importNode);
const nodeOpenBrace = nodeTokens.find((token) => isPunctuator(token, "{"));
const nodeCloseBrace = nodeTokens.find((token) => isPunctuator(token, "}"));
if (nodeOpenBrace && nodeCloseBrace) fixes.push(fixer.replaceTextRange([nodeOpenBrace.range[1], nodeCloseBrace.range[0]], ` ${specifier.valueSpecs.map(formatSpecifier).join(", ")} `));
continue;
}
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 };