UNPKG

knip

Version:

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

709 lines (708 loc) 26.4 kB
import { Visitor } from 'oxc-parser'; import { FIX_FLAGS, IMPORT_FLAGS, OPAQUE, SYMBOL_TYPE } from "../../constants.js"; import { addValue } from "../../util/module-graph.js"; import { isInNodeModules } from "../../util/path.js"; import { getLineAndCol, getStringValue, isStringLiteral } from "./helpers.js"; import { EMPTY_TAGS } from "./jsdoc.js"; import { handleCallExpression, handleNewExpression } from "./calls.js"; import { handleExportAssignment, handleExportDefault, handleExportNamed, handleExpressionStatement, } from "./exports.js"; import { handleImportExpression, handleVariableDeclarator } from "./imports.js"; import { handleJSXMemberExpression, handleMemberExpression } from "./members.js"; let state; const _getFix = (start, end, flags) => state.options.isFixExports ? [start, end, flags ?? FIX_FLAGS.NONE] : undefined; const _getTypeFix = (start, end) => state.options.isFixTypes ? [start, end, FIX_FLAGS.NONE] : undefined; const _addExport = (identifier, type, pos, members, fix, isReExport, jsDocTags) => { const item = state.exports.get(identifier); if (item) { if (members.length) for (const m of members) item.members.push(m); if (fix) item.fixes.push(fix); if (jsDocTags.size) { if (item.jsDocTags === EMPTY_TAGS) { item.jsDocTags = new Set(jsDocTags); } else { for (const t of jsDocTags) item.jsDocTags.add(t); } } item.isReExport = isReExport; } else { const { line, col } = getLineAndCol(state.lineStarts, pos); state.exports.set(identifier, { identifier, type, members, jsDocTags, pos, line, col, hasRefsInFile: false, referencedIn: undefined, fixes: fix ? [fix] : [], isReExport, }); } }; const _collectRefsInType = (node, exportName, signatureOnly) => { if (!node || typeof node !== 'object') return; if (node.type === 'TSTypeQuery') { const name = node.exprName.type === 'Identifier' ? node.exprName.name : undefined; if (name) { const refs = state.referencedInExport.get(name); if (refs) refs.add(exportName); else state.referencedInExport.set(name, new Set([exportName])); } return; } if (signatureOnly && (node.type === 'FunctionBody' || node.type === 'BlockStatement')) return; if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') { const name = node.typeName.name; const refs = state.referencedInExport.get(name); if (refs) refs.add(exportName); else state.referencedInExport.set(name, new Set([exportName])); } for (const key in node) { if (key === 'type' || key === 'parent') continue; const val = node[key]; if (Array.isArray(val)) { for (const item of val) { if (item && typeof item === 'object' && item.type) _collectRefsInType(item, exportName, signatureOnly); } } else if (val && typeof val === 'object' && val.type) { _collectRefsInType(val, exportName, signatureOnly); } } }; const _addRefInExport = (name, exportName) => { const refs = state.referencedInExport.get(name); if (refs) refs.add(exportName); else state.referencedInExport.set(name, new Set([exportName])); }; const _isInNamespace = (node) => state.nsRanges.length > 0 && state.nsRanges.some(([start, end]) => node.start >= start && node.end <= end); export const isShadowed = (name, pos) => { if (state.shadowScopes.size === 0) return false; const ranges = state.shadowScopes.get(name); if (!ranges) return false; if (state.localImportMap.get(name)?.isDynamicImport) return false; for (const range of ranges) { if (pos >= range[0] && pos <= range[1]) return true; } return false; }; const _addLocalRef = (name, pos) => { if (!state.localImportMap.has(name) && !isShadowed(name, pos)) state.localRefs.add(name); }; const _addShadowRange = (name, range) => { const ranges = state.shadowScopes.get(name); if (ranges) ranges.push(range); else state.shadowScopes.set(name, [range]); }; const _addShadow = (name) => { const i = state.scopeDepth - 1; _addShadowRange(name, [state.scopeStarts[i], state.scopeEnds[i]]); }; const _collectBindingNames = (pattern, range) => { if (!pattern) return; if (pattern.type === 'Identifier') { _addShadowRange(pattern.name, range); } else if (pattern.type === 'ObjectPattern') { for (const prop of pattern.properties ?? []) { _collectBindingNames(prop.value ?? prop.argument, range); } } else if (pattern.type === 'ArrayPattern') { for (const el of pattern.elements ?? []) { _collectBindingNames(el, range); } } else if (pattern.type === 'AssignmentPattern') { _collectBindingNames(pattern.left, range); } else if (pattern.type === 'RestElement') { _collectBindingNames(pattern.argument, range); } }; const _addParamShadows = (params, body) => { if (!body || !params) return; const range = [body.start, body.end]; const items = Array.isArray(params) ? params : (params.items ?? params); for (const param of items) _collectBindingNames(param, range); }; const coreVisitorObject = { BlockStatement(node) { state.scopeStarts[state.scopeDepth] = node.start; state.scopeEnds[state.scopeDepth] = node.end; state.scopeDepth++; }, 'BlockStatement:exit'() { state.scopeDepth--; }, TSModuleDeclaration(node) { state.nsRanges.push([node.start, node.end]); }, ClassDeclaration(node) { if (node.id?.name) state.localDeclarationTypes.set(node.id.name, SYMBOL_TYPE.CLASS); }, FunctionDeclaration(node) { if (node.id?.name) { state.localDeclarationTypes.set(node.id.name, SYMBOL_TYPE.FUNCTION); if (state.scopeDepth > 0) _addShadow(node.id.name); } _addParamShadows(node.params, node.body); }, FunctionExpression(node) { _addParamShadows(node.params, node.body); }, ArrowFunctionExpression(node) { _addParamShadows(node.params, node.body); }, CatchClause(node) { if (node.param?.type === 'Identifier' && node.body) { _addShadowRange(node.param.name, [node.body.start, node.body.end]); } }, VariableDeclaration(node) { state.currentVarDeclStart = node.start; if (state.scopeDepth > 0) { for (const decl of node.declarations) { if (decl.id.type === 'Identifier') { state.localDeclarationTypes.set(decl.id.name, SYMBOL_TYPE.VARIABLE); _addShadow(decl.id.name); } } } else { for (const decl of node.declarations) { if (decl.id.type === 'Identifier') state.localDeclarationTypes.set(decl.id.name, SYMBOL_TYPE.VARIABLE); } } }, TSEnumDeclaration(node) { if (node.id.name) state.localDeclarationTypes.set(node.id.name, SYMBOL_TYPE.ENUM); }, ExportNamedDeclaration(node) { handleExportNamed(node, state); }, ExportDefaultDeclaration(node) { handleExportDefault(node, state); }, TSExportAssignment(node) { handleExportAssignment(node, state); }, ExpressionStatement(node) { handleExpressionStatement(node, state); }, VariableDeclarator(node) { handleVariableDeclarator(node, state); }, ImportExpression(node) { handleImportExpression(node, state); }, CallExpression(node) { handleCallExpression(node, state); }, NewExpression(node) { handleNewExpression(node, state); }, MemberExpression(node) { handleMemberExpression(node, state); }, JSXMemberExpression(node) { handleJSXMemberExpression(node, state); }, ForInStatement(node) { if (node.left.type === 'VariableDeclaration' && node.body) { for (const decl of node.left.declarations) _collectBindingNames(decl.id, [node.body.start, node.body.end]); } if (node.right.type === 'Identifier' && !isShadowed(node.right.name, node.right.start)) { const _import = state.localImportMap.get(node.right.name); if (_import?.isNamespace) { const internalImport = state.internal.get(_import.filePath); if (internalImport) addValue(internalImport.import, OPAQUE, state.filePath); } } }, ForOfStatement(node) { if (node.left.type === 'VariableDeclaration' && node.body) { for (const decl of node.left.declarations) _collectBindingNames(decl.id, [node.body.start, node.body.end]); } if (node.right.type === 'Identifier' && !isShadowed(node.right.name, node.right.start)) { const _import = state.localImportMap.get(node.right.name); if (_import?.isNamespace) { const internalImport = state.internal.get(_import.filePath); if (internalImport) addValue(internalImport.import, OPAQUE, state.filePath); } } }, TSQualifiedName(node) { let left = node; const parts = []; while (left.type === 'TSQualifiedName') { if (left.right.type === 'Identifier') parts.unshift(left.right.name); left = left.left; } if (left.type === 'Identifier' && !isShadowed(left.name, left.start)) { const rootName = left.name; const _import = state.localImportMap.get(rootName); if (_import) { const internalImport = state.internal.get(_import.filePath); if (internalImport) { if (parts.length > 0) { let path = ''; for (const part of parts) { path = path ? `${path}.${part}` : part; state.addNsMemberRefs(internalImport, rootName, path); } } else { internalImport.refs.add(rootName); } } } else if (parts.length > 0) { let path = ''; for (const part of parts) { path = path ? `${path}.${part}` : part; state.memberRefsInFile.push(rootName, path); } } } }, TSTypeReference(node) { if (node.typeName.type === 'Identifier') { const name = node.typeName.name; if (!isShadowed(name, node.typeName.start)) { const _import = state.localImportMap.get(name); if (_import) { const internalImport = state.internal.get(_import.filePath); if (internalImport) internalImport.refs.add(name); } } } }, TSTypeQuery(node) { if (node.exprName.type === 'Identifier') { const name = node.exprName.name; if (!isShadowed(name, node.exprName.start)) { const _import = state.localImportMap.get(name); if (_import) { const internalImport = state.internal.get(_import.filePath); if (internalImport) internalImport.refs.add(name); } } } }, TSImportType(node) { const src = node.source; if (isStringLiteral(src)) { const specifier = getStringValue(src); state.addImport(specifier, undefined, undefined, undefined, src.start, IMPORT_FLAGS.TYPE_ONLY); } }, TSImportEqualsDeclaration(node) { if (node.moduleReference.type === 'TSExternalModuleReference') { const expr = node.moduleReference.expression; if (isStringLiteral(expr)) { const specifier = getStringValue(expr); const localName = node.id.name; state.addImport(specifier, 'default', localName, undefined, node.id.start, IMPORT_FLAGS.NONE); if (localName) { const module = state.resolveModule(specifier, state.filePath); if (module && !module.isExternalLibraryImport && !isInNodeModules(module.resolvedFileName)) { state.localImportMap.set(localName, { importedName: 'default', filePath: module.resolvedFileName, isNamespace: false, }); } } } } else if (node.moduleReference.type === 'TSQualifiedName') { const left = node.moduleReference.left; const right = node.moduleReference.right; if (left.type === 'Identifier' && right.type === 'Identifier') { const nsName = left.name; const _import = state.localImportMap.get(nsName); if (_import?.isNamespace) { const internalImport = state.internal.get(_import.filePath); if (internalImport) state.addNsMemberRefs(internalImport, nsName, right.name); } } } }, }; const localRefsVisitorObject = { ClassDeclaration(node) { if (node.superClass?.type === 'Identifier') _addLocalRef(node.superClass.name, node.superClass.start); for (const impl of node.implements ?? []) { if (impl.expression?.type === 'Identifier') _addLocalRef(impl.expression.name, impl.expression.start); } }, TSInterfaceDeclaration(node) { for (const ext of node.extends ?? []) { if (ext.expression?.type === 'Identifier') _addLocalRef(ext.expression.name, ext.expression.start); } }, Property(node) { if (node.value?.type === 'Identifier') _addLocalRef(node.value.name, node.value.start); }, ReturnStatement(node) { if (node.argument?.type === 'Identifier') _addLocalRef(node.argument.name, node.argument.start); }, AssignmentExpression(node) { if (node.right?.type === 'Identifier') _addLocalRef(node.right.name, node.right.start); }, SpreadElement(node) { if (node.argument?.type === 'Identifier') _addLocalRef(node.argument.name, node.argument.start); }, ConditionalExpression(node) { if (node.test?.type === 'Identifier') _addLocalRef(node.test.name, node.test.start); if (node.consequent?.type === 'Identifier') _addLocalRef(node.consequent.name, node.consequent.start); if (node.alternate?.type === 'Identifier') _addLocalRef(node.alternate.name, node.alternate.start); }, ArrayExpression(node) { for (const el of node.elements ?? []) { if (el?.type === 'Identifier') _addLocalRef(el.name, el.start); } }, TemplateLiteral(node) { for (const expr of node.expressions ?? []) { if (expr.type === 'Identifier') _addLocalRef(expr.name, expr.start); } }, BinaryExpression(node) { if (node.left?.type === 'Identifier') _addLocalRef(node.left.name, node.left.start); if (node.right?.type === 'Identifier') _addLocalRef(node.right.name, node.right.start); }, LogicalExpression(node) { if (node.left?.type === 'Identifier') _addLocalRef(node.left.name, node.left.start); if (node.right?.type === 'Identifier') _addLocalRef(node.right.name, node.right.start); }, UnaryExpression(node) { if (node.argument?.type === 'Identifier') _addLocalRef(node.argument.name, node.argument.start); }, SwitchStatement(node) { if (node.discriminant?.type === 'Identifier') _addLocalRef(node.discriminant.name, node.discriminant.start); for (const c of node.cases ?? []) { if (c.test?.type === 'Identifier') _addLocalRef(c.test.name, c.test.start); } }, IfStatement(node) { if (node.test?.type === 'Identifier') _addLocalRef(node.test.name, node.test.start); }, ThrowStatement(node) { if (node.argument?.type === 'Identifier') _addLocalRef(node.argument.name, node.argument.start); }, WhileStatement(node) { if (node.test?.type === 'Identifier') _addLocalRef(node.test.name, node.test.start); }, DoWhileStatement(node) { if (node.test?.type === 'Identifier') _addLocalRef(node.test.name, node.test.start); }, YieldExpression(node) { if (node.argument?.type === 'Identifier') _addLocalRef(node.argument.name, node.argument.start); }, AwaitExpression(node) { if (node.argument?.type === 'Identifier') _addLocalRef(node.argument.name, node.argument.start); }, ArrowFunctionExpression(node) { if (node.body?.type === 'Identifier') _addLocalRef(node.body.name, node.body.start); }, AssignmentPattern(node) { if (node.right?.type === 'Identifier') _addLocalRef(node.right.name, node.right.start); }, SequenceExpression(node) { for (const expr of node.expressions ?? []) { if (expr.type === 'Identifier') _addLocalRef(expr.name, expr.start); } }, TSAsExpression(node) { if (node.expression?.type === 'Identifier') _addLocalRef(node.expression.name, node.expression.start); }, TSSatisfiesExpression(node) { if (node.expression?.type === 'Identifier') _addLocalRef(node.expression.name, node.expression.start); }, TSNonNullExpression(node) { if (node.expression?.type === 'Identifier') _addLocalRef(node.expression.name, node.expression.start); }, TSTypeAssertion(node) { if (node.expression?.type === 'Identifier') _addLocalRef(node.expression.name, node.expression.start); }, ParenthesizedExpression(node) { if (node.expression?.type === 'Identifier') _addLocalRef(node.expression.name, node.expression.start); }, PropertyDefinition(node) { if (node.value?.type === 'Identifier') _addLocalRef(node.value.name, node.value.start); }, ForInStatement(node) { if (node.right?.type === 'Identifier') _addLocalRef(node.right.name, node.right.start); }, ForOfStatement(node) { if (node.right?.type === 'Identifier') _addLocalRef(node.right.name, node.right.start); }, JSXOpeningElement(node) { if (node.name?.type === 'JSXIdentifier') _addLocalRef(node.name.name, node.name.start); for (const attr of node.attributes ?? []) { if (attr.type === 'JSXSpreadAttribute' && attr.argument?.type === 'Identifier') _addLocalRef(attr.argument.name, attr.argument.start); } }, JSXExpressionContainer(node) { if (node.expression?.type === 'Identifier') _addLocalRef(node.expression.name, node.expression.start); }, VariableDeclarator(node) { if (node.init?.type === 'Identifier') _addLocalRef(node.init.name, node.init.start); }, ExpressionStatement(node) { if (node.expression?.type === 'Identifier') _addLocalRef(node.expression.name, node.expression.start); }, CallExpression(node) { if (node.callee?.type === 'Identifier') _addLocalRef(node.callee.name, node.callee.start); for (const arg of node.arguments ?? []) { if (arg.type === 'Identifier') _addLocalRef(arg.name, arg.start); } }, NewExpression(node) { if (node.callee?.type === 'Identifier') _addLocalRef(node.callee.name, node.callee.start); for (const arg of node.arguments ?? []) { if (arg.type === 'Identifier') _addLocalRef(arg.name, arg.start); } }, MemberExpression(node) { if (node.object?.type === 'Identifier') _addLocalRef(node.object.name, node.object.start); if (node.computed && node.property?.type === 'Identifier') _addLocalRef(node.property.name, node.property.start); }, TaggedTemplateExpression(node) { if (node.tag?.type === 'Identifier') _addLocalRef(node.tag.name, node.tag.start); }, TSQualifiedName(node) { let left = node; const parts = []; while (left.type === 'TSQualifiedName') { if (left.right.type === 'Identifier') parts.unshift(left.right.name); left = left.left; } if (left.type === 'Identifier') { const rootName = left.name; if (!state.localImportMap.has(rootName) && !isShadowed(rootName, left.start) && parts.length > 0) { state.localRefs.add(rootName); } } }, TSTypeReference(node) { if (node.typeName?.type === 'Identifier') { const name = node.typeName.name; if (!state.localImportMap.has(name)) _addLocalRef(name, node.typeName.start); } }, TSTypeQuery(node) { if (node.exprName?.type === 'Identifier') { const name = node.exprName.name; if (!state.localImportMap.has(name)) _addLocalRef(name, node.exprName.start); } }, }; export function buildVisitor(pluginVisitorObjects, includeLocalRefs) { const handlerLists = new Map(); const coreHandlers = coreVisitorObject; for (const key in coreHandlers) { const fn = coreHandlers[key]; if (fn) handlerLists.set(key, [fn]); } const extras = includeLocalRefs ? [localRefsVisitorObject, ...pluginVisitorObjects] : pluginVisitorObjects; for (const obj of extras) { for (const key in obj) { const fn = obj[key]; if (!fn) continue; const list = handlerLists.get(key); if (list) list.push(fn); else handlerLists.set(key, [fn]); } } if (extras.length === 0) return new Visitor(coreVisitorObject); const merged = {}; for (const [key, list] of handlerLists) { if (list.length === 1) { merged[key] = list[0]; } else { const fns = list; merged[key] = ((node) => { for (let i = 0; i < fns.length; i++) fns[i](node); }); } } return new Visitor(merged); } export function walkAST(program, sourceText, filePath, ctx) { const isJS = filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs') || filePath.endsWith('.jsx'); state = { ...ctx, filePath, sourceText, isJS, handledImportExpressions: new Set(), bareExprRefs: new Set(), accessedAliases: new Set(), nsContainers: new Map(), accessedNsContainers: new Set(), chainedMemberExprs: new WeakSet(), currentVarDeclStart: -1, nsRanges: [], memberRefsInFile: [], scopeDepth: 0, scopeStarts: [], scopeEnds: [], shadowScopes: new Map(), addExport: _addExport, getFix: _getFix, getTypeFix: _getTypeFix, collectRefsInType: _collectRefsInType, addRefInExport: _addRefInExport, isInNamespace: _isInNamespace, }; ctx.visitor.visit(program); for (let i = 0; i < state.memberRefsInFile.length; i += 2) { const exp = state.exports.get(state.memberRefsInFile[i]); if (exp) { const id = state.memberRefsInFile[i + 1]; for (const member of exp.members) { if (member.identifier === id) member.hasRefsInFile = true; } } } for (const [aliasName, aliasSet] of state.importAliases) { if (!state.accessedAliases.has(aliasName)) { for (const alias of aliasSet) { const _import = state.localImportMap.get(alias.id); if (_import?.isNamespace) { const internalImport = state.internal.get(_import.filePath); if (internalImport) { addValue(internalImport.import, OPAQUE, filePath); } } } } } for (const [containerName, propMap] of state.nsContainers) { for (const [propKey, nsName] of propMap) { if (!state.accessedNsContainers.has(`${containerName}.${propKey}`)) { const _import = state.localImportMap.get(nsName); if (_import) { const internalImport = state.internal.get(_import.filePath); if (internalImport) { addValue(internalImport.import, OPAQUE, filePath); } } } } } if (!state.skipBareExprRefs) { for (const name of state.bareExprRefs) { const item = state.exports.get(name); if (item) item.hasRefsInFile = true; } } const localRefs = state.localRefs; state = undefined; return localRefs; }