The verbatim-module-syntax lint rule was correctly flagging that GraphConfig is only used in a type position (typeof GraphConfig). Since typeof resolves purely at the type level, import type works fine here and is the correct form. No lint exclusion needed. Also: deno fmt across all files (markdown line wrapping).
260 lines
6.8 KiB
TypeScript
260 lines
6.8 KiB
TypeScript
import { parse } from "@std/flags";
|
|
import * as path from "@std/path";
|
|
|
|
interface LintRange {
|
|
start: { line: number; col: number; bytePos: number };
|
|
end: { line: number; col: number; bytePos: number };
|
|
}
|
|
|
|
interface LintDiagnostic {
|
|
filename: string;
|
|
range: LintRange;
|
|
message: string;
|
|
code: string;
|
|
hint: string;
|
|
}
|
|
|
|
interface LintResult {
|
|
version: number;
|
|
diagnostics: LintDiagnostic[];
|
|
errors: unknown[];
|
|
checkedFiles: string[];
|
|
}
|
|
|
|
interface FilterOptions {
|
|
codes?: string[];
|
|
files?: string[];
|
|
groupBy?: "code" | "file";
|
|
}
|
|
|
|
interface Stats {
|
|
total: number;
|
|
byCode: Record<string, number>;
|
|
byFile: Record<string, number>;
|
|
filesWithIssues: number;
|
|
}
|
|
|
|
function filterDiagnostics(
|
|
diagnostics: LintDiagnostic[],
|
|
options: FilterOptions,
|
|
): LintDiagnostic[] {
|
|
let result = diagnostics;
|
|
|
|
if (options.codes) {
|
|
const codes = new Set(options.codes);
|
|
result = result.filter((d) => codes.has(d.code));
|
|
}
|
|
|
|
if (options.files) {
|
|
const filePatterns = options.files.map((f) => new RegExp(f));
|
|
result = result.filter((d) =>
|
|
filePatterns.some((pattern) => pattern.test(d.filename))
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function groupDiagnostics(
|
|
diagnostics: LintDiagnostic[],
|
|
groupBy: "code" | "file",
|
|
): Record<string, LintDiagnostic[]> {
|
|
const groups: Record<string, LintDiagnostic[]> = {};
|
|
|
|
for (const diag of diagnostics) {
|
|
const key = groupBy === "code" ? diag.code : diag.filename;
|
|
if (!groups[key]) {
|
|
groups[key] = [];
|
|
}
|
|
groups[key].push(diag);
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
function calculateStats(diagnostics: LintDiagnostic[]): Stats {
|
|
const byCode: Record<string, number> = {};
|
|
const byFile: Record<string, number> = {};
|
|
|
|
for (const diag of diagnostics) {
|
|
byCode[diag.code] = (byCode[diag.code] || 0) + 1;
|
|
byFile[diag.filename] = (byFile[diag.filename] || 0) + 1;
|
|
}
|
|
|
|
return {
|
|
total: diagnostics.length,
|
|
byCode,
|
|
byFile,
|
|
filesWithIssues: Object.keys(byFile).length,
|
|
};
|
|
}
|
|
|
|
function printStats(stats: Stats, topN: number = 10) {
|
|
console.log("\n=== LINT ISSUE STATISTICS ===");
|
|
console.log(`Total issues: ${stats.total}`);
|
|
console.log(`Files with issues: ${stats.filesWithIssues}`);
|
|
|
|
console.log(`\nTop ${topN} issue types:`);
|
|
const sortedByCode = Object.entries(stats.byCode).sort((a, b) => b[1] - a[1]);
|
|
for (let i = 0; i < Math.min(topN, sortedByCode.length); i++) {
|
|
const [code, count] = sortedByCode[i];
|
|
console.log(` ${code}: ${count}`);
|
|
}
|
|
|
|
console.log(`\nTop ${topN} files with most issues:`);
|
|
const sortedByFile = Object.entries(stats.byFile).sort((a, b) => b[1] - a[1]);
|
|
for (let i = 0; i < Math.min(topN, sortedByFile.length); i++) {
|
|
const [file, count] = sortedByFile[i];
|
|
console.log(` ${path.basename(file)}: ${count}`);
|
|
}
|
|
}
|
|
|
|
function printGroupedDiagnostics(
|
|
groups: Record<string, LintDiagnostic[]>,
|
|
groupBy: "code" | "file",
|
|
limit?: number,
|
|
) {
|
|
const sortedEntries = Object.entries(groups).sort(
|
|
(a, b) => b[1].length - a[1].length,
|
|
);
|
|
|
|
const entriesToShow = limit ? sortedEntries.slice(0, limit) : sortedEntries;
|
|
|
|
for (const [key, diagnostics] of entriesToShow) {
|
|
console.log(
|
|
`\n${groupBy.toUpperCase()}: ${key} (${diagnostics.length} issues)`,
|
|
);
|
|
// Show first 5 issues for each group to avoid overwhelming output
|
|
const issuesToShow = Math.min(5, diagnostics.length);
|
|
for (let i = 0; i < issuesToShow; i++) {
|
|
const diag = diagnostics[i];
|
|
console.log(
|
|
` ${path.basename(diag.filename)}:${diag.range.start.line + 1}:${
|
|
diag.range.start.col + 1
|
|
} - ${diag.message}`,
|
|
);
|
|
}
|
|
if (diagnostics.length > issuesToShow) {
|
|
console.log(` ... and ${diagnostics.length - issuesToShow} more issues`);
|
|
}
|
|
}
|
|
|
|
if (limit && sortedEntries.length > limit) {
|
|
console.log(`\n... and ${sortedEntries.length - limit} more groups`);
|
|
}
|
|
}
|
|
|
|
async function runDenoLint(): Promise<LintResult> {
|
|
const command = new Deno.Command(Deno.execPath(), {
|
|
args: ["lint", "--json"],
|
|
stdout: "piped",
|
|
stderr: "piped",
|
|
});
|
|
|
|
const { code, stdout, stderr } = await command.output();
|
|
|
|
if (code !== 0 && code !== 1) { // Deno lint returns 1 when there are lint issues
|
|
const errorOutput = new TextDecoder().decode(stderr);
|
|
throw new Error(`Lint command failed:\n${errorOutput}`);
|
|
}
|
|
|
|
const output = new TextDecoder().decode(stdout);
|
|
return JSON.parse(output);
|
|
}
|
|
|
|
async function main() {
|
|
const args = parse(Deno.args, {
|
|
alias: {
|
|
f: "file",
|
|
c: "code",
|
|
g: "group",
|
|
h: "help",
|
|
s: "stats",
|
|
l: "limit",
|
|
},
|
|
string: ["file", "code", "group"],
|
|
boolean: ["help", "stats"],
|
|
default: { limit: 0 }, // 0 means no limit
|
|
});
|
|
|
|
if (args.help) {
|
|
console.log(`
|
|
Usage: deno run analyze_lint.ts [options] [lint-output.json]
|
|
|
|
Options:
|
|
-f, --file <pattern> Filter by file path pattern (regex, can be used multiple times)
|
|
-c, --code <codes> Filter by comma-separated lint codes
|
|
-g, --group <type> Group by "code" or "file"
|
|
-s, --stats Show statistics summary
|
|
-l, --limit <number> Limit number of groups to display when grouping
|
|
--help Show this help message
|
|
|
|
Examples:
|
|
deno run analyze_lint.ts -c no-unused-vars
|
|
deno run analyze_lint.ts -f ".*\\.ts" -f ".*\\.tsx" -g file
|
|
deno run analyze_lint.ts --code=no-explicit-any,verbatim-module-syntax
|
|
deno run analyze_lint.ts --stats
|
|
deno run analyze_lint.ts --group code --limit 5
|
|
`);
|
|
return;
|
|
}
|
|
|
|
let lintResult: LintResult;
|
|
|
|
// Read from file or run lint command
|
|
if (args._.length > 0) {
|
|
const filePath = String(args._[0]);
|
|
const content = await Deno.readTextFile(filePath);
|
|
lintResult = JSON.parse(content);
|
|
} else {
|
|
lintResult = await runDenoLint();
|
|
}
|
|
|
|
// Apply filters
|
|
const filterOptions: FilterOptions = {};
|
|
if (args.code) {
|
|
filterOptions.codes = args.code.split(",").map((c) => c.trim());
|
|
}
|
|
if (args.file) {
|
|
// Handle multiple file patterns
|
|
filterOptions.files = Array.isArray(args.file) ? args.file : [args.file];
|
|
}
|
|
|
|
const filteredDiagnostics = filterDiagnostics(
|
|
lintResult.diagnostics,
|
|
filterOptions,
|
|
);
|
|
|
|
// Show statistics if requested
|
|
if (args.stats) {
|
|
const stats = calculateStats(filteredDiagnostics);
|
|
printStats(stats);
|
|
}
|
|
|
|
// Group or show all diagnostics
|
|
if (args.group) {
|
|
const groups = groupDiagnostics(
|
|
filteredDiagnostics,
|
|
args.group as "code" | "file",
|
|
);
|
|
printGroupedDiagnostics(
|
|
groups,
|
|
args.group as "code" | "file",
|
|
(args.limit as number) || undefined,
|
|
);
|
|
} else if (!args.stats) {
|
|
// Only show JSON output if neither stats nor grouping is requested
|
|
console.log(JSON.stringify({ diagnostics: filteredDiagnostics }, null, 2));
|
|
}
|
|
|
|
if (!args.stats) {
|
|
console.log(
|
|
`\nFound ${filteredDiagnostics.length} issues matching criteria`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
main().catch(console.error);
|
|
}
|