UNPKG

knip

Version:

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

231 lines (230 loc) 9.69 kB
import { extractSpecifiers } from "./typescript/follow-imports.js"; import { parseFile } from "./typescript/visitors/helpers.js"; import { CacheConsultant } from "./CacheConsultant.js"; import { getCompilerExtensions } from "./compilers/index.js"; import { DEFAULT_EXTENSIONS } from "./constants.js"; import { _getImportsAndExports } from "./typescript/get-imports-and-exports.js"; import { createBunShellVisitor } from "./typescript/visitors/script-visitors.js"; import { buildVisitor } from "./typescript/visitors/walk.js"; import { createCustomModuleResolver } from "./typescript/resolve-module-names.js"; import { SourceFileManager } from "./typescript/SourceFileManager.js"; import { compact } from "./util/array.js"; import { timerify } from "./util/Performance.js"; import { extname, isInNodeModules, toAbsolute } from "./util/path.js"; export class ProjectPrincipal { entryPaths = new Set(); projectPaths = new Set(); programPaths = new Set(); skipExportsAnalysis = new Set(); pluginCtx = { filePath: '', sourceText: '', addScript: () => { }, addImport: () => { }, }; pluginVisitorObjects = []; _visitor; syncCompilers = new Map(); asyncCompilers = new Map(); paths = {}; rootDirs = []; extensions = new Set(DEFAULT_EXTENSIONS); cache; toSourceFilePath; fileManager; resolveModule = () => undefined; resolvedFiles = new Set(); deletedFiles = new Set(); constructor(options, toSourceFilePath) { this.cache = new CacheConsultant('root', options); this.toSourceFilePath = toSourceFilePath; this.pluginVisitorObjects.push(createBunShellVisitor(this.pluginCtx)); this.fileManager = new SourceFileManager({ compilers: [this.syncCompilers, this.asyncCompilers], }); } addCompilers(compilers) { for (const [ext, compiler] of compilers[0]) { if (!this.syncCompilers.has(ext)) { this.syncCompilers.set(ext, compiler); this.extensions.add(ext); } } for (const [ext, compiler] of compilers[1]) { if (!this.asyncCompilers.has(ext)) { this.asyncCompilers.set(ext, compiler); this.extensions.add(ext); } } } addPaths(paths, basePath) { if (!paths) return; for (const key in paths) { const prefixes = paths[key].map(prefix => toAbsolute(prefix, basePath)); if (key in this.paths) { this.paths[key] = compact([...this.paths[key], ...prefixes]); } else { this.paths[key] = prefixes; } } } addRootDirs(rootDirs) { for (const dir of rootDirs) { if (!this.rootDirs.includes(dir)) this.rootDirs.push(dir); } } init() { this.extensions = new Set([ ...DEFAULT_EXTENSIONS, ...getCompilerExtensions([this.syncCompilers, this.asyncCompilers]), ]); const customCompilerExtensions = getCompilerExtensions([this.syncCompilers, this.asyncCompilers]); const pathsOrUndefined = Object.keys(this.paths).length > 0 ? this.paths : undefined; const rootDirsOrUndefined = this.rootDirs.length > 1 ? this.rootDirs : undefined; this.resolveModule = createCustomModuleResolver({ paths: pathsOrUndefined, rootDirs: rootDirsOrUndefined }, customCompilerExtensions, this.toSourceFilePath); } readFile(filePath) { return this.fileManager.readFile(filePath); } hasAcceptedExtension(filePath) { return this.extensions.has(extname(filePath)); } addEntryPath(filePath, options) { if (!isInNodeModules(filePath) && this.hasAcceptedExtension(filePath)) { this.entryPaths.add(filePath); this.projectPaths.add(filePath); if (options?.skipExportsAnalysis) this.skipExportsAnalysis.add(filePath); } } addEntryPaths(filePaths, options) { for (const filePath of filePaths) this.addEntryPath(filePath, options); } addProgramPath(filePath) { if (!isInNodeModules(filePath) && this.hasAcceptedExtension(filePath)) { this.programPaths.add(filePath); } } addProjectPath(filePath) { if (!isInNodeModules(filePath) && this.hasAcceptedExtension(filePath)) { this.projectPaths.add(filePath); this.deletedFiles.delete(filePath); } } removeProjectPath(filePath) { this.entryPaths.delete(filePath); this.projectPaths.delete(filePath); this.invalidateFile(filePath); this.deletedFiles.add(filePath); } async runAsyncCompilers() { const add = timerify(this.fileManager.compileAndAddSourceFile.bind(this.fileManager)); const extensions = Array.from(this.asyncCompilers.keys()); const files = Array.from(this.projectPaths).filter(filePath => extensions.includes(extname(filePath))); for (const filePath of files) { await add(filePath); } } walkAndAnalyze(analyzeFile) { this.resolvedFiles.clear(); const visited = new Set([...this.entryPaths, ...this.programPaths]); let lastEntrySize = this.entryPaths.size; let lastProgramSize = this.programPaths.size; for (const filePath of visited) { const sourceText = this.fileManager.readFile(filePath); if (!sourceText) { if (this.projectPaths.has(filePath)) analyzeFile(filePath, undefined, ''); continue; } try { const result = parseFile(filePath, sourceText); this.fileManager.sourceTextCache.delete(filePath); if (this.projectPaths.has(filePath)) { const internalPaths = analyzeFile(filePath, result, sourceText); if (internalPaths) { for (const p of internalPaths) visited.add(p); } } else { for (const specifier of extractSpecifiers(result, sourceText, filePath)) { const resolved = this.resolveSpecifier(specifier, filePath); if (resolved && !isInNodeModules(resolved)) visited.add(resolved); } } if (this.entryPaths.size > lastEntrySize || this.programPaths.size > lastProgramSize) { for (const p of this.entryPaths) visited.add(p); for (const p of this.programPaths) visited.add(p); lastEntrySize = this.entryPaths.size; lastProgramSize = this.programPaths.size; } } catch { } } this.resolvedFiles = visited; } getUsedResolvedFiles() { this.resolvedFiles.clear(); const visited = new Set([...this.entryPaths, ...this.programPaths]); for (const filePath of visited) { const sourceText = this.fileManager.readFile(filePath); if (!sourceText) continue; try { const result = parseFile(filePath, sourceText); for (const specifier of extractSpecifiers(result, sourceText, filePath)) { const resolved = this.resolveSpecifier(specifier, filePath); if (resolved && !isInNodeModules(resolved)) visited.add(resolved); } } catch { } } this.resolvedFiles = visited; return Array.from(this.projectPaths).filter(filePath => visited.has(filePath)); } resolveSpecifier(specifier, containingFile) { return this.resolveModule(specifier, containingFile)?.resolvedFileName; } getUnreferencedFiles() { return Array.from(this.projectPaths).filter(filePath => !this.resolvedFiles.has(filePath)); } analyzeSourceFile(filePath, options, ignoreExportsUsedInFile, parseResult, sourceText) { const fd = this.cache.getFileDescriptor(filePath); if (!fd.changed && fd.meta?.data) return fd.meta.data; sourceText ??= this.fileManager.readFile(filePath); const skipExports = this.skipExportsAnalysis.has(filePath); if (options.isFixExports || options.isFixTypes) { const ext = extname(filePath); if (!DEFAULT_EXTENSIONS.has(ext) && (this.syncCompilers.has(ext) || this.asyncCompilers.has(ext))) { options = { ...options, isFixExports: false, isFixTypes: false }; } } if (!this._visitor) this._visitor = buildVisitor(this.pluginVisitorObjects, !!ignoreExportsUsedInFile); return _getImportsAndExports(filePath, sourceText, this.resolveModule, options, ignoreExportsUsedInFile, skipExports, this._visitor, this.pluginVisitorObjects.length > 0 ? this.pluginCtx : undefined, parseResult); } invalidateFile(filePath) { this.fileManager.invalidate(filePath); } reconcileCache(graph) { for (const [filePath, file] of graph) { const fd = this.cache.getFileDescriptor(filePath); if (!fd?.meta) continue; fd.meta.data = { ...file, internalImportCache: undefined, importedBy: undefined }; } this.cache.reconcile(); } }