@ts-for-gir/cli
Version:
TypeScript type definition generator for GObject introspection GIR files
345 lines (288 loc) ⢠10.7 kB
text/typescript
/**
* 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,
};