diff --git a/vibeguard/reports/generator.py b/vibeguard/reports/generator.py new file mode 100644 index 0000000..4a12225 --- /dev/null +++ b/vibeguard/reports/generator.py @@ -0,0 +1,174 @@ +"""Report generator for VibeGuard.""" + +import json +from datetime import datetime +from pathlib import Path +from typing import Any + +from jinja2 import Environment, FileSystemLoader + + +class ReportGenerator: + """Generates reports in various formats.""" + + def __init__(self, template_dir: str | None = None) -> None: + """Initialize report generator with templates.""" + self.template_dir = template_dir or str( + Path(__file__).parent.parent / "templates" + ) + self._env = Environment( + loader=FileSystemLoader(self.template_dir), + autoescape=True, + ) + + def generate_json( + self, issues: list[dict[str, Any]], output_path: str + ) -> None: + """Generate JSON report.""" + report = { + "version": "0.1.0", + "timestamp": datetime.utcnow().isoformat(), + "summary": self._generate_summary(issues), + "issues": issues, + } + + with open(output_path, "w", encoding="utf-8") as f: + json.dump(report, f, indent=2) + + def load_json(self, input_path: str) -> list[dict[str, Any]]: + """Load issues from JSON file.""" + with open(input_path, "r", encoding="utf-8") as f: + data = json.load(f) + return data.get("issues", []) + + def generate_html( + self, + issues: list[dict[str, Any]], + output_path: str, + summary: dict[str, int] | None = None, + ) -> None: + """Generate HTML report.""" + if summary is None: + summary = self._generate_summary(issues) + + template = self._env.get_template("report.html") + html_content = template.render( + title="VibeGuard Analysis Report", + timestamp=datetime.utcnow().isoformat(), + summary=summary, + issues=issues, + total_issues=len(issues), + ) + + with open(output_path, "w", encoding="utf-8") as f: + f.write(html_content) + + def generate_sarif( + self, issues: list[dict[str, Any]], output_path: str + ) -> None: + """Generate SARIF report for GitHub Code Scanning.""" + sarif = { + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "VibeGuard", + "version": "0.1.0", + "informationUri": "https://github.com/vibeguard/vibeguard", + "rules": self._generate_sarif_rules(issues), + } + }, + "results": self._generate_sarif_results(issues), + } + ], + } + + with open(output_path, "w", encoding="utf-8") as f: + json.dump(sarif, f, indent=2) + + def _generate_summary(self, issues: list[dict[str, Any]]) -> dict[str, int]: + """Generate summary statistics.""" + summary: dict[str, int] = { + "critical": 0, + "error": 0, + "warning": 0, + "info": 0, + "total": len(issues), + } + + for issue in issues: + severity = issue.get("severity", "info") + summary[severity] = summary.get(severity, 0) + 1 + + return summary + + def _generate_sarif_rules( + self, issues: list[dict[str, Any]] + ) -> list[dict[str, Any]]: + """Generate SARIF rule definitions.""" + rules: dict[str, dict[str, Any]] = {} + + for issue in issues: + rule_id = issue.get("pattern", "UNKNOWN") + if rule_id not in rules: + rules[rule_id] = { + "id": rule_id, + "name": rule_id, + "shortDescription": { + "text": issue.get("message", "Unknown issue") + }, + "fullDescription": { + "text": issue.get("suggestion", "") + }, + "defaultConfiguration": { + "level": self._severity_to_sarif_level( + issue.get("severity", "warning") + ) + }, + } + + return list(rules.values()) + + def _generate_sarif_results( + self, issues: list[dict[str, Any]] + ) -> list[dict[str, Any]]: + """Generate SARIF result objects.""" + results: list[dict[str, Any]] = [] + + for issue in issues: + result = { + "ruleId": issue.get("pattern", "UNKNOWN"), + "message": { + "text": issue.get("message", "") + }, + "level": self._severity_to_sarif_level( + issue.get("severity", "warning") + ), + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": issue.get("file", "") + }, + "region": { + "startLine": issue.get("line", 1), + }, + } + } + ], + } + results.append(result) + + return results + + def _severity_to_sarif_level(self, severity: str) -> str: + """Convert VibeGuard severity to SARIF level.""" + mapping = { + "critical": "error", + "error": "error", + "warning": "warning", + "info": "note", + } + return mapping.get(severity, "warning")