UNPKG

knip

Version:

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

306 lines (305 loc) 14.2 kB
import { isBuiltin } from 'node:module'; import { 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 { extractImportsFromComments } from './comments.js'; import { _parseFile, buildLineStarts, getLineAndCol, shouldCountRefs, } from './ast-nodes.js'; import { buildJSDocTagLookup } from './visitors/jsdoc.js'; import { _walkAST } from './visitors/walk.js'; const getImportsAndExports = (filePath, sourceText, resolveModule, options, ignoreExportsUsedInFile, skipExportsForFile, visitor, pluginCtx, cachedParseResult) => { const skipExports = skipExportsForFile || !options.isReportExports; const isDts = filePath.endsWith('.d.ts') || filePath.endsWith('.d.cts') || filePath.endsWith('.d.mts'); 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 specifierExportNames = new Set(); const scripts = new Set(); const importAliases = new Map(); const addImportAlias = (aliasName, id, importFilePath) => { const aliases = importAliases.get(aliasName); if (aliases) aliases.add({ id, filePath: importFilePath }); else importAliases.set(aliasName, new Set([{ id, filePath: importFilePath }])); }; const localImportMap = new Map(); const localDeclarationTypes = new Map(); const referencedInExport = new Map(); const destructuredExports = new Set(); 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 addInternalImport = (opts) => { const { filePath: importFilePath, namespace, specifier, modifiers } = opts; const identifier = opts.identifier ?? (modifiers & IMPORT_FLAGS.OPAQUE ? OPAQUE : SIDE_EFFECTS); const isStar = identifier === IMPORT_STAR; imports.add({ filePath: importFilePath, specifier, identifier: namespace ?? opts.identifier, pos: opts.pos, line: opts.line, col: opts.col, isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY), }); const file = internal.get(importFilePath); const importMaps = file ?? createImports(); if (!file) internal.set(importFilePath, importMaps); const nsOrAlias = opts.alias; if (modifiers & IMPORT_FLAGS.RE_EXPORT) { if (isStar && namespace) { addValue(importMaps.reExportNs, namespace, filePath); } else if (nsOrAlias) { addNsValue(importMaps.reExportAs, identifier, nsOrAlias, filePath); } else { addValue(importMaps.reExport, identifier, filePath); } } else { if (nsOrAlias && nsOrAlias !== identifier) { if (isStar) { addValue(importMaps.importNs, nsOrAlias, filePath); } else { addNsValue(importMaps.importAs, identifier, nsOrAlias, filePath); } } else if (identifier !== IMPORT_STAR) { addValue(importMaps.import, identifier, filePath); } } }; const addImport = (specifier, identifier, alias, namespace, pos, modifiers, specifierPos, jsDocTags, preResolvedModule) => { if (!specifier || isBuiltin(specifier)) return; const module = preResolvedModule ?? resolveModule(specifier, filePath); if (module) { const resolvedFileName = module.resolvedFileName; if (resolvedFileName) { if (!isInNodeModules(resolvedFileName)) { if (modifiers & IMPORT_FLAGS.ENTRY) entryFiles.add(resolvedFileName); if (modifiers & IMPORT_FLAGS.BRIDGE) programFiles.add(resolvedFileName); } if (!module.isExternalLibraryImport || !isInNodeModules(resolvedFileName)) { const { line, col } = getLineAndCol(lineStarts, pos); addInternalImport({ identifier, alias, namespace, filePath: resolvedFileName, specifier, pos, line, col, modifiers, }); } if (module.isExternalLibraryImport) { if (options.skipTypeOnly && modifiers & IMPORT_FLAGS.TYPE_ONLY) return; const sanitizedSpecifier = sanitizeSpecifier(isInNodeModules(resolvedFileName) || isInNodeModules(specifier) ? getPackageNameFromFilePath(specifier) : specifier); if (!isStartsLikePackageName(sanitizedSpecifier)) return; const ePos = specifierPos ?? pos; const { line, col } = getLineAndCol(lineStarts, ePos); external.add({ filePath: resolvedFileName, specifier: sanitizedSpecifier, identifier: identifier ?? SIDE_EFFECTS, pos: ePos, line, col, isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY), }); } } } else { if (options.skipTypeOnly && modifiers & IMPORT_FLAGS.TYPE_ONLY) return; if (specifier.startsWith(PROTOCOL_VIRTUAL)) return; if (modifiers && modifiers & IMPORT_FLAGS.OPTIONAL) { programFiles.add(resolve(dirname(filePath), specifier)); return; } const uPos = specifierPos ?? pos; const { line, col } = getLineAndCol(lineStarts, uPos); if (!(jsDocTags?.size && shouldIgnore(jsDocTags, options.tags))) { unresolved.add({ filePath: undefined, specifier, identifier: identifier ?? SIDE_EFFECTS, pos: uPos, line, col, isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY), }); } } }; const result = cachedParseResult ?? _parseFile(filePath, sourceText); const lineStarts = buildLineStarts(sourceText); const getJSDocTags = buildJSDocTagLookup(result.comments, sourceText); let hasNodeModuleImport = false; let hasWorkerThreadsImport = false; let hasChildProcessImport = false; let hasPathJoinImport = false; let hasPathResolveImport = false; for (const _imports of result.module.staticImports) { const specifier = _imports.moduleRequest.value; if (specifier === 'node:module' || specifier === 'module') hasNodeModuleImport = true; else if (specifier === 'node:worker_threads' || specifier === 'worker_threads') hasWorkerThreadsImport = true; else if (specifier === 'node:child_process' || specifier === 'child_process') hasChildProcessImport = true; const isPathImport = specifier === 'node:path' || specifier === 'path'; const pos = _imports.moduleRequest.start; const jsdocTags = getJSDocTags(_imports.start); if (_imports.entries.length === 0) { addImport(specifier, undefined, undefined, undefined, pos, IMPORT_FLAGS.SIDE_EFFECTS, undefined, jsdocTags); continue; } const resolved = resolveModule(specifier, filePath); const internalPath = resolved && !resolved.isExternalLibraryImport && !isInNodeModules(resolved.resolvedFileName) ? resolved.resolvedFileName : undefined; for (const entry of _imports.entries) { const modifiers = entry.isType ? IMPORT_FLAGS.TYPE_ONLY : IMPORT_FLAGS.NONE; if (entry.importName.kind === 'NamespaceObject') { const localName = entry.localName.value; addImport(specifier, IMPORT_STAR, localName, undefined, entry.localName.start, modifiers, pos, jsdocTags, resolved); if (internalPath) localImportMap.set(localName, { importedName: IMPORT_STAR, filePath: internalPath, isNamespace: true }); } else if (entry.importName.kind === 'Default') { const localName = entry.localName.value; const alias = localName !== 'default' ? localName : undefined; addImport(specifier, 'default', alias, undefined, entry.localName.start, modifiers, pos, jsdocTags, resolved); if (internalPath) localImportMap.set(localName, { importedName: 'default', filePath: internalPath, isNamespace: false }); } else { const importedName = entry.importName.name; const localName = entry.localName.value; const alias = localName !== importedName ? localName : undefined; if (isPathImport && !alias) { if (importedName === 'join') hasPathJoinImport = true; else if (importedName === 'resolve') hasPathResolveImport = true; } addImport(specifier, importedName, alias, undefined, entry.localName.start, modifiers, pos, jsdocTags, resolved); if (internalPath) localImportMap.set(localName, { importedName, filePath: internalPath, isNamespace: false }); } } } for (const se of result.module.staticExports) { const jsdocTags = getJSDocTags(se.start); let reExportResolved; let reExportSpecifier; for (const entry of se.entries) { if (entry.moduleRequest) { const specifier = entry.moduleRequest.value; const modifiers = IMPORT_FLAGS.RE_EXPORT | (entry.isType ? IMPORT_FLAGS.TYPE_ONLY : IMPORT_FLAGS.NONE); const pos = entry.moduleRequest.start; if (specifier !== reExportSpecifier) { reExportSpecifier = specifier; reExportResolved = resolveModule(specifier, filePath); } if (entry.importName.kind === 'AllButDefault') { addImport(specifier, IMPORT_STAR, undefined, undefined, pos, modifiers, undefined, jsdocTags, reExportResolved); } else if (entry.importName.kind === 'All') { const ns = entry.exportName.name; addImport(specifier, IMPORT_STAR, undefined, ns, entry.start, modifiers, pos, jsdocTags, reExportResolved); } else if (entry.importName.kind === 'Name') { const importedName = entry.importName.name; const exportedName = entry.exportName.name; const alias = exportedName && exportedName !== importedName ? exportedName : undefined; addImport(specifier, importedName, alias, undefined, entry.start, modifiers, pos, undefined, reExportResolved); } continue; } if (skipExports) continue; } } if (pluginCtx) { pluginCtx.filePath = filePath; pluginCtx.sourceText = sourceText; pluginCtx.addScript = (s) => scripts.add(s); pluginCtx.addImport = (spec, pos, mod) => addImport(spec, undefined, undefined, undefined, pos, mod); } const localRefs = _walkAST(result.program, sourceText, filePath, { lineStarts, skipExports, options, exports, aliasedExports, specifierExportNames, scripts, addImport, addNsMemberRefs, addImportAlias, internal, localImportMap, localDeclarationTypes, importAliases, referencedInExport, skipBareExprRefs: !!ignoreExportsUsedInFile, localRefs: ignoreExportsUsedInFile ? new Set() : undefined, destructuredExports, hasNodeModuleImport, hasWorkerThreadsImport, hasChildProcessImport, hasPathJoinImport, hasPathResolveImport, resolveModule, programFiles, entryFiles, visitor, getJSDocTags, }); const firstStmtStart = result.program.body[0]?.start ?? Number.POSITIVE_INFINITY; extractImportsFromComments(result.comments, firstStmtStart, addImport); for (const [id, item] of exports) { item.referencedIn = referencedInExport.get(id); if (localRefs && shouldCountRefs(ignoreExportsUsedInFile, item.type) && (localRefs.has(id) || item.isReExport)) { item.hasRefsInFile = true; } } 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);