from abc import ABC, abstractmethod from rich.console import Console from rich.panel import Panel from rich.style import Style from rich.table import Table from rich.text import Text from ..core import Issue, IssueCategory, IssueSeverity, ReviewResult class BaseFormatter(ABC): @abstractmethod def format(self, result: ReviewResult) -> str: pass class TerminalFormatter(BaseFormatter): def __init__(self, theme: str = "auto", show_line_numbers: bool = True): self.console = Console() self.show_line_numbers = show_line_numbers self.use_colors = theme != "dark" if theme == "auto" else theme == "dark" def _get_severity_style(self, severity: IssueSeverity) -> Style: styles = { IssueSeverity.CRITICAL: Style(color="red", bold=True), IssueSeverity.WARNING: Style(color="yellow"), IssueSeverity.INFO: Style(color="blue"), } return styles.get(severity, Style()) def _get_category_icon(self, category: IssueCategory) -> str: icons = { IssueCategory.BUG: "[BUG]", IssueCategory.SECURITY: "[SECURITY]", IssueCategory.STYLE: "[STYLE]", IssueCategory.PERFORMANCE: "[PERF]", IssueCategory.DOCUMENTATION: "[DOC]", } return icons.get(category, "") def _format_issue(self, issue: Issue) -> Text: text = Text() text.append(f"{issue.file}:{issue.line} ", style="dim") text.append(f"[{issue.severity.value.upper()}] ", self._get_severity_style(issue.severity)) text.append(f"{self._get_category_icon(issue.category)} ") text.append(issue.message) if issue.suggestion: text.append("\n Suggestion: ", style="dim") text.append(issue.suggestion) return text def format(self, result: ReviewResult) -> str: output: list[Panel | Table] = [] if result.error: output.append(Panel( f"[red]Error: {result.error}[/red]", title="Review Failed", expand=False )) return "\n".join(str(p) for p in output) summary = result.summary summary_panel = Panel( f"[bold]Files Reviewed:[/bold] {summary.files_reviewed}\n" f"[bold]Lines Changed:[/bold] {summary.lines_changed}\n\n" f"[red]Critical:[/red] {summary.critical_count} " f"[yellow]Warnings:[/yellow] {summary.warning_count} " f"[blue]Info:[/blue] {summary.info_count}\n\n" f"[bold]Assessment:[/bold] {summary.overall_assessment}", title="Review Summary", expand=False ) output.append(summary_panel) if result.issues: issues_table = Table(title="Issues Found", show_header=True) issues_table.add_column("File", style="dim") issues_table.add_column("Line", justify="right", style="dim") issues_table.add_column("Severity", width=10) issues_table.add_column("Category", width=12) issues_table.add_column("Message") for issue in result.issues: issues_table.add_row( issue.file, str(issue.line), f"[{issue.severity.value.upper()}]", f"[{issue.category.value.upper()}]", issue.message, style=self._get_severity_style(issue.severity) ) output.append(issues_table) suggestions_panel = Panel( "\n".join( f"[bold]{issue.file}:{issue.line}[/bold]\n" f" {issue.message}\n" + (f" [green]→ {issue.suggestion}[/green]\n" if issue.suggestion else "") for issue in result.issues if issue.suggestion ), title="Suggestions", expand=False ) output.append(suggestions_panel) model_info = Panel( f"[bold]Model:[/bold] {result.model_used}\n" f"[bold]Tokens Used:[/bold] {result.tokens_used}\n" f"[bold]Mode:[/bold] {result.review_mode}", title="Review Info", expand=False ) output.append(model_info) return "\n".join(str(o) for o in output) class JSONFormatter(BaseFormatter): def format(self, result: ReviewResult) -> str: return result.to_json() class MarkdownFormatter(BaseFormatter): def format(self, result: ReviewResult) -> str: return result.to_markdown() def get_formatter(format_type: str = "terminal", **kwargs) -> BaseFormatter: formatters: dict[str, type[BaseFormatter]] = { "terminal": TerminalFormatter, "json": JSONFormatter, "markdown": MarkdownFormatter, } formatter_class = formatters.get(format_type, TerminalFormatter) return formatter_class(**kwargs) # type: ignore[arg-type]