knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
306 lines (305 loc) • 14.2 kB
JavaScript
import { isBuiltin } from 'node:module';
import { IMPORT_FLAGS, IMPORT_STAR, OPAQUE, PROTOCOL_VIRTUAL, SIDE_EFFECTS } from '../constants.js';
import { addNsValue, addValue, createImports } from '../util/module-graph.js';
import { getPackageNameFromFilePath, isStartsLikePackageName, sanitizeSpecifier } from '../util/modules.js';
import { timerify } from '../util/Performance.js';
import { dirname, isInNodeModules, resolve } from '../util/path.js';
import { shouldIgnore } from '../util/tag.js';
import { extractImportsFromComments } from './comments.js';
import { _parseFile, buildLineStarts, getLineAndCol, shouldCountRefs, } from './ast-nodes.js';
import { buildJSDocTagLookup } from './visitors/jsdoc.js';
import { _walkAST } from './visitors/walk.js';
const getImportsAndExports = (filePath, sourceText, resolveModule, options, ignoreExportsUsedInFile, skipExportsForFile, visitor, pluginCtx, cachedParseResult) => {
const skipExports = skipExportsForFile || !options.isReportExports;
const isDts = filePath.endsWith('.d.ts') || filePath.endsWith('.d.cts') || filePath.endsWith('.d.mts');
const internal = new Map();
const external = new Set();
const unresolved = new Set();
const programFiles = new Set();
const entryFiles = new Set();
const imports = new Set();
const exports = new Map();
const aliasedExports = new Map();
const specifierExportNames = new Set();
const scripts = new Set();
const importAliases = new Map();
const addImportAlias = (aliasName, id, importFilePath) => {
const aliases = importAliases.get(aliasName);
if (aliases)
aliases.add({ id, filePath: importFilePath });
else
importAliases.set(aliasName, new Set([{ id, filePath: importFilePath }]));
};
const localImportMap = new Map();
const localDeclarationTypes = new Map();
const referencedInExport = new Map();
const destructuredExports = new Set();
const addNsMemberRefs = (internalImport, namespace, member) => {
if (typeof member === 'string') {
internalImport.refs.add(`${namespace}.${member}`);
}
else {
for (const m of member)
internalImport.refs.add(`${namespace}.${m}`);
}
};
const addInternalImport = (opts) => {
const { filePath: importFilePath, namespace, specifier, modifiers } = opts;
const identifier = opts.identifier ?? (modifiers & IMPORT_FLAGS.OPAQUE ? OPAQUE : SIDE_EFFECTS);
const isStar = identifier === IMPORT_STAR;
imports.add({
filePath: importFilePath,
specifier,
identifier: namespace ?? opts.identifier,
pos: opts.pos,
line: opts.line,
col: opts.col,
isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY),
});
const file = internal.get(importFilePath);
const importMaps = file ?? createImports();
if (!file)
internal.set(importFilePath, importMaps);
const nsOrAlias = opts.alias;
if (modifiers & IMPORT_FLAGS.RE_EXPORT) {
if (isStar && namespace) {
addValue(importMaps.reExportNs, namespace, filePath);
}
else if (nsOrAlias) {
addNsValue(importMaps.reExportAs, identifier, nsOrAlias, filePath);
}
else {
addValue(importMaps.reExport, identifier, filePath);
}
}
else {
if (nsOrAlias && nsOrAlias !== identifier) {
if (isStar) {
addValue(importMaps.importNs, nsOrAlias, filePath);
}
else {
addNsValue(importMaps.importAs, identifier, nsOrAlias, filePath);
}
}
else if (identifier !== IMPORT_STAR) {
addValue(importMaps.import, identifier, filePath);
}
}
};
const addImport = (specifier, identifier, alias, namespace, pos, modifiers, specifierPos, jsDocTags, preResolvedModule) => {
if (!specifier || isBuiltin(specifier))
return;
const module = preResolvedModule ?? resolveModule(specifier, filePath);
if (module) {
const resolvedFileName = module.resolvedFileName;
if (resolvedFileName) {
if (!isInNodeModules(resolvedFileName)) {
if (modifiers & IMPORT_FLAGS.ENTRY)
entryFiles.add(resolvedFileName);
if (modifiers & IMPORT_FLAGS.BRIDGE)
programFiles.add(resolvedFileName);
}
if (!module.isExternalLibraryImport || !isInNodeModules(resolvedFileName)) {
const { line, col } = getLineAndCol(lineStarts, pos);
addInternalImport({
identifier,
alias,
namespace,
filePath: resolvedFileName,
specifier,
pos,
line,
col,
modifiers,
});
}
if (module.isExternalLibraryImport) {
if (options.skipTypeOnly && modifiers & IMPORT_FLAGS.TYPE_ONLY)
return;
const sanitizedSpecifier = sanitizeSpecifier(isInNodeModules(resolvedFileName) || isInNodeModules(specifier)
? getPackageNameFromFilePath(specifier)
: specifier);
if (!isStartsLikePackageName(sanitizedSpecifier))
return;
const ePos = specifierPos ?? pos;
const { line, col } = getLineAndCol(lineStarts, ePos);
external.add({
filePath: resolvedFileName,
specifier: sanitizedSpecifier,
identifier: identifier ?? SIDE_EFFECTS,
pos: ePos,
line,
col,
isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY),
});
}
}
}
else {
if (options.skipTypeOnly && modifiers & IMPORT_FLAGS.TYPE_ONLY)
return;
if (specifier.startsWith(PROTOCOL_VIRTUAL))
return;
if (modifiers && modifiers & IMPORT_FLAGS.OPTIONAL) {
programFiles.add(resolve(dirname(filePath), specifier));
return;
}
const uPos = specifierPos ?? pos;
const { line, col } = getLineAndCol(lineStarts, uPos);
if (!(jsDocTags?.size && shouldIgnore(jsDocTags, options.tags))) {
unresolved.add({
filePath: undefined,
specifier,
identifier: identifier ?? SIDE_EFFECTS,
pos: uPos,
line,
col,
isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY),
});
}
}
};
const result = cachedParseResult ?? _parseFile(filePath, sourceText);
const lineStarts = buildLineStarts(sourceText);
const getJSDocTags = buildJSDocTagLookup(result.comments, sourceText);
let hasNodeModuleImport = false;
let hasWorkerThreadsImport = false;
let hasChildProcessImport = false;
let hasPathJoinImport = false;
let hasPathResolveImport = false;
for (const _imports of result.module.staticImports) {
const specifier = _imports.moduleRequest.value;
if (specifier === 'node:module' || specifier === 'module')
hasNodeModuleImport = true;
else if (specifier === 'node:worker_threads' || specifier === 'worker_threads')
hasWorkerThreadsImport = true;
else if (specifier === 'node:child_process' || specifier === 'child_process')
hasChildProcessImport = true;
const isPathImport = specifier === 'node:path' || specifier === 'path';
const pos = _imports.moduleRequest.start;
const jsdocTags = getJSDocTags(_imports.start);
if (_imports.entries.length === 0) {
addImport(specifier, undefined, undefined, undefined, pos, IMPORT_FLAGS.SIDE_EFFECTS, undefined, jsdocTags);
continue;
}
const resolved = resolveModule(specifier, filePath);
const internalPath = resolved && !resolved.isExternalLibraryImport && !isInNodeModules(resolved.resolvedFileName)
? resolved.resolvedFileName
: undefined;
for (const entry of _imports.entries) {
const modifiers = entry.isType ? IMPORT_FLAGS.TYPE_ONLY : IMPORT_FLAGS.NONE;
if (entry.importName.kind === 'NamespaceObject') {
const localName = entry.localName.value;
addImport(specifier, IMPORT_STAR, localName, undefined, entry.localName.start, modifiers, pos, jsdocTags, resolved);
if (internalPath)
localImportMap.set(localName, { importedName: IMPORT_STAR, filePath: internalPath, isNamespace: true });
}
else if (entry.importName.kind === 'Default') {
const localName = entry.localName.value;
const alias = localName !== 'default' ? localName : undefined;
addImport(specifier, 'default', alias, undefined, entry.localName.start, modifiers, pos, jsdocTags, resolved);
if (internalPath)
localImportMap.set(localName, { importedName: 'default', filePath: internalPath, isNamespace: false });
}
else {
const importedName = entry.importName.name;
const localName = entry.localName.value;
const alias = localName !== importedName ? localName : undefined;
if (isPathImport && !alias) {
if (importedName === 'join')
hasPathJoinImport = true;
else if (importedName === 'resolve')
hasPathResolveImport = true;
}
addImport(specifier, importedName, alias, undefined, entry.localName.start, modifiers, pos, jsdocTags, resolved);
if (internalPath)
localImportMap.set(localName, { importedName, filePath: internalPath, isNamespace: false });
}
}
}
for (const se of result.module.staticExports) {
const jsdocTags = getJSDocTags(se.start);
let reExportResolved;
let reExportSpecifier;
for (const entry of se.entries) {
if (entry.moduleRequest) {
const specifier = entry.moduleRequest.value;
const modifiers = IMPORT_FLAGS.RE_EXPORT | (entry.isType ? IMPORT_FLAGS.TYPE_ONLY : IMPORT_FLAGS.NONE);
const pos = entry.moduleRequest.start;
if (specifier !== reExportSpecifier) {
reExportSpecifier = specifier;
reExportResolved = resolveModule(specifier, filePath);
}
if (entry.importName.kind === 'AllButDefault') {
addImport(specifier, IMPORT_STAR, undefined, undefined, pos, modifiers, undefined, jsdocTags, reExportResolved);
}
else if (entry.importName.kind === 'All') {
const ns = entry.exportName.name;
addImport(specifier, IMPORT_STAR, undefined, ns, entry.start, modifiers, pos, jsdocTags, reExportResolved);
}
else if (entry.importName.kind === 'Name') {
const importedName = entry.importName.name;
const exportedName = entry.exportName.name;
const alias = exportedName && exportedName !== importedName ? exportedName : undefined;
addImport(specifier, importedName, alias, undefined, entry.start, modifiers, pos, undefined, reExportResolved);
}
continue;
}
if (skipExports)
continue;
}
}
if (pluginCtx) {
pluginCtx.filePath = filePath;
pluginCtx.sourceText = sourceText;
pluginCtx.addScript = (s) => scripts.add(s);
pluginCtx.addImport = (spec, pos, mod) => addImport(spec, undefined, undefined, undefined, pos, mod);
}
const localRefs = _walkAST(result.program, sourceText, filePath, {
lineStarts,
skipExports,
options,
exports,
aliasedExports,
specifierExportNames,
scripts,
addImport,
addNsMemberRefs,
addImportAlias,
internal,
localImportMap,
localDeclarationTypes,
importAliases,
referencedInExport,
skipBareExprRefs: !!ignoreExportsUsedInFile,
localRefs: ignoreExportsUsedInFile ? new Set() : undefined,
destructuredExports,
hasNodeModuleImport,
hasWorkerThreadsImport,
hasChildProcessImport,
hasPathJoinImport,
hasPathResolveImport,
resolveModule,
programFiles,
entryFiles,
visitor,
getJSDocTags,
});
const firstStmtStart = result.program.body[0]?.start ?? Number.POSITIVE_INFINITY;
extractImportsFromComments(result.comments, firstStmtStart, addImport);
for (const [id, item] of exports) {
item.referencedIn = referencedInExport.get(id);
if (localRefs && shouldCountRefs(ignoreExportsUsedInFile, item.type) && (localRefs.has(id) || item.isReExport)) {
item.hasRefsInFile = true;
}
}
return {
imports: { internal, external, externalRefs: new Set(), programFiles, entryFiles, imports, unresolved },
exports,
duplicates: [...aliasedExports.values()],
scripts,
importedBy: undefined,
internalImportCache: undefined,
};
};
export const _getImportsAndExports = timerify(getImportsAndExports);