Created
August 27, 2025 09:35
-
-
Save alkimiadev/1f8f23c90c808825951b90b7757d952a to your computer and use it in GitHub Desktop.
Useful script for analyzing deno lint output. It includes utilities for grouping by code, file, search patterns, and so on.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // deno_package/scripts/analyze_lint.ts | |
| import { parse } from "https://deno.land/std@0.224.0/flags/mod.ts"; | |
| import * as path from "https://deno.land/std@0.224.0/path/mod.ts"; | |
| 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"], | |
| number: ["limit"], | |
| 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 || 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); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment