UNPKG

knip

Version:

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

412 lines (411 loc) 21.2 kB
import { _getInputsFromScripts } from '../binaries/index.js'; import { getCompilerExtensions, getIncludedCompilers } 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 } 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(p => prependDirToPattern(options.cwd, p))); collector.addIgnoreFilesPatterns(chief.config.ignoreFiles.map(p => prependDirToPattern(options.cwd, p))); 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 compilers = getIncludedCompilers(chief.config.syncCompilers, chief.config.asyncCompilers, dependencies); const extensions = getCompilerExtensions(compilers); const extensionGlobStr = `.{${[...DEFAULT_EXTENSIONS, ...extensions].map(ext => ext.slice(1)).join(',')}}`; const config = chief.getConfigForWorkspace(name, extensions); 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, 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 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 }; collector.addIgnorePatterns(config.ignore.map(p => prependDirToPattern(options.cwd, prependDirToPattern(name, p)))); collector.addIgnoreFilesPatterns(config.ignoreFiles.map(p => prependDirToPattern(options.cwd, prependDirToPattern(name, p)))); 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); 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 (!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) { 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 { const isIgnored = isGitIgnored(join(dirname(filePath), sanitizedSpecifier)); if (!isIgnored) { const ext = extname(sanitizedSpecifier); const hasIgnoredExtension = FOREIGN_FILE_EXTENSIONS.has(ext); if (!ext || (ext !== '.json' && !hasIgnoredExtension)) 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, }; }