UNPKG

@ts-for-gir/cli

Version:

TypeScript type definition generator for GObject introspection GIR files

345 lines (288 loc) • 10.7 kB
/** * Everything you need for the `ts-for-gir analyze` command is located here */ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { APP_NAME, Logger } from "@ts-for-gir/lib"; import type { ProblemEntry } from "@ts-for-gir/reporter"; import { analyzeOptions } from "../config.ts"; import type { AnalyzeCommandArgs, ReportData } from "../types/index.ts"; import { createBuilder } from "./command-builder.ts"; const command = "analyze [options]"; const description = "Analyze report files generated by ts-for-gir reporter"; const examples: ReadonlyArray<[string, string?]> = [ [`${APP_NAME} analyze -f ./ts-for-gir-report.json`, "Show summary statistics of the report"], [`${APP_NAME} analyze -f ./ts-for-gir-report.json --summary`, "Show only summary statistics"], [`${APP_NAME} analyze -f ./ts-for-gir-report.json --severity error critical`, "Show only errors and critical issues"], [ `${APP_NAME} analyze -f ./ts-for-gir-report.json --category type_resolution --detailed`, "Show detailed type resolution problems", ], [`${APP_NAME} analyze -f ./ts-for-gir-report.json --namespace GLib --top 5`, "Show top 5 problems in GLib namespace"], [ `${APP_NAME} analyze -f ./ts-for-gir-report.json --type time_t --export ./time_t_issues.json`, "Export all time_t related issues", ], [ `${APP_NAME} analyze -f ./ts-for-gir-report.json --search "Unable to resolve" --format csv`, "Search for resolution failures and export as CSV", ], ]; const builder = createBuilder<AnalyzeCommandArgs>(analyzeOptions, examples); const parseReportDate = (dateValue: string | Date): Date => { return typeof dateValue === "string" ? new Date(dateValue) : dateValue; }; const loadReportFile = (filePath: string): ReportData => { if (!existsSync(filePath)) { throw new Error(`Report file not found: ${filePath}`); } try { const content = readFileSync(filePath, "utf-8"); const report = JSON.parse(content) as ReportData; // Convert string dates to Date objects report.metadata.generatedAt = parseReportDate(report.metadata.generatedAt); report.statistics.startTime = parseReportDate(report.statistics.startTime); if (report.statistics.endTime) { report.statistics.endTime = parseReportDate(report.statistics.endTime); } // Convert problem timestamps report.problems = report.problems.map((problem) => ({ ...problem, timestamp: parseReportDate(problem.timestamp), })); return report; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; throw new Error(`Failed to parse report file: ${errorMessage}`); } }; const filterProblems = (problems: ProblemEntry[], args: AnalyzeCommandArgs): ProblemEntry[] => { let filtered = [...problems]; // Filter by severity if (args.severity?.length) { filtered = filtered.filter((p) => args.severity?.includes(p.severity)); } // Filter by category if (args.category?.length) { filtered = filtered.filter((p) => args.category?.includes(p.category)); } // Filter by namespace if (args.namespace?.length) { filtered = filtered.filter((p) => args.namespace?.some((ns) => p.location?.includes(ns) || p.module?.includes(ns) || p.metadata?.namespace === ns), ); } // Filter by type name if (args.type?.length) { filtered = filtered.filter((p) => Boolean(p.typeName && args.type?.includes(p.typeName))); } // Search filter if (args.search) { const searchLower = args.search.toLowerCase(); filtered = filtered.filter( (p) => p.message.toLowerCase().includes(searchLower) || p.details?.toLowerCase().includes(searchLower) || p.typeName?.toLowerCase().includes(searchLower), ); } // Time range filters if (args.since) { const sinceDate = new Date(args.since); filtered = filtered.filter((p) => p.timestamp >= sinceDate); } if (args.until) { const untilDate = new Date(args.until); filtered = filtered.filter((p) => p.timestamp <= untilDate); } return filtered; }; const displaySummary = (report: ReportData, args: AnalyzeCommandArgs): void => { const { statistics } = report; console.log("šŸ“Š Report Summary\n"); // Basic info console.log(`Generated: ${report.metadata.generatedAt}`); console.log(`Version: ${report.metadata.version}`); console.log(`Total Problems: ${statistics.totalProblems}`); if (statistics.durationMs) { console.log(`Generation Duration: ${(statistics.durationMs / 1000).toFixed(2)}s`); } // Problems by severity console.log("\nšŸ”“ Problems by Severity:"); Object.entries(statistics.bySeverity) .filter(([, count]) => count > 0) .sort(([, a], [, b]) => b - a) .forEach(([severity, count]) => { console.log(` ${severity}: ${count}`); }); // Problems by category console.log("\nšŸ“‚ Problems by Category:"); Object.entries(statistics.byCategory) .filter(([, count]) => count > 0) .sort(([, a], [, b]) => b - a) .forEach(([category, count]) => { console.log(` ${category}: ${count}`); }); // Top problematic namespaces if (statistics.typeStatistics.problematicNamespaces.length > 0) { console.log("\nšŸ¢ Most Problematic Namespaces:"); const topCount = args.top ?? 10; statistics.typeStatistics.problematicNamespaces.slice(0, topCount).forEach((ns) => { console.log(` ${ns.namespace}: ${ns.problems} problems`); if (args.detailed) { const typesList = ns.types.slice(0, 5).join(", "); const moreTypes = ns.types.length > 5 ? "..." : ""; console.log(` Types: ${typesList}${moreTypes}`); } }); } // Common unresolved types if (statistics.typeStatistics.commonUnresolvedTypes.length > 0) { console.log("\nšŸ” Most Common Unresolved Types:"); const topCount = args.top ?? 10; statistics.typeStatistics.commonUnresolvedTypes.slice(0, topCount).forEach((type) => { console.log(` ${type.type}: ${type.count} occurrences`); if (args.detailed) { const namespacesList = type.namespaces.slice(0, 3).join(", "); const moreNamespaces = type.namespaces.length > 3 ? "..." : ""; console.log(` Namespaces: ${namespacesList}${moreNamespaces}`); } }); } }; const displayProblems = (problems: ProblemEntry[], args: AnalyzeCommandArgs): void => { if (problems.length === 0) { console.log("No problems match the specified filters."); return; } console.log(`\nšŸ” Found ${problems.length} matching problems:\n`); problems.forEach((problem, index) => { const message = `${index + 1}. [${problem.severity.toUpperCase()}] ${problem.message}`; console.log(message); if (args.detailed) { console.log(` ID: ${problem.id}`); console.log(` Category: ${problem.category}`); console.log(` Module: ${problem.module}`); if (problem.typeName) { console.log(` Type: ${problem.typeName}`); } if (problem.location) { console.log(` Location: ${problem.location}`); } if (problem.details) { console.log(` Details: ${problem.details}`); } console.log(` Timestamp: ${problem.timestamp}`); if (problem.metadata && Object.keys(problem.metadata).length > 0) { console.log(` Metadata: ${JSON.stringify(problem.metadata)}`); } } else if (problem.typeName) { const location = problem.location ?? "unknown"; console.log(` Type: ${problem.typeName} | Location: ${location}`); } if (index < problems.length - 1) { console.log(""); } }); }; const formatAsTable = (problems: ProblemEntry[]): string => { if (problems.length === 0) { return "No problems found."; } const headers = ["Severity", "Category", "Module", "Type", "Message"]; const rows = problems.map((p) => [ p.severity, p.category, p.module ?? "", p.typeName ?? "", p.message.length > 50 ? `${p.message.substring(0, 47)}...` : p.message, ]); const columnWidths = headers.map((header, i) => Math.max(header.length, ...rows.map((row) => row[i].length))); const separator = columnWidths.map((w) => "-".repeat(w)).join(" | "); const headerRow = headers.map((h, i) => h.padEnd(columnWidths[i])).join(" | "); const dataRows = rows.map((row) => row.map((cell, i) => cell.padEnd(columnWidths[i])).join(" | ")); return [headerRow, separator, ...dataRows].join("\n"); }; const formatAsCsv = (problems: ProblemEntry[]): string => { const headers = ["id", "severity", "category", "module", "typeName", "location", "message", "details", "timestamp"]; const rows = problems.map((p) => [ p.id, p.severity, p.category, p.module ?? "", p.typeName ?? "", p.location ?? "", `"${p.message.replace(/"/g, '""')}"`, `"${(p.details ?? "").replace(/"/g, '""')}"`, p.timestamp.toISOString(), ]); return [headers.join(","), ...rows.map((row) => row.join(","))].join("\n"); }; const exportResults = (problems: ProblemEntry[], filePath: string, format: string, logger: Logger): void => { let content: string; switch (format) { case "json": { content = JSON.stringify(problems, null, 2); break; } case "csv": { content = formatAsCsv(problems); break; } case "table": { content = formatAsTable(problems); break; } default: { throw new Error(`Unsupported export format: ${format}`); } } writeFileSync(filePath, content, "utf-8"); logger.success(`Results exported to: ${filePath}`); }; const handler = async (args: AnalyzeCommandArgs): Promise<void> => { const logger = new Logger(args.verbose ?? false, "AnalyzeCommand"); try { // Load and parse report file const report = loadReportFile(args.reportFile); if (args.verbose) { logger.info(`Loaded report with ${report.problems.length} problems`); } // Show summary if requested or if no specific filters are applied const hasFilters = Boolean(args.severity || args.category || args.namespace || args.type || args.search); if (args.summary || !hasFilters) { displaySummary(report, args); } // If summary-only mode, stop here if (args.summary) { return; } // Filter problems based on criteria const filteredProblems = filterProblems(report.problems, args); // Display filtered results if (hasFilters || args.detailed) { displayProblems(filteredProblems, args); } // Export results if requested if (args.export) { const format = args.format ?? "json"; exportResults(filteredProblems, args.export, format, logger); } // Show filter summary if filters were applied if (hasFilters && !args.summary) { console.log( `\nšŸ“‹ Filter Summary: Showing ${filteredProblems.length} of ${report.problems.length} total problems`, ); } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; logger.error(`Analysis failed: ${errorMessage}`); process.exit(1); } }; export const analyze = { command, description, builder, handler, examples, };