UNPKG

knip

Version:

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

475 lines (474 loc) 23.9 kB
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);