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; byFile: Record; 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 { const groups: Record = {}; 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 = {}; const byFile: Record = {}; 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, 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 { 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 Filter by file path pattern (regex, can be used multiple times) -c, --code Filter by comma-separated lint codes -g, --group Group by "code" or "file" -s, --stats Show statistics summary -l, --limit 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); }