UNPKG

knip

Version:

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

429 lines (428 loc) 22 kB
import { _getInputsFromScripts } from "../binaries/index.js"; import { getCompilerExtensions, getIncludedCompilers, normalizeCompilerExtension } from "../compilers/index.js"; import { DEFAULT_EXTENSIONS, FOREIGN_FILE_EXTENSIONS, IS_DTS } from "../constants.js"; import { partition } from "../util/array.js"; import { createInputHandler } from "../util/create-input-handler.js"; import { debugLog, debugLogArray } from "../util/debug.js"; import { _glob, _syncGlob, negate, prependDirToPattern as prependDir } from "../util/glob.js"; import { isAlias, isConfig, isDeferResolveEntry, isDeferResolveProductionEntry, isEntry, isIgnore, isProductionEntry, isProject, toProductionEntry, } from "../util/input.js"; import { loadTSConfig } from "../util/load-tsconfig.js"; import { createFileNode, updateImportMap } from "../util/module-graph.js"; import { getPackageNameFromModuleSpecifier, isStartsLikePackageName, sanitizeSpecifier } from "../util/modules.js"; import { perfObserver } from "../util/Performance.js"; import { getEntrySpecifiersFromManifest, getManifestImportDependencies } from "../util/package-json.js"; import { dirname, extname, isAbsolute, join, relative, toRelative } from "../util/path.js"; import { augmentWorkspace, getModuleSourcePathHandler, getToSourcePathsHandler } from "../util/to-source-path.js"; import { WorkspaceWorker } from "../WorkspaceWorker.js"; export async function build({ chief, collector, counselor, deputy, factory, isGitIgnored, streamer, workspaces, options, }) { const configFilesMap = new Map(); const enabledPluginsStore = new Map(); const toModuleSourceFilePath = getModuleSourcePathHandler(chief); const toSourceFilePaths = getToSourcePathsHandler(chief); const addIssue = (issue) => collector.addIssue(issue) && options.isWatch && collector.retainIssue(issue); const externalRefsFromInputs = options.isSession ? new Map() : undefined; const handleInput = createInputHandler(deputy, chief, isGitIgnored, addIssue, externalRefsFromInputs, options); const rootManifest = chief.getManifestForWorkspace('.'); for (const workspace of workspaces) { const { name, dir, manifestPath, manifestStr } = workspace; const manifest = chief.getManifestForWorkspace(name); if (!manifest) continue; deputy.addWorkspace({ name, cwd: options.cwd, dir, manifestPath, manifestStr, manifest, ...chief.getIgnores(name), }); counselor.addWorkspace(manifest); } collector.addIgnorePatterns(chief.config.ignore.map(id => ({ pattern: prependDir(options.cwd, id), id }))); collector.addIgnoreFilesPatterns(chief.config.ignoreFiles.map(id => ({ pattern: prependDir(options.cwd, id), id }))); for (const workspace of workspaces) { const { name, dir, ancestors, pkgName, manifestPath: filePath } = workspace; streamer.cast('Analyzing workspace', name); const manifest = chief.getManifestForWorkspace(name); if (!manifest) continue; const dependencies = deputy.getDependencies(name); const baseConfig = chief.getConfigForWorkspace(name); const tsConfigFilePath = join(dir, options.tsConfigFile ?? 'tsconfig.json'); const { isFile, compilerOptions, fileNames } = await loadTSConfig(tsConfigFilePath); const [definitionPaths, tscSourcePaths] = partition(fileNames, filePath => IS_DTS.test(filePath)); if (isFile) augmentWorkspace(workspace, dir, compilerOptions); const worker = new WorkspaceWorker({ name, dir, config: baseConfig, manifest, dependencies, rootManifest, handleInput: (input) => handleInput(input, workspace), findWorkspaceByFilePath: chief.findWorkspaceByFilePath.bind(chief), negatedWorkspacePatterns: chief.getNegatedWorkspacePatterns(name), ignoredWorkspacePatterns: chief.getIgnoredWorkspacesFor(name), enabledPluginsInAncestors: ancestors.flatMap(ancestor => enabledPluginsStore.get(ancestor) ?? []), getSourceFile: (filePath) => principal.backend.fileManager.getSourceFile(filePath), configFilesMap, options, }); await worker.init(); const compilers = getIncludedCompilers(chief.config.syncCompilers, chief.config.asyncCompilers, dependencies); const registerCompiler = async ({ extension, compiler }) => { const ext = normalizeCompilerExtension(extension); if (compilers[0].has(ext)) return; compilers[0].set(ext, compiler); }; await worker.registerCompilers(registerCompiler); const extensions = getCompilerExtensions(compilers); const extensionGlobStr = `.{${[...DEFAULT_EXTENSIONS, ...extensions].map(ext => ext.slice(1)).join(',')}}`; const config = chief.getConfigForWorkspace(name, extensions); worker.config = config; const inputs = new Set(); if (definitionPaths.length > 0) { debugLogArray(name, 'Definition paths', definitionPaths); for (const id of definitionPaths) inputs.add(toProductionEntry(id, { containingFilePath: tsConfigFilePath })); } const sharedGlobOptions = { cwd: options.cwd, dir, gitignore: options.gitignore }; const fn = (id) => ({ pattern: prependDir(options.cwd, prependDir(name, id)), id, workspaceName: name }); collector.addIgnorePatterns(config.ignore.map(fn)); collector.addIgnoreFilesPatterns(config.ignoreFiles.map(fn)); const entrySpecifiersFromManifest = getEntrySpecifiersFromManifest(manifest); const label = 'entry paths from package.json'; for (const filePath of await toSourceFilePaths(entrySpecifiersFromManifest, dir, extensionGlobStr, label)) { inputs.add(toProductionEntry(filePath)); } for (const identifier of entrySpecifiersFromManifest) { if (!identifier.startsWith('!') && !isGitIgnored(join(dir, identifier))) { const files = _syncGlob({ patterns: [identifier], cwd: dir }); if (files.length === 0) { collector.addConfigurationHint({ type: 'package-entry', filePath, identifier, workspaceName: name }); } } } for (const dep of getManifestImportDependencies(manifest)) deputy.addReferencedDependency(name, dep); const principal = factory.createPrincipal(options, { dir, isFile, compilerOptions, compilers, pkgName, toSourceFilePath: toModuleSourceFilePath, }); principal.addPaths(config.paths, dir); const inputsFromPlugins = await worker.runPlugins(); for (const id of inputsFromPlugins) inputs.add(Object.assign(id, { skipExportsAnalysis: !id.allowIncludeExports })); enabledPluginsStore.set(name, worker.enabledPlugins); worker.registerVisitors(visitors => { if (visitors.dynamicImport) principal.visitors.dynamicImport.push(...visitors.dynamicImport); if (visitors.script) principal.visitors.script.push(...visitors.script); }); const DEFAULT_GROUP = 'default'; const createPatternMap = () => new Map([[DEFAULT_GROUP, new Set()]]); const groups = new Set([DEFAULT_GROUP]); const entryPatterns = createPatternMap(); const entryPatternsSkipExports = createPatternMap(); const productionPatterns = createPatternMap(); const productionPatternsSkipExports = createPatternMap(); const projectFilePatterns = new Set(); const addPattern = (map, input, pattern) => { if (input.group && !map.has(input.group)) map.set(input.group, new Set()); map.get(input.group ?? DEFAULT_GROUP).add(pattern); }; const toWorkspaceRelative = (path) => (isAbsolute(path) ? relative(dir, path) : path); for (const input of inputs) { if (input.group) groups.add(input.group); const specifier = input.specifier; if (isEntry(input)) { const targetMap = input.skipExportsAnalysis ? entryPatternsSkipExports : entryPatterns; addPattern(targetMap, input, toWorkspaceRelative(specifier)); } else if (isProductionEntry(input)) { const targetMap = input.skipExportsAnalysis ? productionPatternsSkipExports : productionPatterns; addPattern(targetMap, input, toWorkspaceRelative(specifier)); } else if (isProject(input)) { projectFilePatterns.add(toWorkspaceRelative(specifier)); } else if (isAlias(input)) { principal.addPaths({ [input.specifier]: input.prefixes }, input.dir ?? dir); } else if (isIgnore(input)) { if (input.issueType === 'dependencies' || input.issueType === 'unlisted') { deputy.addIgnoredDependencies(name, input.specifier); } else if (input.issueType === 'binaries') { deputy.addIgnoredBinaries(name, input.specifier); } else if (input.issueType === 'unresolved') { deputy.addIgnoredUnresolved(name, input.specifier); } } else if (!isConfig(input)) { const ws = (input.containingFilePath && chief.findWorkspaceByFilePath(input.containingFilePath)) || workspace; const resolvedFilePath = handleInput(input, ws); if (resolvedFilePath) { if (isDeferResolveProductionEntry(input)) { addPattern(productionPatternsSkipExports, input, resolvedFilePath); } else if (isDeferResolveEntry(input)) { if (!options.isProduction || !input.optional) addPattern(entryPatternsSkipExports, input, resolvedFilePath); } else { principal.addEntryPath(resolvedFilePath, { skipExportsAnalysis: true }); } } } } const negatedEntryPatterns = []; if (options.isProduction) { for (const map of [entryPatterns, entryPatternsSkipExports]) { for (const patterns of map.values()) for (const pattern of patterns) negatedEntryPatterns.push(negate(pattern)); } } const userEntryPatterns = options.isProduction ? worker.getProductionEntryFilePatterns(negatedEntryPatterns) : worker.getEntryFilePatterns(); const userEntryPaths = await _glob({ ...sharedGlobOptions, patterns: userEntryPatterns, gitignore: false, label: 'entry paths', }); for (const group of groups) { { const patterns = worker.getPluginEntryFilePatterns([ ...((!options.isProduction && entryPatterns.get(group)) || []), ...((!options.isProduction && group === DEFAULT_GROUP && worker.getPluginConfigPatterns()) || []), ...(productionPatterns.get(group) ?? []), ]); const label = `entry paths from plugins${group !== DEFAULT_GROUP ? ` - ${group}` : ''}`; const pluginWorkspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label }); principal.addEntryPaths(pluginWorkspaceEntryPaths); } { const patterns = worker.getPluginEntryFilePatterns([ ...((!options.isProduction && entryPatternsSkipExports.get(group)) || []), ...(productionPatternsSkipExports.get(group) ?? []), ]); const label = `entry paths from plugins (ignore exports)${group !== DEFAULT_GROUP ? ` - ${group}` : ''}`; const pluginWorkspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label }); principal.addEntryPaths(pluginWorkspaceEntryPaths, { skipExportsAnalysis: true }); } } if (!options.isProduction) { const hints = worker.getConfigurationHints('entry', userEntryPatterns, userEntryPaths, principal.entryPaths); for (const hint of hints) collector.addConfigurationHint(hint); } principal.addEntryPaths(userEntryPaths); if (options.isUseTscFiles && isFile) { const isIgnoredWorkspace = chief.createIgnoredWorkspaceMatcher(name, dir); debugLogArray(name, 'Using tsconfig files as project files', tscSourcePaths); for (const filePath of tscSourcePaths) { if (!isGitIgnored(filePath) && !isIgnoredWorkspace(filePath)) { principal.addProgramPath(filePath); principal.addProjectPath(filePath); } } } else { const patterns = options.isProduction ? worker.getProductionProjectFilePatterns(negatedEntryPatterns) : worker.getProjectFilePatterns([ ...(productionPatternsSkipExports.get(DEFAULT_GROUP) ?? []), ...projectFilePatterns, ...worker.getPluginProjectFilePatterns(), ]); const projectPaths = await _glob({ ...sharedGlobOptions, patterns, label: 'project paths' }); if (!options.isProduction) { const hints = worker.getConfigurationHints('project', config.project, projectPaths, principal.projectPaths); for (const hint of hints) collector.addConfigurationHint(hint); } for (const projectPath of projectPaths) principal.addProjectPath(projectPath); } if (options.configFilePath) { factory.getPrincipals().at(0)?.addEntryPath(options.configFilePath, { skipExportsAnalysis: true }); } worker.onDispose(); } const principals = factory.getPrincipals(); debugLog('*', `Created ${principals.length} programs for ${workspaces.length} workspaces`); const graph = new Map(); const analyzedFiles = new Set(); const unreferencedFiles = new Set(); const entryPaths = new Set(); const isInternalWorkspace = (packageName) => chief.availableWorkspacePkgNames.has(packageName); const getPrincipalByFilePath = (filePath) => { const workspace = chief.findWorkspaceByFilePath(filePath); if (workspace) return factory.getPrincipalByPackageName(workspace.pkgName); }; const analyzeOpts = { isFixExports: options.isFixUnusedExports, isFixTypes: options.isFixUnusedTypes, isReportClassMembers: options.isReportClassMembers, isReportExports: options.isReportExports, skipTypeOnly: options.isStrict, tags: options.tags, }; const analyzeSourceFile = (filePath, principal) => { if (!options.isWatch && !options.isSession && analyzedFiles.has(filePath)) return; analyzedFiles.add(filePath); const workspace = chief.findWorkspaceByFilePath(filePath); if (workspace) { const file = principal.analyzeSourceFile(filePath, analyzeOpts, chief.config.ignoreExportsUsedInFile); const unresolvedImports = new Set(); for (const unresolvedImport of file.imports.unresolved) { const { specifier } = unresolvedImport; if (specifier.startsWith('http')) continue; const sanitizedSpecifier = sanitizeSpecifier(specifier); if (isStartsLikePackageName(sanitizedSpecifier)) { file.imports.external.add({ ...unresolvedImport, specifier: sanitizedSpecifier }); } else { if (!isGitIgnored(join(dirname(filePath), sanitizedSpecifier))) { const ext = extname(sanitizedSpecifier); if (!ext || (ext !== '.json' && !FOREIGN_FILE_EXTENSIONS.has(ext))) unresolvedImports.add(unresolvedImport); } } } for (const filePath of file.imports.programFiles) { const isIgnored = isGitIgnored(filePath); if (!isIgnored) principal.addProgramPath(filePath); } for (const filePath of file.imports.entryFiles) { const isIgnored = isGitIgnored(filePath); if (!isIgnored) principal.addEntryPath(filePath, { skipExportsAnalysis: true }); } for (const _import of file.imports.imports) { if (_import.filePath) { const packageName = getPackageNameFromModuleSpecifier(_import.specifier); if (packageName && isInternalWorkspace(packageName)) { file.imports.external.add({ ..._import, specifier: packageName }); const principal = getPrincipalByFilePath(_import.filePath); if (principal && !isGitIgnored(_import.filePath)) { principal.addProgramPath(_import.filePath); } } } } if (file.scripts && file.scripts.size > 0) { const dependencies = deputy.getDependencies(workspace.name); const manifestScriptNames = new Set(Object.keys(chief.getManifestForWorkspace(workspace.name)?.scripts ?? {})); const dir = dirname(filePath); const opts = { cwd: dir, rootCwd: options.cwd, containingFilePath: filePath, dependencies, manifestScriptNames, rootManifest, }; const inputs = _getInputsFromScripts(file.scripts, opts); for (const input of inputs) { input.containingFilePath ??= filePath; input.dir ??= dir; const specifierFilePath = handleInput(input, workspace); if (specifierFilePath) principal.addEntryPath(specifierFilePath, { skipExportsAnalysis: true }); } } file.imports.unresolved = unresolvedImports; const pluginRefs = externalRefsFromInputs?.get(filePath); if (pluginRefs) for (const ref of pluginRefs) file.imports.externalRefs.add(ref); const node = graph.get(filePath); if (node) { node.imports = file.imports; node.exports = file.exports; node.duplicates = file.duplicates; node.scripts = file.scripts; updateImportMap(node, file.imports.internal, graph); node.internalImportCache = file.imports.internal; } else { updateImportMap(file, file.imports.internal, graph); file.internalImportCache = file.imports.internal; graph.set(filePath, file); } } }; for (let i = 0; i < principals.length; ++i) { const principal = principals[i]; if (!principal) continue; principal.init(); if (principal.asyncCompilers.size > 0) { streamer.cast('Running async compilers'); await principal.runAsyncCompilers(); } streamer.cast('Analyzing source files', toRelative(principal.cwd, options.cwd)); let size = principal.entryPaths.size; let round = 0; do { size = principal.entryPaths.size; const resolvedFiles = principal.getUsedResolvedFiles(); const files = resolvedFiles.filter(filePath => !analyzedFiles.has(filePath)); debugLogArray('*', `Analyzing used resolved files [P${i + 1}/${++round}]`, files); for (const filePath of files) analyzeSourceFile(filePath, principal); } while (size !== principal.entryPaths.size); for (const filePath of principal.getUnreferencedFiles()) unreferencedFiles.add(filePath); for (const filePath of principal.entryPaths) entryPaths.add(filePath); principal.reconcileCache(graph); if (options.isIsolateWorkspaces || (options.isSkipLibs && !options.isWatch && !options.isSession)) { factory.deletePrincipal(principal, options.cwd); principals[i] = undefined; } perfObserver.addMemoryMark(factory.getPrincipalCount()); } if (!options.isWatch && !options.isSession && options.isSkipLibs && !options.isIsolateWorkspaces) { for (const principal of principals) { if (principal) factory.deletePrincipal(principal, options.cwd); } principals.length = 0; } if (externalRefsFromInputs) { for (const [filePath, refs] of externalRefsFromInputs) { if (!graph.has(filePath)) graph.set(filePath, createFileNode()); for (const ref of refs) graph.get(filePath).imports.externalRefs.add(ref); } } return { graph, entryPaths, analyzedFiles, unreferencedFiles, analyzeSourceFile, enabledPluginsStore, }; }