knip
Version:
Find unused files, dependencies and exports in your TypeScript and JavaScript projects
248 lines (247 loc) • 10.9 kB
JavaScript
import ts from 'typescript';
import { CacheConsultant } from './CacheConsultant.js';
import { getCompilerExtensions } from './compilers/index.js';
import { ANONYMOUS, DEFAULT_EXTENSIONS, FOREIGN_FILE_EXTENSIONS, PUBLIC_TAG } from './constants.js';
import { createHosts } from './typescript/create-hosts.js';
import { _getImportsAndExports } from './typescript/get-imports-and-exports.js';
import { timerify } from './util/Performance.js';
import { compact } from './util/array.js';
import { getPackageNameFromModuleSpecifier, isStartsLikePackageName, sanitizeSpecifier } from './util/modules.js';
import { dirname, extname, isInNodeModules, join } 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();
nonEntryPaths = new Set();
skipExportsAnalysis = new Set();
cwd;
compilerOptions;
extensions;
syncCompilers;
asyncCompilers;
isSkipLibs;
isWatch;
cache;
toSourceFilePath;
backend;
findReferences;
constructor({ compilerOptions, cwd, compilers, isSkipLibs, isWatch, pkgName, toSourceFilePath, isCache, cacheLocation, }) {
this.cwd = cwd;
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.isSkipLibs = isSkipLibs;
this.isWatch = isWatch;
this.cache = new CacheConsultant({ name: pkgName || ANONYMOUS, isEnabled: isCache, cacheLocation });
this.toSourceFilePath = toSourceFilePath;
}
init() {
const { fileManager, compilerHost, resolveModuleNames, languageServiceHost } = createHosts({
cwd: this.cwd,
compilerOptions: this.compilerOptions,
entryPaths: this.entryPaths,
compilers: [this.syncCompilers, this.asyncCompilers],
isSkipLibs: this.isSkipLibs,
toSourceFilePath: this.toSourceFilePath,
useResolverCache: !this.isWatch,
});
this.backend = {
fileManager,
compilerHost,
resolveModuleNames,
languageServiceHost,
};
}
addPaths(paths) {
this.compilerOptions.paths = { ...this.compilerOptions.paths, ...paths };
}
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.nonEntryPaths], 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);
}
addNonEntryPath(filePath) {
if (!isInNodeModules(filePath) && this.hasAcceptedExtension(filePath)) {
this.nonEntryPaths.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, isGitIgnored, isPackageNameInternalWorkspace, getPrincipalByFilePath) {
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('Must initialize TypeChecker 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];
const { imports, ...rest } = _getImportsAndExports(sourceFile, resolve, typeChecker, { ...options, skipExports });
const { internal, resolved, specifiers, unresolved, external } = imports;
const unresolvedImports = new Set();
for (const [specifier, specifierFilePath] of specifiers) {
const packageName = getPackageNameFromModuleSpecifier(specifier);
if (packageName && isPackageNameInternalWorkspace(packageName)) {
external.add(packageName);
const principal = getPrincipalByFilePath(specifierFilePath);
if (principal && !isGitIgnored(specifierFilePath))
principal.addNonEntryPath(specifierFilePath);
}
}
for (const filePath of resolved) {
const isIgnored = isGitIgnored(filePath);
if (!isIgnored)
this.addEntryPath(filePath, { skipExportsAnalysis: true });
}
for (const unresolvedImport of unresolved) {
const { specifier } = unresolvedImport;
if (specifier.startsWith('http'))
continue;
const sanitizedSpecifier = sanitizeSpecifier(specifier);
if (isStartsLikePackageName(sanitizedSpecifier)) {
external.add(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);
}
}
}
}
return { imports: { internal, unresolved: unresolvedImports, external }, ...rest };
}
invalidateFile(filePath) {
this.backend.fileManager.snapshotCache.delete(filePath);
this.backend.fileManager.sourceFileCache.delete(filePath);
}
findUnusedMembers(filePath, members) {
if (!this.findReferences) {
const languageService = ts.createLanguageService(this.backend.languageServiceHost, ts.createDocumentRegistry());
this.findReferences = timerify(languageService.findReferences);
}
return members.filter(member => {
if (member.jsDocTags.has(PUBLIC_TAG))
return false;
const referencedSymbols = this.findReferences?.(filePath, member.pos) ?? [];
const refs = referencedSymbols.flatMap(refs => refs.references).filter(ref => !ref.isDefinition);
return refs.length === 0;
});
}
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.entries()) {
const fd = this.cache.getFileDescriptor(filePath);
if (!fd?.meta)
continue;
const { imported, internalImportCache, ...clone } = file;
fd.meta.data = clone;
}
this.cache.reconcile();
}
}