knip
Version:
Find unused files, dependencies and exports in your TypeScript and JavaScript projects
341 lines (340 loc) • 16.1 kB
JavaScript
import { isBuiltin } from 'node:module';
import ts from 'typescript';
import { ALIAS_TAG, ANONYMOUS, IMPORT_STAR, PROTOCOL_VIRTUAL } from '../constants.js';
import { timerify } from '../util/Performance.js';
import { addNsValue, addValue, createImports } from '../util/dependency-graph.js';
import { getPackageNameFromFilePath, isStartsLikePackageName, sanitizeSpecifier } from '../util/modules.js';
import { isInNodeModules } from '../util/path.js';
import { shouldIgnore } from '../util/tag.js';
import { getAccessMembers, getDestructuredIds, getJSDocTags, getLineAndCharacterOfPosition, getTypeName, isAccessExpression, isConsiderReferencedNS, isDestructuring, isImportSpecifier, isInForIteration, isObjectEnumerationCallExpressionArgument, isReferencedInExport, } from './ast-helpers.js';
import { findInternalReferences, isType } from './find-internal-references.js';
import getDynamicImportVisitors from './visitors/dynamic-imports/index.js';
import getExportVisitors from './visitors/exports/index.js';
import { getImportsFromPragmas } from './visitors/helpers.js';
import getImportVisitors from './visitors/imports/index.js';
import getScriptVisitors from './visitors/scripts/index.js';
const getVisitors = (sourceFile) => ({
export: getExportVisitors(sourceFile),
import: getImportVisitors(sourceFile),
dynamicImport: getDynamicImportVisitors(sourceFile),
script: getScriptVisitors(sourceFile),
});
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,
refs: [0, false],
jsDocTags: getJSDocTags(member.node),
};
};
const getImportsAndExports = (sourceFile, resolveModule, typeChecker, options) => {
const { skipTypeOnly, tags, ignoreExportsUsedInFile } = options;
const internal = new Map();
const external = new Set();
const unresolved = new Set();
const resolved = new Set();
const specifiers = new Set();
const exports = new Map();
const aliasedExports = new Map();
const scripts = new Set();
const traceRefs = new Set();
const importedInternalSymbols = new Map();
const referencedSymbolsInExport = new Set();
const visitors = getVisitors(sourceFile);
const addNsMemberRefs = (internalImport, namespace, member) => {
if (typeof member === 'string') {
internalImport.refs.add(`${namespace}.${member}`);
traceRefs.add(`${namespace}.${member}`);
}
else {
for (const m of member) {
internalImport.refs.add(`${namespace}.${m}`);
traceRefs.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 { identifier, symbol, filePath, namespace, alias, specifier, isReExport } = options;
const isStar = identifier === IMPORT_STAR;
specifiers.add([specifier, filePath]);
const file = internal.get(filePath);
const imports = file ?? createImports();
if (!file)
internal.set(filePath, imports);
const nsOrAlias = symbol ? String(symbol.escapedName) : alias;
if (isReExport) {
if (isStar && namespace) {
addValue(imports.reExportedNs, namespace, sourceFile.fileName);
}
else if (nsOrAlias) {
addNsValue(imports.reExportedAs, identifier, nsOrAlias, sourceFile.fileName);
}
else {
addValue(imports.reExported, identifier, sourceFile.fileName);
}
}
else {
if (nsOrAlias && nsOrAlias !== identifier) {
if (isStar) {
addValue(imports.importedNs, nsOrAlias, sourceFile.fileName);
}
else {
addNsValue(imports.importedAs, identifier, nsOrAlias, sourceFile.fileName);
}
}
else if (identifier !== ANONYMOUS && identifier !== IMPORT_STAR) {
addValue(imports.imported, identifier, sourceFile.fileName);
}
if (symbol)
importedInternalSymbols.set(symbol, filePath);
}
};
const addImport = (options, node) => {
const { specifier, isTypeOnly, pos, identifier = ANONYMOUS, isReExport = false } = options;
if (isBuiltin(specifier))
return;
const module = resolveModule(specifier);
if (module) {
const filePath = module.resolvedFileName;
if (filePath) {
if (options.resolve && !isInNodeModules(filePath)) {
resolved.add(filePath);
return;
}
if (!module.isExternalLibraryImport || !isInNodeModules(filePath)) {
addInternalImport({ ...options, identifier, filePath, isReExport });
}
if (module.isExternalLibraryImport) {
if (skipTypeOnly && isTypeOnly)
return;
const sanitizedSpecifier = sanitizeSpecifier(isInNodeModules(specifier) || isInNodeModules(filePath) ? getPackageNameFromFilePath(specifier) : specifier);
if (!isStartsLikePackageName(sanitizedSpecifier)) {
return;
}
external.add(sanitizedSpecifier);
}
}
}
else {
if (skipTypeOnly && isTypeOnly)
return;
if (shouldIgnore(getJSDocTags(node), tags))
return;
if (specifier.startsWith(PROTOCOL_VIRTUAL))
return;
if (typeof pos === 'number') {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
unresolved.add({ specifier, pos, line: line + 1, col: character + 1 });
}
else {
unresolved.add({ specifier });
}
}
};
const addExport = ({ node, symbol, identifier, type, pos, members = [], fix }) => {
if (options.skipExports)
return;
if (symbol) {
const importedSymbolFilePath = importedInternalSymbols.get(symbol);
if (importedSymbolFilePath) {
const importId = String(symbol.escapedName);
const internalImport = internal.get(importedSymbolFilePath);
if (internalImport) {
if (importId !== identifier) {
addNsValue(internalImport.reExportedAs, importId, identifier, sourceFile.fileName);
}
else if (symbol.declarations && ts.isNamespaceImport(symbol.declarations[0])) {
addValue(internalImport.reExportedNs, identifier, sourceFile.fileName);
}
else {
addValue(internalImport.reExported, importId, sourceFile.fileName);
}
}
}
}
const jsDocTags = getJSDocTags(node);
const exportMembers = members.map(member => createMember(node, member, member.pos));
const isReExport = Boolean(node.parent?.parent && ts.isExportDeclaration(node.parent.parent) && node.parent.parent.moduleSpecifier);
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: node.symbol,
type,
members: exportMembers,
jsDocTags,
pos,
line: line + 1,
col: character + 1,
fixes: fix ? [fix] : [],
refs: [0, false],
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 && ts.isInTopLevelContext(node);
if (isTopLevel) {
for (const visitor of visitors.import) {
const result = visitor(node, options);
result && (Array.isArray(result) ? result.forEach(addImportWithNode) : addImportWithNode(result));
}
for (const visitor of visitors.export) {
const result = visitor(node, options);
result && (Array.isArray(result) ? result.forEach(addExport) : addExport(result));
}
}
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 (symbol) {
if (filePath) {
if (!isImportSpecifier(node)) {
const imports = internal.get(filePath);
if (imports) {
traceRefs.add(id);
if (isAccessExpression(node.parent)) {
if (isDestructuring(node.parent)) {
if (ts.isPropertyAccessExpression(node.parent)) {
const ns = String(symbol.escapedName);
const key = String(node.parent.name.escapedText);
const members = getDestructuredIds(node.parent.parent.name).map(n => `${key}.${n}`);
addNsMemberRefs(imports, ns, key);
addNsMemberRefs(imports, ns, members);
}
}
else {
const members = getAccessMembers(typeChecker, node);
addNsMemberRefs(imports, id, members);
}
}
else if (isDestructuring(node)) {
const members = getDestructuredIds(node.parent.name);
addNsMemberRefs(imports, id, members);
}
else {
const typeName = getTypeName(node);
if (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 if (imports.importedNs.has(id) && isConsiderReferencedNS(node)) {
imports.refs.add(id);
}
else if (isObjectEnumerationCallExpressionArgument(node)) {
imports.refs.add(id);
}
else if (isInForIteration(node)) {
imports.refs.add(id);
}
}
}
}
}
if (!isTopLevel && symbol.exportSymbol && isReferencedInExport(node)) {
referencedSymbolsInExport.add(symbol.exportSymbol);
}
}
}
if (isTopLevel &&
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);
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
const pragmaImports = getImportsFromPragmas(sourceFile);
if (pragmaImports)
for (const node of pragmaImports)
addImport(node, sourceFile);
for (const item of exports.values()) {
if (!isType(item) && item.symbol && referencedSymbolsInExport.has(item.symbol)) {
item.refs = [1, true];
}
else {
const isBindingElement = item.symbol?.valueDeclaration && ts.isBindingElement(item.symbol.valueDeclaration);
if (ignoreExportsUsedInFile === true ||
(typeof ignoreExportsUsedInFile === 'object' &&
item.type !== 'unknown' &&
ignoreExportsUsedInFile[item.type]) ||
isBindingElement) {
item.refs = findInternalReferences(item, sourceFile, typeChecker, referencedSymbolsInExport, isBindingElement);
}
}
for (const member of item.members) {
member.refs = findInternalReferences(member, sourceFile, typeChecker, referencedSymbolsInExport);
member.symbol = undefined;
}
item.symbol = undefined;
}
return {
imports: { internal, external, resolved, specifiers, unresolved },
exports,
duplicates: [...aliasedExports.values()],
scripts,
traceRefs,
};
};
export const _getImportsAndExports = timerify(getImportsAndExports);