@ts-for-gir/reporter
Version:
Problem reporting and comprehensive generation analysis for ts-for-gir
569 lines (484 loc) ⢠16.8 kB
text/typescript
/**
* Console Reporter - A concrete implementation of ReporterBase
* that provides console logging and comprehensive problem tracking
*/
import { writeFile } from "node:fs/promises";
import { blue, gray, green, red, yellow, yellowBright } from "colorette";
import { PACKAGE_VERSION } from "./constants.ts";
import { analyzeError, analyzeWarning } from "./message-analyzer.ts";
import { ReporterBase } from "./reporter-base.ts";
import type { GenerationReport, ProblemEntry, ReporterConfig, ReportStatistics } from "./types/index.ts";
import { ProblemCategory, ProblemSeverity } from "./types/index.ts";
/**
* Console Reporter implementation with full logging capabilities
*/
export class ConsoleReporter extends ReporterBase {
constructor(config: ReporterConfig);
constructor(verbose: boolean, moduleName: string, reporterEnabled?: boolean, outputPath?: string);
constructor(
configOrVerbose: ReporterConfig | boolean,
moduleName?: string,
reporterEnabled = false,
outputPath?: string,
) {
let config: ReporterConfig;
if (typeof configOrVerbose === "boolean") {
// Legacy Logger constructor compatibility
config = {
enabled: reporterEnabled,
verbose: configOrVerbose,
moduleName: moduleName || "Unknown",
outputPath,
};
} else {
config = configOrVerbose;
}
super(config);
}
// === Logger compatibility methods ===
public log(...args: unknown[]): void {
if (!this.config.verbose) {
return;
}
console.log(...args);
}
public dir(...args: unknown[]): void {
if (!this.config.verbose) {
return;
}
args.forEach((arg) => {
console.dir(arg);
});
}
public info(txt: string, ...args: unknown[]): void {
if (!this.config.verbose) {
return;
}
console.info(blue(txt), ...args);
}
public warn(txt: string, ...args: unknown[]): void {
// Analyze message for specific problem types
if (this.config.enabled) {
const analyzed = analyzeWarning(txt, args);
if (analyzed) {
this.addProblem(
analyzed.severity,
analyzed.category,
txt,
analyzed.details,
analyzed.typeName,
analyzed.namespace || this.config.moduleName,
analyzed.metadata,
);
}
}
if (!this.config.verbose) {
return;
}
const formattedTxt = this.prependInfo(txt, "WARN:");
console.warn(yellow(formattedTxt), ...args);
}
public debug(txt: string, ...args: unknown[]): void {
if (!this.config.verbose) {
return;
}
const formattedTxt = this.prependInfo(txt, "DEBUG:");
console.debug(yellowBright(formattedTxt), ...args);
}
public error(txt: string, ...args: unknown[]): void {
// Analyze message for specific problem types
if (this.config.enabled) {
const analyzed = analyzeError(txt, args);
if (analyzed) {
this.addProblem(
analyzed.severity,
analyzed.category,
txt,
analyzed.details,
analyzed.typeName,
analyzed.namespace || this.config.moduleName,
analyzed.metadata,
);
}
}
const formattedTxt = this.prependInfo(txt, "ERROR:");
this.danger(formattedTxt, ...args);
}
public success(txt: string, ...args: unknown[]): void {
if (!this.config.verbose) {
return;
}
this.log(green(txt), ...args);
}
public danger(txt: string, ...args: unknown[]): void {
console.error(red(txt), ...args);
}
public muted(txt: string, ...args: unknown[]): void {
this.log(gray(txt), ...args);
}
public white(txt: string, ...args: unknown[]): void {
this.log(txt, ...args);
}
public yellow(txt: string, ...args: unknown[]): void {
this.log(yellow(txt), ...args);
}
public gray(txt: string, ...args: unknown[]): void {
this.log(gray(txt), ...args);
}
// === Problem-specific reporting methods ===
public reportTypeResolutionError(typeName: string, namespace: string, message: string, details?: string): void {
this.addProblem(ProblemSeverity.ERROR, ProblemCategory.TYPE_RESOLUTION, message, details, typeName, namespace, {
namespace,
typeName,
});
if (this.config.verbose) {
const txt = this.prependInfo(message, "ERROR:");
console.error(red(txt));
}
}
public reportTypeResolutionWarning(typeName: string, namespace: string, message: string, details?: string): void {
this.addProblem(ProblemSeverity.WARNING, ProblemCategory.TYPE_RESOLUTION, message, details, typeName, namespace, {
namespace,
typeName,
});
if (this.config.verbose) {
const txt = this.prependInfo(message, "WARN:");
console.warn(yellow(txt));
}
}
public reportParsingFailure(itemName: string, itemType: string, namespace: string, error: Error | string): void {
const message = `Failed to parse ${itemType}: ${itemName}`;
const details = error instanceof Error ? error.message : error;
this.addProblem(ProblemSeverity.ERROR, ProblemCategory.PARSING_FAILURE, message, details, itemName, namespace, {
itemType,
namespace,
error: details,
});
if (this.config.verbose) {
const txt = this.prependInfo(message, "ERROR:");
console.error(red(txt), details);
}
}
public reportGenerationFailure(namespace: string, error: Error | string, context?: string): void {
const message = `Failed to generate ${context || "namespace"}: ${namespace}`;
const details = error instanceof Error ? error.message : error;
this.addProblem(ProblemSeverity.ERROR, ProblemCategory.GENERATION_FAILURE, message, details, undefined, namespace, {
namespace,
context,
error: details,
});
if (this.config.verbose) {
const txt = this.prependInfo(message, "ERROR:");
console.error(red(txt), details);
}
}
public reportTypeConflict(conflictType: string, elementName: string, namespace: string, details?: string): void {
const message = `Type conflict (${conflictType}): ${elementName}`;
this.addProblem(ProblemSeverity.WARNING, ProblemCategory.TYPE_CONFLICT, message, details, elementName, namespace, {
conflictType,
namespace,
});
if (this.config.verbose) {
const txt = this.prependInfo(message, "WARN:");
console.warn(yellow(txt), details || "");
}
}
public reportDependencyIssue(dependencyName: string, issue: string, details?: string): void {
const message = `Dependency issue: ${dependencyName} - ${issue}`;
this.addProblem(
ProblemSeverity.WARNING,
ProblemCategory.DEPENDENCY_ISSUE,
message,
details,
dependencyName,
this.config.moduleName,
{ dependencyName, issue },
);
if (this.config.verbose) {
this.warn(message, details);
}
}
// === Report generation methods ===
private generateStatistics(): ReportStatistics {
const bySeverity = Object.values(ProblemSeverity).reduce(
(acc, severity) => {
acc[severity] = 0;
return acc;
},
{} as Record<ProblemSeverity, number>,
);
const byCategory = Object.values(ProblemCategory).reduce(
(acc, category) => {
acc[category] = 0;
return acc;
},
{} as Record<ProblemCategory, number>,
);
const byModule: Record<string, number> = {};
// Type-specific tracking
const unresolvedTypes: Record<string, { count: number; namespaces: Set<string> }> = {};
const typeConflicts: Record<string, { count: number; examples: Set<string> }> = {};
const namespaceProblems: Record<string, { count: number; types: Set<string> }> = {};
for (const problem of this.problems) {
bySeverity[problem.severity]++;
byCategory[problem.category]++;
byModule[problem.module] = (byModule[problem.module] || 0) + 1;
// Track type resolution problems
if (problem.category === ProblemCategory.TYPE_RESOLUTION && problem.typeName) {
if (!unresolvedTypes[problem.typeName]) {
unresolvedTypes[problem.typeName] = { count: 0, namespaces: new Set() };
}
unresolvedTypes[problem.typeName].count++;
if (problem.location) {
unresolvedTypes[problem.typeName].namespaces.add(problem.location);
}
// Track namespace problems
if (problem.location) {
if (!namespaceProblems[problem.location]) {
namespaceProblems[problem.location] = { count: 0, types: new Set() };
}
namespaceProblems[problem.location].count++;
namespaceProblems[problem.location].types.add(problem.typeName);
}
}
// Track type conflicts
if (problem.category === ProblemCategory.TYPE_CONFLICT && problem.metadata?.conflictType) {
const conflictType = problem.metadata.conflictType as string;
if (!typeConflicts[conflictType]) {
typeConflicts[conflictType] = { count: 0, examples: new Set() };
}
typeConflicts[conflictType].count++;
if (problem.typeName) {
typeConflicts[conflictType].examples.add(problem.typeName);
}
}
}
// Convert to arrays and sort
const commonUnresolvedTypes = Object.entries(unresolvedTypes)
.map(([type, data]) => ({
type,
count: data.count,
namespaces: Array.from(data.namespaces),
}))
.sort((a, b) => b.count - a.count)
.slice(0, 20);
const commonTypeConflicts = Object.entries(typeConflicts)
.map(([conflictType, data]) => ({
conflictType,
count: data.count,
examples: Array.from(data.examples).slice(0, 5),
}))
.sort((a, b) => b.count - a.count);
const problematicNamespaces = Object.entries(namespaceProblems)
.map(([namespace, data]) => ({
namespace,
problems: data.count,
types: Array.from(data.types).slice(0, 10),
}))
.sort((a, b) => b.problems - a.problems)
.slice(0, 10);
const mostProblematicModules = Object.entries(byModule)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([module, count]) => ({ module, count }));
const endTime = new Date();
const durationMs = endTime.getTime() - this.startTime.getTime();
return {
bySeverity,
byCategory,
byModule,
totalProblems: this.problems.length,
mostProblematicModules,
typeStatistics: {
commonUnresolvedTypes,
commonTypeConflicts,
problematicNamespaces,
},
startTime: this.startTime,
endTime,
durationMs,
};
}
private generateSummary(statistics: ReportStatistics): GenerationReport["summary"] {
const { bySeverity, byCategory, totalProblems } = statistics;
let status: "success" | "partial" = "success";
if (bySeverity[ProblemSeverity.ERROR] > 0 || bySeverity[ProblemSeverity.WARNING] > 20) {
status = "partial";
}
const keyIssues: string[] = [];
const recommendations: string[] = [];
// Analyze key issues
if (byCategory[ProblemCategory.TYPE_RESOLUTION] > 0) {
keyIssues.push(`${byCategory[ProblemCategory.TYPE_RESOLUTION]} type resolution issues detected`);
recommendations.push("Review GIR files for missing or incorrect type definitions");
}
if (byCategory[ProblemCategory.PARSING_FAILURE] > 0) {
keyIssues.push(`${byCategory[ProblemCategory.PARSING_FAILURE]} parsing failures occurred`);
recommendations.push("Check GIR file syntax and ensure proper introspection data");
}
if (byCategory[ProblemCategory.GENERATION_FAILURE] > 0) {
keyIssues.push(`${byCategory[ProblemCategory.GENERATION_FAILURE]} generation failures encountered`);
recommendations.push("Review template configuration and output settings");
}
if (byCategory[ProblemCategory.TYPE_CONFLICT] > 5) {
keyIssues.push(`High number of type conflicts (${byCategory[ProblemCategory.TYPE_CONFLICT]})`);
recommendations.push("Consider using ignore patterns or updating GIR files to resolve conflicts");
}
if (keyIssues.length === 0 && totalProblems > 0) {
keyIssues.push(`${totalProblems} minor issues detected`);
}
if (recommendations.length === 0 && totalProblems > 0) {
recommendations.push("Review detailed problem list for specific improvement opportunities");
}
return {
status,
keyIssues,
recommendations,
};
}
public generateReport(): GenerationReport {
const statistics = this.generateStatistics();
const summary = this.generateSummary(statistics);
const problemsByCategory = Object.values(ProblemCategory).reduce(
(acc, category) => {
acc[category] = [];
return acc;
},
{} as Record<ProblemCategory, ProblemEntry[]>,
);
for (const problem of this.problems) {
problemsByCategory[problem.category].push(problem);
}
return {
metadata: {
version: PACKAGE_VERSION,
generatedAt: new Date(),
},
statistics,
problems: [...this.problems],
problemsByCategory,
summary,
};
}
public async saveReport(outputPath?: string): Promise<void> {
if (!this.config.enabled) {
return;
}
const report = this.generateReport();
const filePath = outputPath || this.config.outputPath || "ts-for-gir-report.json";
try {
await writeFile(filePath, JSON.stringify(report, null, 2), "utf-8");
if (this.config.verbose) {
this.success(`Report saved to: ${filePath}`);
}
} catch (error) {
this.danger(`Failed to save report to ${filePath}: ${error}`);
}
}
public printSummary(): void {
if (!this.config.enabled) {
return;
}
const report = this.generateReport();
const { statistics, summary } = report;
console.log(`\n${"=".repeat(50)}`);
console.log("š GENERATION REPORT SUMMARY");
console.log("=".repeat(50));
// Status
const statusColor = summary.status === "success" ? green : summary.status === "partial" ? yellow : red;
console.log(`Status: ${statusColor(summary.status.toUpperCase())}`);
// Statistics
console.log(`\nš Statistics:`);
console.log(` Total Problems: ${statistics.totalProblems}`);
console.log(` Duration: ${Math.round((statistics.durationMs || 0) / 1000)}s`);
if (statistics.totalProblems > 0) {
console.log(`\nš By Severity:`);
for (const [severity, count] of Object.entries(statistics.bySeverity)) {
if (count > 0) {
const color = severity === "error" || severity === "critical" ? red : severity === "warning" ? yellow : blue;
console.log(` ${color(severity)}: ${count}`);
}
}
console.log(`\nš By Category:`);
for (const [category, count] of Object.entries(statistics.byCategory)) {
if (count > 0) {
console.log(` ${category.replace(/_/g, " ")}: ${count}`);
}
}
// Type-specific statistics
if (statistics.typeStatistics.commonUnresolvedTypes.length > 0) {
console.log(`\nā Most Common Unresolved Types:`);
statistics.typeStatistics.commonUnresolvedTypes.slice(0, 10).forEach(({ type, count, namespaces }) => {
console.log(` ${red(type)}: ${count} occurrences in ${namespaces.length} namespace(s)`);
if (namespaces.length <= 3) {
console.log(` āā ${gray(namespaces.join(", "))}`);
}
});
}
if (statistics.typeStatistics.commonTypeConflicts.length > 0) {
console.log(`\nāļø Type Conflicts:`);
statistics.typeStatistics.commonTypeConflicts.forEach(({ conflictType, count, examples }) => {
console.log(` ${yellow(conflictType)}: ${count} conflicts`);
if (examples.length > 0) {
console.log(` āā Examples: ${gray(examples.join(", "))}`);
}
});
}
if (statistics.typeStatistics.problematicNamespaces.length > 0) {
console.log(`\nšØ Most Problematic Namespaces:`);
statistics.typeStatistics.problematicNamespaces.slice(0, 5).forEach(({ namespace, problems, types }) => {
console.log(` ${namespace}: ${problems} problems`);
if (types.length > 0) {
const typeList = types.slice(0, 5).join(", ");
const moreTypes = types.length > 5 ? ` and ${types.length - 5} more` : "";
console.log(` āā Types: ${gray(typeList + moreTypes)}`);
}
});
}
if (statistics.mostProblematicModules.length > 0) {
console.log(`\nš¦ Most Problematic Modules:`);
statistics.mostProblematicModules.slice(0, 5).forEach(({ module, count }) => {
console.log(` ${module}: ${count} issues`);
});
}
}
// Key Issues
if (summary.keyIssues.length > 0) {
console.log(`\nā ļø Key Issues:`);
for (const issue of summary.keyIssues) {
console.log(` ⢠${issue}`);
}
}
// Recommendations
if (summary.recommendations.length > 0) {
console.log(`\nš” Recommendations:`);
for (const rec of summary.recommendations) {
console.log(` ⢠${rec}`);
}
}
console.log(`${"=".repeat(50)}\n`);
}
// === Private helper methods ===
private static prepend(txt: string, prepend: string): string {
if (typeof txt === "string") {
txt = `${prepend}${txt}`;
}
return txt;
}
private prependInfo(txt: string, logLevel?: "WARN:" | "ERROR:" | "INFO:" | "DEBUG:"): string {
if (logLevel || this.config.moduleName.length > 0) {
txt = ConsoleReporter.prepend(txt, " ");
}
if (logLevel) {
if (this.config.moduleName.length > 0) {
txt = ConsoleReporter.prepend(txt, ` ${logLevel}`);
} else {
txt = ConsoleReporter.prepend(txt, logLevel);
}
}
if (this.config.moduleName.length > 0) {
txt = ConsoleReporter.prepend(txt, `[${this.config.moduleName}]`);
}
return txt;
}
}