UNPKG

knip

Version:

Find unused files, dependencies and exports in your TypeScript and JavaScript projects

341 lines (340 loc) 16.1 kB
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);