knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
140 lines (139 loc) • 4.96 kB
JavaScript
import picomatch from 'picomatch';
import { partition } from './util/array.js';
import { initCounters, initIssues } from './util/issue-initializers.js';
import { join, 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;
filter;
issues = initIssues();
counters = initCounters();
referencedFiles = new Set();
configurationHints = new Map();
tagHints = new Set();
ignorePatterns = new Set();
ignoreFilesPatterns = new Set();
isMatch;
isFileMatch;
issueMatchers = new Map();
constructor(options) {
this.cwd = options.cwd;
this.rules = options.rules;
this.filter = options.workspace ? join(options.cwd, options.workspace) : undefined;
this.isMatch = () => false;
this.isFileMatch = () => false;
}
addIgnorePatterns(patterns) {
for (const pattern of patterns)
this.ignorePatterns.add(pattern);
this.isMatch = createMatcher(this.ignorePatterns);
}
addIgnoreFilesPatterns(patterns) {
for (const pattern of patterns)
this.ignoreFilesPatterns.add(pattern);
this.isFileMatch = createMatcher(this.ignoreFilesPatterns);
}
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;
const relativePath = relative(this.cwd, filePath);
return matcher(relativePath);
}
addFileCounts({ processed, unused }) {
this.counters.processed += processed;
this.counters.total += processed + unused;
}
addFilesIssues(filePaths) {
for (const filePath of filePaths) {
if (this.filter && !filePath.startsWith(`${this.filter}/`))
continue;
if (this.referencedFiles.has(filePath))
continue;
if (this.isMatch(filePath))
continue;
if (this.isFileMatch(filePath))
continue;
if (this.shouldIgnoreIssue(filePath, 'files'))
continue;
this.issues.files.add(filePath);
const symbol = relative(this.cwd, filePath);
this.issues._files[symbol] = [{ type: 'files', filePath, symbol, severity: this.rules.files }];
this.counters.files++;
this.counters.processed++;
}
}
addIssue(issue) {
if (this.filter && !issue.filePath.startsWith(`${this.filter}/`))
return;
if (this.isMatch(issue.filePath))
return;
if (this.shouldIgnoreIssue(issue.filePath, issue.type))
return;
const key = relative(this.cwd, issue.filePath);
const { type } = issue;
issue.severity = this.rules[type];
const issues = this.issues[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 = this.issues.files;
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()),
};
}
retainedIssues = [];
retainIssue(issue) {
this.retainedIssues.push(issue);
}
getRetainedIssues() {
return this.retainedIssues;
}
}