UNPKG

knip

Version:

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

237 lines (236 loc) 14.2 kB
import { createGraphExplorer } from '../graph-explorer/explorer.js'; import { getIssueType, hasStrictlyEnumReferences } from '../graph-explorer/utils.js'; import traceReporter from '../reporters/trace.js'; import { getPackageNameFromModuleSpecifier } from '../util/modules.js'; import { findMatch } from '../util/regex.js'; import { getShouldIgnoreHandler, getShouldIgnoreTagHandler } from '../util/tag.js'; export const analyze = async ({ analyzedFiles, counselor, chief, collector, deputy, entryPaths, factory, graph, streamer, unreferencedFiles, options, }) => { const shouldIgnore = getShouldIgnoreHandler(options.isProduction); const shouldIgnoreTags = getShouldIgnoreTagHandler(options.tags); const explorer = createGraphExplorer(graph, entryPaths); const ignoreExportsUsedInFile = chief.config.ignoreExportsUsedInFile; const isExportReferencedInFile = (exportedItem) => exportedItem.self[1] || (exportedItem.self[0] > 0 && (typeof ignoreExportsUsedInFile === 'object' ? exportedItem.type !== 'unknown' && !!ignoreExportsUsedInFile[exportedItem.type] : ignoreExportsUsedInFile)); const analyzeGraph = async () => { if (options.isReportValues || options.isReportTypes) { streamer.cast('Connecting the dots'); for (const [filePath, file] of graph) { const exportItems = file.exports; if (!exportItems || exportItems.size === 0) continue; const workspace = chief.findWorkspaceByFilePath(filePath); if (workspace) { const { isIncludeEntryExports } = workspace.config; const principal = factory.getPrincipalByPackageName(workspace.pkgName); const isEntry = entryPaths.has(filePath); if (!isIncludeEntryExports && isEntry) { continue; } const importsForExport = file.imported; for (const [identifier, exportedItem] of exportItems) { if (shouldIgnore(exportedItem.jsDocTags)) continue; const isIgnored = shouldIgnoreTags(exportedItem.jsDocTags); if (importsForExport) { const [isReferenced, reExportingEntryFile] = explorer.isReferenced(filePath, identifier, { includeEntryExports: isIncludeEntryExports, }); if ((isReferenced || exportedItem.self[1]) && isIgnored) { for (const tagName of exportedItem.jsDocTags) { if (options.tags[1].includes(tagName.replace(/^@/, ''))) { collector.addTagHint({ type: 'tag', filePath, identifier, tagName }); } } } if (isIgnored) continue; if (reExportingEntryFile && !isReferenced) { if (!isIncludeEntryExports) { continue; } const reExportedItem = graph.get(reExportingEntryFile)?.exports.get(identifier); if (reExportedItem && shouldIgnore(reExportedItem.jsDocTags)) continue; } if (isReferenced) { if (options.includedIssueTypes.enumMembers && exportedItem.type === 'enum') { if (!options.includedIssueTypes.nsTypes && importsForExport.refs.has(identifier)) continue; if (hasStrictlyEnumReferences(importsForExport, identifier)) continue; for (const member of exportedItem.members) { if (findMatch(workspace.ignoreMembers, member.identifier)) continue; if (shouldIgnore(member.jsDocTags)) continue; if (member.self[0] === 0) { const id = `${identifier}.${member.identifier}`; const [isMemberReferenced] = explorer.isReferenced(filePath, id, { includeEntryExports: true, }); const isIgnored = shouldIgnoreTags(member.jsDocTags); if (!isMemberReferenced) { if (isIgnored) continue; collector.addIssue({ type: 'enumMembers', filePath, workspace: workspace.name, symbol: member.identifier, parentSymbol: identifier, pos: member.pos, line: member.line, col: member.col, fixes: member.fix ? [member.fix] : [], }); } else if (isIgnored) { for (const tagName of exportedItem.jsDocTags) { if (options.tags[1].includes(tagName.replace(/^@/, ''))) { collector.addTagHint({ type: 'tag', filePath, identifier: id, tagName }); } } } } } } if (principal && options.isReportClassMembers && exportedItem.type === 'class') { const members = exportedItem.members.filter(member => !(findMatch(workspace.ignoreMembers, member.identifier) || shouldIgnore(member.jsDocTags))); for (const member of principal.findUnusedMembers(filePath, members)) { if (shouldIgnoreTags(member.jsDocTags)) { const identifier = `${exportedItem.identifier}.${member.identifier}`; for (const tagName of exportedItem.jsDocTags) { if (options.tags[1].includes(tagName.replace(/^@/, ''))) { collector.addTagHint({ type: 'tag', filePath, identifier, tagName }); } } continue; } collector.addIssue({ type: 'classMembers', filePath, workspace: workspace.name, symbol: member.identifier, parentSymbol: exportedItem.identifier, pos: member.pos, line: member.line, col: member.col, fixes: member.fix ? [member.fix] : [], }); } } continue; } } const [hasStrictlyNsRefs, namespace] = explorer.hasStrictlyNsReferences(filePath, identifier); const isType = ['enum', 'type', 'interface'].includes(exportedItem.type); if (hasStrictlyNsRefs && ((!options.includedIssueTypes.nsTypes && isType) || !(options.includedIssueTypes.nsExports || isType))) continue; if (!isExportReferencedInFile(exportedItem)) { if (isIgnored) continue; if (!options.isSkipLibs && principal?.hasExternalReferences(filePath, exportedItem)) continue; const type = getIssueType(hasStrictlyNsRefs, isType); collector.addIssue({ type, filePath, workspace: workspace.name, symbol: identifier, symbolType: exportedItem.type, parentSymbol: namespace, pos: exportedItem.pos, line: exportedItem.line, col: exportedItem.col, fixes: exportedItem.fixes, }); } } } } } for (const [filePath, file] of graph) { const ws = chief.findWorkspaceByFilePath(filePath); if (ws) { if (file.duplicates && options.includedIssueTypes.duplicates) { for (const symbols of file.duplicates) { if (symbols.length > 1) { const symbol = symbols.map(s => s.symbol).join('|'); collector.addIssue({ type: 'duplicates', filePath, workspace: ws.name, symbol, symbols, fixes: [] }); } } } if (file.imports?.external) { for (const extImport of file.imports.external) { const packageName = getPackageNameFromModuleSpecifier(extImport.specifier); const isHandled = packageName && deputy.maybeAddReferencedExternalDependency(ws, packageName); if (!isHandled) collector.addIssue({ type: 'unlisted', filePath, workspace: ws.name, symbol: packageName ?? extImport.specifier, specifier: extImport.specifier, pos: extImport.pos, line: extImport.line, col: extImport.col, fixes: [], }); } } if (file.imports?.unresolved) { for (const unresolvedImport of file.imports.unresolved) { const { specifier, pos, line, col } = unresolvedImport; collector.addIssue({ type: 'unresolved', filePath, workspace: ws.name, symbol: specifier, pos, line, col, fixes: [], }); } } } } const unusedFiles = options.isReportFiles ? [...unreferencedFiles].filter(filePath => !analyzedFiles.has(filePath)) : []; if (options.isReportFiles) collector.addFilesIssues(unusedFiles); collector.addFileCounts({ processed: analyzedFiles.size, unused: unusedFiles.length }); if (options.isReportDependencies) { const { dependencyIssues, devDependencyIssues, optionalPeerDependencyIssues } = deputy.settleDependencyIssues(); for (const issue of dependencyIssues) collector.addIssue(issue); if (!options.isProduction) for (const issue of devDependencyIssues) collector.addIssue(issue); for (const issue of optionalPeerDependencyIssues) collector.addIssue(issue); deputy.removeIgnoredIssues(collector.getIssues()); const configurationHints = deputy.getConfigurationHints(); for (const hint of configurationHints) collector.addConfigurationHint(hint); } const catalogIssues = await counselor.settleCatalogIssues(options); for (const issue of catalogIssues) collector.addIssue(issue); const unusedIgnoredWorkspaces = chief.getUnusedIgnoredWorkspaces(); for (const identifier of unusedIgnoredWorkspaces) { collector.addConfigurationHint({ type: 'ignoreWorkspaces', identifier }); } for (const hint of chief.getConfigurationHints()) collector.addConfigurationHint(hint); }; await analyzeGraph(); if (options.isTrace) traceReporter({ graph, explorer, options, isExportReferencedInFile }); return analyzeGraph; };