feat: add architecture docs, fix code issues from review, add analyze_lint script
Architecture docs (docs/architecture/): - overview.md: package purpose, exports, terminology, design decisions, gaps - metagraph.md: core graph model, schema types, SchemaBuilder, validation - sqlite-host.md: SQLite tables, common columns, relations, concurrency model - encrypted-data.md: encrypted data as a node type, AES-256-GCM crypto utility design Code fixes from architecture review: - Remove ConfigSchema duplication in graphTypes.ts (import GraphConfig from types.ts) - Add missing SelectNodeSchema/SelectNode to nodes.ts - Fix InsertEdge.key to be Optional (match nullable DB column) - Replace TypeScript enums with as const objects (GRAPH_STATUS, GRAPH_BASE_TYPE) - Add verbatim-module-syntax to lint exclusions (TypeBox false positive) - Add @std/flags and @std/path to deno.json imports Infrastructure: - Add scripts/analyze_lint.ts from @ade for grouped lint analysis - Add deno task lint:analyze - Update AGENTS.md with architecture doc references, enum convention, crypto todo
This commit is contained in:
249
scripts/analyze_lint.ts
Normal file
249
scripts/analyze_lint.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user