knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
475 lines (474 loc) • 23.9 kB
JavaScript
import { isBuiltin } from 'node:module';
import ts from 'typescript';
import { ALIAS_TAG, IMPORT_FLAGS, IMPORT_STAR, OPAQUE, PROTOCOL_VIRTUAL, SIDE_EFFECTS } from "../constants.js";
import { addNsValue, addValue, createImports } from "../util/module-graph.js";
import { getPackageNameFromFilePath, isStartsLikePackageName, sanitizeSpecifier } from "../util/modules.js";
import { timerify } from "../util/Performance.js";
import { dirname, isInNodeModules, resolve } from "../util/path.js";
import { shouldIgnore } from "../util/tag.js";
import { getAccessMembers, getDestructuredNames, getJSDocTags, getLineAndCharacterOfPosition, getTypeRef, isAccessExpression, isConsiderReferencedNS, isDestructuring, isImportSpecifier, isInForIteration, isKeyofTypeof, isObjectEnumerationCallExpressionArgument, isReferencedInExport, } from "./ast-helpers.js";
import { _hasRefsInFile } from "./has-refs-in-file.js";
import { getImportsFromPragmas } from "./pragmas/index.js";
import getDynamicImportVisitors from "./visitors/dynamic-imports/index.js";
import getExportVisitors from "./visitors/exports/index.js";
import getImportVisitors from "./visitors/imports/index.js";
import getScriptVisitors from "./visitors/scripts/index.js";
const isInTopLevelScope = (node, sourceFile) => {
let current = node.parent;
while (current && current !== sourceFile) {
if (ts.isFunctionLike(current) || ts.isClassLike(current))
return false;
current = current.parent;
}
return current === sourceFile;
};
const getVisitors = (sourceFile, visitors) => ({
export: getExportVisitors(sourceFile),
import: getImportVisitors(sourceFile),
dynamicImport: getDynamicImportVisitors(sourceFile, visitors.dynamicImport),
script: getScriptVisitors(sourceFile, visitors.script),
});
const shouldCountRefs = (ignoreExportsUsedInFile, type) => ignoreExportsUsedInFile === true ||
(typeof ignoreExportsUsedInFile === 'object' && type !== 'unknown' && ignoreExportsUsedInFile[type]);
const createMember = (node, member, pos) => {
const { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(pos);
return {
symbol: member.node.symbol,
identifier: member.identifier,
type: member.type,
pos: member.pos,
line: line + 1,
col: character + 1,
fix: member.fix,
hasRefsInFile: false,
jsDocTags: getJSDocTags(member.node),
flags: member.flags,
};
};
const getImportsAndExports = (sourceFile, resolveModule, typeChecker, options, ignoreExportsUsedInFile, skipExportsForFile, pluginVisitors) => {
const skipExports = skipExportsForFile || !options.isReportExports;
const internal = new Map();
const external = new Set();
const unresolved = new Set();
const programFiles = new Set();
const entryFiles = new Set();
const imports = new Set();
const exports = new Map();
const aliasedExports = new Map();
const scripts = new Set();
const importedInternalSymbols = new Map();
const importAliases = new Map();
const addImportAlias = (aliasName, id, filePath) => {
const aliases = importAliases.get(aliasName);
if (aliases)
aliases.add({ id, filePath });
else
importAliases.set(aliasName, new Set([{ id, filePath }]));
};
const referencedInExport = new Map();
const visitors = getVisitors(sourceFile, pluginVisitors);
const addNsMemberRefs = (internalImport, namespace, member) => {
if (typeof member === 'string') {
internalImport.refs.add(`${namespace}.${member}`);
}
else {
for (const m of member) {
internalImport.refs.add(`${namespace}.${m}`);
}
}
};
const maybeAddAliasedExport = (node, alias) => {
const identifier = node?.getText();
if (node && identifier) {
const symbol = sourceFile.symbol?.exports?.get(identifier);
if (symbol?.valueDeclaration) {
if (!aliasedExports.has(identifier)) {
const pos = getLineAndCharacterOfPosition(symbol.valueDeclaration, symbol.valueDeclaration.pos);
aliasedExports.set(identifier, [{ symbol: identifier, ...pos }]);
}
const aliasedExport = aliasedExports.get(identifier);
if (aliasedExport) {
const pos = getLineAndCharacterOfPosition(node, node.pos);
aliasedExport.push({ symbol: alias, ...pos });
}
}
}
};
const addInternalImport = (options) => {
const { symbol, filePath, namespace, specifier, modifiers } = options;
const identifier = options.identifier ?? (modifiers & IMPORT_FLAGS.OPAQUE ? OPAQUE : SIDE_EFFECTS);
const isStar = identifier === IMPORT_STAR;
imports.add({
filePath,
specifier,
identifier: namespace ?? options.identifier,
pos: options.pos,
line: options.line,
col: options.col,
isTypeOnly: !!(modifiers & IMPORT_FLAGS.TYPE_ONLY),
});
const file = internal.get(filePath);
const importMaps = file ?? createImports();
if (!file)
internal.set(filePath, importMaps);
const nsOrAlias = symbol ? String(symbol.escapedName) : options.alias;
if (modifiers & IMPORT_FLAGS.RE_EXPORT) {
if (isStar && namespace) {
addValue(importMaps.reExportNs, namespace, sourceFile.fileName);
}
else if (nsOrAlias) {
addNsValue(importMaps.reExportAs, identifier, nsOrAlias, sourceFile.fileName);
}
else {
addValue(importMaps.reExport, identifier, sourceFile.fileName);
}
}
else {
if (nsOrAlias && nsOrAlias !== identifier) {
if (isStar) {
addValue(importMaps.importNs, nsOrAlias, sourceFile.fileName);
}
else {
addNsValue(importMaps.importAs, identifier, nsOrAlias, sourceFile.fileName);
}
}
else if (identifier !== IMPORT_STAR) {
addValue(importMaps.import, identifier, sourceFile.fileName);
}
if (symbol)
importedInternalSymbols.set(symbol, filePath);
}
};
const addImport = (opts, node) => {
if (isBuiltin(opts.specifier))
return;
const module = resolveModule(opts.specifier);
if (module) {
const filePath = module.resolvedFileName;
if (filePath) {
if (!isInNodeModules(filePath)) {
if (opts.modifiers & IMPORT_FLAGS.ENTRY)
entryFiles.add(filePath);
if (opts.modifiers & IMPORT_FLAGS.BRIDGE)
programFiles.add(filePath);
}
if (!module.isExternalLibraryImport || !isInNodeModules(filePath)) {
const { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(opts.pos);
addInternalImport({
...opts,
filePath,
line: line + 1,
col: character + 1,
});
}
if (module.isExternalLibraryImport) {
if (options.skipTypeOnly && opts.modifiers & IMPORT_FLAGS.TYPE_ONLY)
return;
const sanitizedSpecifier = sanitizeSpecifier(isInNodeModules(filePath) || isInNodeModules(opts.specifier)
? getPackageNameFromFilePath(opts.specifier)
: opts.specifier);
if (!isStartsLikePackageName(sanitizedSpecifier)) {
return;
}
const pos = node.moduleSpecifier?.getStart() ?? opts.pos;
const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
external.add({
filePath,
specifier: sanitizedSpecifier,
identifier: opts.identifier ?? SIDE_EFFECTS,
pos,
line: line + 1,
col: character + 2,
isTypeOnly: !!(opts.modifiers & IMPORT_FLAGS.TYPE_ONLY),
});
}
}
}
else {
if (options.skipTypeOnly && opts.modifiers & IMPORT_FLAGS.TYPE_ONLY)
return;
if (shouldIgnore(getJSDocTags(node), options.tags))
return;
if (opts.specifier.startsWith(PROTOCOL_VIRTUAL))
return;
if (opts.modifiers && opts.modifiers & IMPORT_FLAGS.OPTIONAL) {
programFiles.add(resolve(dirname(sourceFile.fileName), opts.specifier));
return;
}
const pos = node.moduleSpecifier?.getStart() ?? node.pos;
const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
unresolved.add({
filePath: undefined,
specifier: opts.specifier,
identifier: opts.identifier ?? SIDE_EFFECTS,
pos,
line: line + 1,
col: character + 2,
isTypeOnly: !!(opts.modifiers & IMPORT_FLAGS.TYPE_ONLY),
});
}
};
const addExport = ({ node, symbol, identifier, type, pos, members, fix }) => {
let isReExport = Boolean(node.parent?.parent && ts.isExportDeclaration(node.parent.parent) && node.parent.parent.moduleSpecifier);
if (symbol) {
const importedSymbolFilePath = importedInternalSymbols.get(symbol);
if (importedSymbolFilePath) {
isReExport = true;
const importId = String(symbol.escapedName);
const internalImport = internal.get(importedSymbolFilePath);
if (internalImport) {
if (importId !== identifier) {
addNsValue(internalImport.reExportAs, importId, identifier, sourceFile.fileName);
}
else if (symbol.declarations && ts.isNamespaceImport(symbol.declarations[0])) {
addValue(internalImport.reExportNs, identifier, sourceFile.fileName);
}
else {
addValue(internalImport.reExport, importId, sourceFile.fileName);
}
}
}
}
const jsDocTags = getJSDocTags(node);
const exportMembers = members.map(member => createMember(node, member, member.pos));
const item = exports.get(identifier);
if (item) {
const members = [...item.members, ...exportMembers];
const tags = new Set([...item.jsDocTags, ...jsDocTags]);
const fixes = fix ? [...item.fixes, fix] : item.fixes;
exports.set(identifier, { ...item, members, jsDocTags: tags, fixes, isReExport });
}
else {
const { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(pos);
exports.set(identifier, {
identifier,
symbol: symbol ?? node.symbol,
type,
members: exportMembers,
jsDocTags,
pos,
line: line + 1,
col: character + 1,
hasRefsInFile: false,
referencedIn: undefined,
fixes: fix ? [fix] : [],
isReExport,
});
}
if (!jsDocTags.has(ALIAS_TAG)) {
if (ts.isExportAssignment(node))
maybeAddAliasedExport(node.expression, 'default');
if (ts.isVariableDeclaration(node))
maybeAddAliasedExport(node.initializer, identifier);
}
};
const addScript = (script) => scripts.add(script);
const getImport = (id, node) => {
const local = sourceFile.locals?.get(id);
const symbol = node.symbol ?? node.parent.symbol ?? local;
const filePath = importedInternalSymbols.get(symbol) ?? (local && importedInternalSymbols.get(local));
return { symbol, filePath };
};
const visit = (node) => {
const addImportWithNode = (result) => addImport(result, node);
const isTopLevel = node !== sourceFile && isInTopLevelScope(node, sourceFile);
if (isTopLevel) {
for (const visitor of visitors.import) {
const result = visitor(node, options);
result && (Array.isArray(result) ? result.forEach(addImportWithNode) : addImportWithNode(result));
}
if (!skipExports) {
for (const visitor of visitors.export) {
const result = visitor(node, options);
result && (Array.isArray(result) ? result.forEach(addExport) : addExport(result));
}
}
if (ts.isImportEqualsDeclaration(node) &&
ts.isQualifiedName(node.moduleReference) &&
ts.isIdentifier(node.moduleReference.left)) {
const { left, right } = node.moduleReference;
const namespace = left.text;
const { filePath } = getImport(namespace, node);
if (filePath) {
const internalImport = internal.get(filePath);
if (internalImport)
addNsMemberRefs(internalImport, namespace, right.text);
}
}
}
for (const visitor of visitors.dynamicImport) {
const result = visitor(node, options);
result && (Array.isArray(result) ? result.forEach(addImportWithNode) : addImportWithNode(result));
}
for (const visitor of visitors.script) {
const result = visitor(node, options);
result && (Array.isArray(result) ? result.forEach(addScript) : addScript(result));
}
if (ts.isIdentifier(node)) {
const id = String(node.escapedText);
const { symbol, filePath } = getImport(id, node);
if (importAliases.has(id) && isAccessExpression(node.parent)) {
const members = getAccessMembers(typeChecker, node);
for (const { id: aliasedId, filePath: aliasFilePath } of importAliases.get(id)) {
const aliasImports = internal.get(aliasFilePath);
if (aliasImports)
addNsMemberRefs(aliasImports, aliasedId, members);
}
}
if (symbol) {
if (filePath) {
if (!isImportSpecifier(node)) {
const imports = internal.get(filePath);
if (imports) {
const isPropName = ts.isPropertyAccessExpression(node.parent) && node.parent.name === node;
if (isPropName && isAccessExpression(node.parent.parent)) {
const members = getAccessMembers(typeChecker, node.parent);
addNsMemberRefs(imports, id, members);
}
else if (isAccessExpression(node.parent) && !isPropName) {
if (isDestructuring(node.parent)) {
if (ts.isPropertyAccessExpression(node.parent)) {
const ns = String(symbol.escapedName);
const key = String(node.parent.name.escapedText);
const [members, hasSpread] = getDestructuredNames(node.parent.parent.name);
if (hasSpread)
imports.refs.add(id);
else {
const ids = members.map(id => `${key}.${id}`);
addNsMemberRefs(imports, ns, key);
addNsMemberRefs(imports, ns, ids);
}
}
}
else {
const members = getAccessMembers(typeChecker, node);
addNsMemberRefs(imports, id, members);
}
}
else if (isDestructuring(node)) {
const [members, hasSpread] = getDestructuredNames(node.parent.name);
if (hasSpread)
imports.refs.add(id);
else
addNsMemberRefs(imports, id, members);
}
else if (ts.isSpreadAssignment(node.parent)) {
const path = [];
let _node = node.parent;
while (_node && !ts.isVariableDeclaration(_node)) {
if (ts.isPropertyAssignment(_node) && ts.isIdentifier(_node.name))
path.unshift(_node.name.text);
_node = _node.parent;
}
if (_node && ts.isIdentifier(_node.name)) {
const varName = _node.name.text;
if (exports.has(varName)) {
addNsValue(imports.reExportAs, id, [varName, ...path].join('.'), sourceFile.fileName);
}
else if (path.length === 0) {
addImportAlias(varName, id, filePath);
}
}
imports.refs.add(id);
}
else {
const typeRef = getTypeRef(node);
if (typeRef) {
if (ts.isQualifiedName(typeRef.typeName)) {
const typeName = typeRef.typeName;
const [ns, ...right] = [typeName.left.getText(), typeName.right.getText()].join('.').split('.');
const members = right.map((_r, index) => right.slice(0, index + 1).join('.'));
addNsMemberRefs(imports, ns, members);
}
else {
imports.refs.add(id);
}
}
else if (ts.isVariableDeclaration(node.parent) &&
node.parent.initializer === node &&
ts.isIdentifier(node.parent.name)) {
const aliasName = node.parent.name.text;
if (exports.has(aliasName)) {
addNsValue(imports.reExportAs, id, aliasName, sourceFile.fileName);
}
else {
addImportAlias(aliasName, id, filePath);
imports.refs.add(id);
}
}
else if (ts.isConditionalExpression(node.parent) || ts.isBinaryExpression(node.parent)) {
let _node = node.parent;
while (_node && !ts.isVariableDeclaration(_node))
_node = _node.parent;
if (_node && ts.isIdentifier(_node.name))
addImportAlias(_node.name.text, id, filePath);
imports.refs.add(id);
}
else if (ts.isShorthandPropertyAssignment(node.parent)) {
let _node = node.parent;
while (_node && !ts.isVariableDeclaration(_node))
_node = _node.parent;
if (_node && ts.isIdentifier(_node.name))
addImportAlias(`${_node.name.text}.${id}`, id, filePath);
imports.refs.add(id);
}
else if (imports.importNs.has(id) && isConsiderReferencedNS(node)) {
imports.refs.add(id);
}
else if (isObjectEnumerationCallExpressionArgument(node)) {
imports.refs.add(id);
}
else if (isInForIteration(node)) {
imports.refs.add(id);
}
else if (isKeyofTypeof(node)) {
imports.refs.add(id);
}
}
}
}
}
}
if (ts.isTypeReferenceNode(node.parent) || ts.isTypeQueryNode(node.parent)) {
const containingExport = isReferencedInExport(node);
if (containingExport && containingExport !== id) {
const refId = symbol?.exportSymbol ? String(symbol.exportSymbol.escapedName) : id;
const refs = referencedInExport.get(refId);
if (refs)
refs.add(containingExport);
else
referencedInExport.set(refId, new Set([containingExport]));
}
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
const pragmaImports = getImportsFromPragmas(sourceFile);
if (pragmaImports)
for (const node of pragmaImports)
addImport(node, sourceFile);
for (const [id, item] of exports) {
item.referencedIn = referencedInExport.get(id);
if (shouldCountRefs(ignoreExportsUsedInFile, item.type) ||
(item.symbol?.valueDeclaration && ts.isBindingElement(item.symbol.valueDeclaration))) {
item.hasRefsInFile = _hasRefsInFile(item, sourceFile, typeChecker);
}
for (const member of item.members) {
if (item.type === 'enum' || shouldCountRefs(ignoreExportsUsedInFile, member.type)) {
member.hasRefsInFile = _hasRefsInFile(member, sourceFile, typeChecker);
}
delete member.symbol;
}
delete item.symbol;
}
return {
imports: { internal, external, externalRefs: new Set(), programFiles, entryFiles, imports, unresolved },
exports,
duplicates: [...aliasedExports.values()],
scripts,
importedBy: undefined,
internalImportCache: undefined,
};
};
export const _getImportsAndExports = timerify(getImportsAndExports);