UNPKG

knip

Version:

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

234 lines (233 loc) 10.4 kB
import ts from 'typescript'; import { CacheConsultant } from './CacheConsultant.js'; import { getCompilerExtensions } from './compilers/index.js'; import { ANONYMOUS, DEFAULT_EXTENSIONS, MEMBER_FLAGS, PUBLIC_TAG } from './constants.js'; import { createHosts } from './typescript/create-hosts.js'; import { _getImportsAndExports } from './typescript/get-imports-and-exports.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'; const baseCompilerOptions = { allowJs: true, allowSyntheticDefaultImports: true, declaration: false, declarationMap: false, esModuleInterop: true, inlineSourceMap: false, inlineSources: false, jsx: ts.JsxEmit.Preserve, jsxImportSource: undefined, lib: [], noEmit: true, skipDefaultLibCheck: true, skipLibCheck: true, sourceMap: false, types: ['node'], }; const tsCreateProgram = timerify(ts.createProgram); export class ProjectPrincipal { entryPaths = new Set(); projectPaths = new Set(); programPaths = new Set(); skipExportsAnalysis = new Set(); cwd; compilerOptions; extensions; syncCompilers; asyncCompilers; isWatch; cache; toSourceFilePath; backend; findReferences; getImplementationAtPosition; constructor(options, { compilerOptions, compilers, pkgName, toSourceFilePath }) { this.compilerOptions = { ...compilerOptions, ...baseCompilerOptions, types: compact([...(compilerOptions.types ?? []), ...(baseCompilerOptions.types ?? [])]), allowNonTsExtensions: true, }; const [syncCompilers, asyncCompilers] = compilers; this.extensions = new Set([...DEFAULT_EXTENSIONS, ...getCompilerExtensions(compilers)]); this.syncCompilers = syncCompilers; this.asyncCompilers = asyncCompilers; this.cwd = options.cwd; this.isWatch = options.isWatch || options.isSession; this.cache = new CacheConsultant(pkgName || ANONYMOUS, options); this.toSourceFilePath = toSourceFilePath; this.backend = { fileManager: new SourceFileManager({ compilers, isSkipLibs: options.isSkipLibs }), }; } init() { const { compilerHost, resolveModuleNames, languageServiceHost } = createHosts({ cwd: this.cwd, compilerOptions: this.compilerOptions, entryPaths: this.entryPaths, compilers: [this.syncCompilers, this.asyncCompilers], toSourceFilePath: this.toSourceFilePath, useResolverCache: !this.isWatch, fileManager: this.backend.fileManager, }); this.backend.compilerHost = compilerHost; this.backend.resolveModuleNames = resolveModuleNames; this.backend.languageServiceHost = languageServiceHost; } addPaths(paths, basePath) { if (!paths) return; this.compilerOptions.paths ??= {}; for (const key in paths) { const prefixes = paths[key].map(prefix => toAbsolute(prefix, basePath)); if (key in this.compilerOptions.paths) { this.compilerOptions.paths[key] = compact([...this.compilerOptions.paths[key], ...prefixes]); } else { this.compilerOptions.paths[key] = prefixes; } } } addCompilers(compilers) { this.syncCompilers = new Map([...this.syncCompilers, ...compilers[0]]); this.asyncCompilers = new Map([...this.asyncCompilers, ...compilers[1]]); this.extensions = new Set([...this.extensions, ...getCompilerExtensions(compilers)]); } createProgram() { this.backend.program = tsCreateProgram([...this.entryPaths, ...this.programPaths], this.compilerOptions, this.backend.compilerHost, this.backend.program); const typeChecker = timerify(this.backend.program.getTypeChecker); this.backend.typeChecker = typeChecker(); } 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); } } deletedFiles = new Set(); removeProjectPath(filePath) { this.entryPaths.delete(filePath); this.projectPaths.delete(filePath); this.invalidateFile(filePath); this.deletedFiles.add(filePath); } async runAsyncCompilers() { const add = timerify(this.backend.fileManager.compileAndAddSourceFile.bind(this.backend.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); } } getUsedResolvedFiles() { this.createProgram(); const sourceFiles = this.getProgramSourceFiles(); return Array.from(this.projectPaths).filter(filePath => sourceFiles.has(filePath)); } getProgramSourceFiles() { const programSourceFiles = this.backend.program?.getSourceFiles().map(sourceFile => sourceFile.fileName); return new Set(programSourceFiles); } getUnreferencedFiles() { const sourceFiles = this.getProgramSourceFiles(); return Array.from(this.projectPaths).filter(filePath => !sourceFiles.has(filePath)); } analyzeSourceFile(filePath, options, ignoreExportsUsedInFile) { const fd = this.cache.getFileDescriptor(filePath); if (!fd.changed && fd.meta?.data) return fd.meta.data; const typeChecker = this.backend.typeChecker; if (!typeChecker) throw new Error('TypeChecker must be initialized before source file analysis'); const sourceFile = this.backend.fileManager.getSourceFile(filePath); if (!sourceFile) throw new Error(`Unable to find ${filePath}`); const skipExports = this.skipExportsAnalysis.has(filePath); const resolve = (specifier) => this.backend.resolveModuleNames([specifier], sourceFile.fileName)[0]; return _getImportsAndExports(sourceFile, resolve, typeChecker, options, ignoreExportsUsedInFile, skipExports); } invalidateFile(filePath) { this.backend.fileManager.invalidate(filePath); } findUnusedMembers(filePath, members) { if (!this.findReferences || !this.getImplementationAtPosition) { const languageService = ts.createLanguageService(this.backend.languageServiceHost, ts.createDocumentRegistry()); this.findReferences = timerify(languageService.findReferences); this.getImplementationAtPosition = timerify(languageService.getImplementationAtPosition); } return members.filter(member => { if (member.jsDocTags.has(PUBLIC_TAG)) return false; const implementations = this.getImplementationAtPosition(filePath, member.pos)?.filter(impl => impl.fileName !== filePath || impl.textSpan.start !== member.pos) ?? []; const referencedSymbols = this.findReferences(filePath, member.pos) ?? []; if (referencedSymbols.length > 1 && referencedSymbols.some(sym => isInNodeModules(sym.definition.fileName))) { return false; } const refs = referencedSymbols .filter(sym => !implementations.some(impl => impl.fileName === sym.definition.fileName && impl.textSpan.start === sym.definition.textSpan.start && impl.textSpan.length === sym.definition.textSpan.length)) .flatMap(refs => refs.references) .filter(ref => !ref.isDefinition); if (refs.length === 0) return true; if (member.flags & MEMBER_FLAGS.SETTER) return false; return !refs.some(ref => !ref.isWriteAccess); }); } hasExternalReferences(filePath, exportedItem) { if (exportedItem.jsDocTags.has(PUBLIC_TAG)) return false; if (!this.findReferences) { const languageService = ts.createLanguageService(this.backend.languageServiceHost, ts.createDocumentRegistry()); this.findReferences = timerify(languageService.findReferences); } const referencedSymbols = this.findReferences(filePath, exportedItem.pos); if (!referencedSymbols?.length) return false; const externalRefs = referencedSymbols .flatMap(refs => refs.references) .filter(ref => !ref.isDefinition && ref.fileName !== filePath) .filter(ref => { const sourceFile = this.backend.program?.getSourceFile(ref.fileName); if (!sourceFile) return true; const node = ts.getTokenAtPosition(sourceFile, ref.textSpan.start); if (!node?.parent?.parent?.parent) return true; return !(ts.isExportSpecifier(node.parent) && node.parent.parent.parent.moduleSpecifier); }); return externalRefs.length > 0; } reconcileCache(graph) { for (const [filePath, file] of graph) { const fd = this.cache.getFileDescriptor(filePath); if (!fd?.meta) continue; fd.meta.data = { ...file, internalImportCache: undefined, imported: undefined }; } this.cache.reconcile(); } }