knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
199 lines (198 loc) • 7.27 kB
JavaScript
import picomatch from 'picomatch';
import { partition } from "./util/array.js";
import { initCounters, initIssues } from "./util/issue-initializers.js";
import { relative } from "./util/path.js";
const createMatcher = (patterns) => {
const [negated, positive] = partition(patterns, p => p[0] === '!');
if (positive.length === 0) {
if (negated.length === 0)
return () => false;
return picomatch(negated, { dot: true });
}
return picomatch(positive, { dot: true, ignore: negated.map(p => p.slice(1)) });
};
export class IssueCollector {
cwd;
rules;
workspaceFilter;
issues = initIssues();
counters = initCounters();
referencedFiles = new Set();
configurationHints = new Map();
tagHints = new Set();
ignorePatterns = new Set();
ignoreFilesPatterns = new Set();
isMatch;
isFileMatch;
issueMatchers = new Map();
isTrackUnusedIgnorePatterns;
unusedIgnorePatterns = new Map();
unusedIgnoreFilesPatterns = new Map();
constructor(options) {
this.cwd = options.cwd;
this.rules = options.rules;
this.workspaceFilter = () => true;
this.isMatch = () => false;
this.isFileMatch = () => false;
this.isTrackUnusedIgnorePatterns = !options.isDisableConfigHints;
}
setWorkspaceFilter(workspaceFilePathFilter) {
if (workspaceFilePathFilter)
this.workspaceFilter = workspaceFilePathFilter;
}
addIgnorePatterns(entries) {
for (const entry of entries) {
this.ignorePatterns.add(entry.pattern);
if (!this.isTrackUnusedIgnorePatterns)
continue;
if (entry.pattern.startsWith('!'))
continue;
if (this.unusedIgnorePatterns.has(entry.pattern))
continue;
this.unusedIgnorePatterns.set(entry.pattern, {
hint: { type: 'ignore', identifier: entry.id, workspaceName: entry.workspaceName },
isMatch: picomatch(entry.pattern, { dot: true }),
});
}
this.isMatch = createMatcher(this.ignorePatterns);
}
addIgnoreFilesPatterns(entries) {
for (const entry of entries) {
this.ignoreFilesPatterns.add(entry.pattern);
if (!this.isTrackUnusedIgnorePatterns)
continue;
if (entry.pattern.startsWith('!'))
continue;
if (this.unusedIgnoreFilesPatterns.has(entry.pattern))
continue;
this.unusedIgnoreFilesPatterns.set(entry.pattern, {
hint: { type: 'ignoreFiles', identifier: entry.id, workspaceName: entry.workspaceName },
isMatch: picomatch(entry.pattern, { dot: true }),
});
}
this.isFileMatch = createMatcher(this.ignoreFilesPatterns);
}
markUsedPatterns(filePath, unused) {
if (unused.size === 0)
return;
for (const [pattern, { isMatch }] of unused) {
if (isMatch(filePath))
unused.delete(pattern);
}
}
setIgnoreIssues(ignoreIssues) {
if (!ignoreIssues)
return;
const issueTypePatterns = new Map();
for (const [pattern, issueTypes] of Object.entries(ignoreIssues)) {
for (const issueType of issueTypes) {
if (!issueTypePatterns.has(issueType)) {
issueTypePatterns.set(issueType, []);
}
issueTypePatterns.get(issueType)?.push(pattern);
}
}
for (const [issueType, patterns] of issueTypePatterns) {
this.issueMatchers.set(issueType, picomatch(patterns, { dot: true }));
}
}
shouldIgnoreIssue(filePath, issueType) {
const matcher = this.issueMatchers.get(issueType);
if (!matcher)
return false;
return matcher(relative(this.cwd, filePath));
}
addFileCounts({ processed, unused }) {
this.counters.processed += processed;
this.counters.total += processed + unused;
}
addFilesIssues(filePaths) {
for (const filePath of filePaths) {
if (!this.workspaceFilter(filePath))
continue;
if (this.referencedFiles.has(filePath))
continue;
if (this.isMatch(filePath)) {
this.markUsedPatterns(filePath, this.unusedIgnorePatterns);
continue;
}
if (this.isFileMatch(filePath)) {
this.markUsedPatterns(filePath, this.unusedIgnoreFilesPatterns);
continue;
}
if (this.shouldIgnoreIssue(filePath, 'files'))
continue;
const symbol = relative(this.cwd, filePath);
this.issues.files[symbol] = {
[symbol]: { type: 'files', filePath, symbol, workspace: '', severity: this.rules.files, fixes: [] },
};
this.counters.files++;
this.counters.processed++;
}
}
addIssue(issue) {
if (!this.workspaceFilter(issue.filePath))
return;
if (this.isMatch(issue.filePath)) {
this.markUsedPatterns(issue.filePath, this.unusedIgnorePatterns);
return;
}
if (this.shouldIgnoreIssue(issue.filePath, issue.type))
return;
if (this.rules[issue.type] === 'off')
return;
const key = relative(this.cwd, issue.filePath);
issue.severity = this.rules[issue.type];
const issues = this.issues[issue.type];
issues[key] = issues[key] ?? {};
const symbol = issue.parentSymbol ? `${issue.parentSymbol}.${issue.symbol}` : issue.symbol;
if (!issues[key][symbol]) {
issues[key][symbol] = issue;
this.counters[issue.type]++;
}
return true;
}
addConfigurationHint(issue) {
const key = `${issue.workspaceName}::${issue.type}::${issue.identifier}`;
if (!this.configurationHints.has(key))
this.configurationHints.set(key, issue);
}
addTagHint(issue) {
this.tagHints.add(issue);
}
purge() {
const unusedFiles = new Set();
for (const issues of Object.values(this.issues.files)) {
for (const issue of Object.values(issues))
unusedFiles.add(issue.filePath);
}
this.issues = initIssues();
this.counters = initCounters();
return unusedFiles;
}
getIssues() {
return {
issues: this.issues,
counters: this.counters,
tagHints: this.tagHints,
configurationHints: Array.from(this.configurationHints.values()),
};
}
getUnusedIgnorePatternHints(options) {
if (!options.isReportFiles)
return [];
const hints = [];
for (const p of this.unusedIgnorePatterns.values())
hints.push(p.hint);
for (const p of this.unusedIgnoreFilesPatterns.values())
hints.push(p.hint);
return hints;
}
retainedIssues = [];
retainIssue(issue) {
this.retainedIssues.push(issue);
}
getRetainedIssues() {
return this.retainedIssues;
}
}