knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
253 lines (252 loc) • 14.8 kB
JavaScript
import { createGraphExplorer } from "../graph-explorer/explorer.js";
import { getIssueType, hasStrictlyEnumReferences } from "../graph-explorer/utils.js";
import traceReporter from "../reporters/trace.js";
import { getPackageNameFromModuleSpecifier } from "../util/modules.js";
import { findMatch } from "../util/regex.js";
import { getShouldIgnoreHandler, getShouldIgnoreTagHandler } from "../util/tag.js";
export const analyze = async ({ analyzedFiles, counselor, chief, collector, deputy, entryPaths, factory, graph, streamer, unreferencedFiles, options, }) => {
const shouldIgnore = getShouldIgnoreHandler(options.isProduction);
const shouldIgnoreTags = getShouldIgnoreTagHandler(options.tags);
const explorer = createGraphExplorer(graph, entryPaths);
const isReferencedInUsedExport = (exportedItem, filePath, includeEntryExports) => {
if (!exportedItem.referencedIn)
return false;
const file = graph.get(filePath);
if (!file)
return false;
for (const containingExport of exportedItem.referencedIn) {
if (explorer.isReferenced(filePath, containingExport, { includeEntryExports })[0])
return true;
const inExport = file.exports.get(containingExport);
if (!inExport)
return false;
if (inExport.hasRefsInFile && (inExport.type === 'type' || inExport.type === 'interface'))
return true;
}
return false;
};
const analyzeGraph = async () => {
if (options.isReportValues || options.isReportTypes) {
streamer.cast('Connecting the dots');
for (const [filePath, file] of graph) {
const exportItems = file.exports;
if (!exportItems || exportItems.size === 0)
continue;
const workspace = chief.findWorkspaceByFilePath(filePath);
if (workspace) {
const { isIncludeEntryExports } = workspace.config;
const principal = factory.getPrincipalByPackageName(workspace.pkgName);
const isEntry = entryPaths.has(filePath);
if (!isIncludeEntryExports && isEntry) {
continue;
}
const importsForExport = file.importedBy;
for (const [identifier, exportedItem] of exportItems) {
if (shouldIgnore(exportedItem.jsDocTags))
continue;
const isIgnored = shouldIgnoreTags(exportedItem.jsDocTags);
if (importsForExport) {
const [isReferenced, reExportingEntryFile] = explorer.isReferenced(filePath, identifier, {
includeEntryExports: isIncludeEntryExports,
});
if (isIgnored &&
(isReferenced || isReferencedInUsedExport(exportedItem, filePath, isIncludeEntryExports))) {
for (const tagName of exportedItem.jsDocTags) {
if (options.tags[1].includes(tagName.replace(/^@/, ''))) {
collector.addTagHint({ type: 'tag', filePath, identifier, tagName });
}
}
}
if (isIgnored)
continue;
if (reExportingEntryFile && !isReferenced) {
if (!isIncludeEntryExports) {
continue;
}
const reExportedItem = graph.get(reExportingEntryFile)?.exports.get(identifier);
if (reExportedItem && shouldIgnore(reExportedItem.jsDocTags))
continue;
}
if (isReferenced) {
if (options.includedIssueTypes.enumMembers && exportedItem.type === 'enum') {
if (!options.includedIssueTypes.nsTypes && importsForExport.refs.has(identifier))
continue;
if (hasStrictlyEnumReferences(importsForExport, identifier))
continue;
for (const member of exportedItem.members) {
if (findMatch(workspace.ignoreMembers, member.identifier))
continue;
if (shouldIgnore(member.jsDocTags))
continue;
if (!member.hasRefsInFile) {
const id = `${identifier}.${member.identifier}`;
const [isMemberReferenced] = explorer.isReferenced(filePath, id, {
includeEntryExports: true,
});
const isIgnored = shouldIgnoreTags(member.jsDocTags);
if (!isMemberReferenced) {
if (isIgnored)
continue;
collector.addIssue({
type: 'enumMembers',
filePath,
workspace: workspace.name,
symbol: member.identifier,
parentSymbol: identifier,
pos: member.pos,
line: member.line,
col: member.col,
fixes: member.fix ? [member.fix] : [],
});
}
else if (isIgnored) {
for (const tagName of exportedItem.jsDocTags) {
if (options.tags[1].includes(tagName.replace(/^@/, ''))) {
collector.addTagHint({ type: 'tag', filePath, identifier: id, tagName });
}
}
}
}
}
}
if (principal && options.isReportClassMembers && exportedItem.type === 'class') {
const members = exportedItem.members.filter(member => !(findMatch(workspace.ignoreMembers, member.identifier) || shouldIgnore(member.jsDocTags)));
for (const member of principal.findUnusedMembers(filePath, members)) {
if (shouldIgnoreTags(member.jsDocTags)) {
const identifier = `${exportedItem.identifier}.${member.identifier}`;
for (const tagName of exportedItem.jsDocTags) {
if (options.tags[1].includes(tagName.replace(/^@/, ''))) {
collector.addTagHint({ type: 'tag', filePath, identifier, tagName });
}
}
continue;
}
collector.addIssue({
type: 'classMembers',
filePath,
workspace: workspace.name,
symbol: member.identifier,
parentSymbol: exportedItem.identifier,
pos: member.pos,
line: member.line,
col: member.col,
fixes: member.fix ? [member.fix] : [],
});
}
}
continue;
}
}
const [hasStrictlyNsRefs, namespace] = explorer.hasStrictlyNsReferences(filePath, identifier);
const isType = ['enum', 'type', 'interface'].includes(exportedItem.type);
if (isIgnored ||
exportedItem.hasRefsInFile ||
isReferencedInUsedExport(exportedItem, filePath, isIncludeEntryExports) ||
(hasStrictlyNsRefs &&
((!options.includedIssueTypes.nsTypes && isType) ||
!(options.includedIssueTypes.nsExports || isType))) ||
(!options.isSkipLibs && principal?.hasExternalReferences(filePath, exportedItem))) {
continue;
}
const type = getIssueType(hasStrictlyNsRefs, isType);
collector.addIssue({
type,
filePath,
workspace: workspace.name,
symbol: identifier,
symbolType: exportedItem.type,
parentSymbol: namespace,
pos: exportedItem.pos,
line: exportedItem.line,
col: exportedItem.col,
fixes: exportedItem.fixes,
});
}
}
}
}
for (const [filePath, file] of graph) {
const ws = chief.findWorkspaceByFilePath(filePath);
if (ws) {
if (file.duplicates && options.includedIssueTypes.duplicates) {
for (const symbols of file.duplicates) {
if (symbols.length > 1) {
const symbol = symbols.map(s => s.symbol).join('|');
collector.addIssue({ type: 'duplicates', filePath, workspace: ws.name, symbol, symbols, fixes: [] });
}
}
}
if (file.imports?.external) {
for (const extImport of file.imports.external) {
const packageName = getPackageNameFromModuleSpecifier(extImport.specifier);
const isHandled = packageName && deputy.maybeAddReferencedExternalDependency(ws, packageName);
if (!isHandled)
collector.addIssue({
type: 'unlisted',
filePath,
workspace: ws.name,
symbol: packageName ?? extImport.specifier,
specifier: extImport.specifier,
pos: extImport.pos,
line: extImport.line,
col: extImport.col,
fixes: [],
});
}
}
if (file.imports?.unresolved) {
for (const unresolvedImport of file.imports.unresolved) {
const { specifier, pos, line, col } = unresolvedImport;
collector.addIssue({
type: 'unresolved',
filePath,
workspace: ws.name,
symbol: specifier,
pos,
line,
col,
fixes: [],
});
}
}
}
}
const unusedFiles = options.isReportFiles
? [...unreferencedFiles].filter(filePath => !analyzedFiles.has(filePath))
: [];
if (options.isReportFiles)
collector.addFilesIssues(unusedFiles);
collector.addFileCounts({ processed: analyzedFiles.size, unused: unusedFiles.length });
if (options.isReportDependencies) {
const { dependencyIssues, devDependencyIssues, optionalPeerDependencyIssues } = deputy.settleDependencyIssues();
for (const issue of dependencyIssues)
collector.addIssue(issue);
if (!options.isProduction)
for (const issue of devDependencyIssues)
collector.addIssue(issue);
for (const issue of optionalPeerDependencyIssues)
collector.addIssue(issue);
deputy.removeIgnoredIssues(collector.getIssues());
const configurationHints = deputy.getConfigurationHints();
for (const hint of configurationHints)
collector.addConfigurationHint(hint);
}
const catalogIssues = await counselor.settleCatalogIssues(options);
for (const issue of catalogIssues)
collector.addIssue(issue);
const unusedIgnoredWorkspaces = chief.getUnusedIgnoredWorkspaces();
for (const identifier of unusedIgnoredWorkspaces) {
collector.addConfigurationHint({ type: 'ignoreWorkspaces', identifier });
}
for (const hint of collector.getUnusedIgnorePatternHints(options)) {
collector.addConfigurationHint(hint);
}
for (const hint of chief.getConfigurationHints())
collector.addConfigurationHint(hint);
};
await analyzeGraph();
if (options.isTrace) {
traceReporter({ graph, explorer, options, workspaceFilePathFilter: chief.workspaceFilePathFilter });
}
return analyzeGraph;
};