use colored::Colorize;
use schemars::JsonSchema;
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt::{self, Write};
use std::path::PathBuf;
pub const DEFAULT_MIN_FINDING_CONFIDENCE: u8 = 70;
mod colors {
use crate::theme;
use crate::theme::names::tokens;
pub fn accent_primary() -> (u8, u8, u8) {
let c = theme::current().color(tokens::ACCENT_PRIMARY);
(c.r, c.g, c.b)
}
pub fn accent_secondary() -> (u8, u8, u8) {
let c = theme::current().color(tokens::ACCENT_SECONDARY);
(c.r, c.g, c.b)
}
pub fn accent_tertiary() -> (u8, u8, u8) {
let c = theme::current().color(tokens::ACCENT_TERTIARY);
(c.r, c.g, c.b)
}
pub fn warning() -> (u8, u8, u8) {
let c = theme::current().color(tokens::WARNING);
(c.r, c.g, c.b)
}
pub fn error() -> (u8, u8, u8) {
let c = theme::current().color(tokens::ERROR);
(c.r, c.g, c.b)
}
pub fn text_secondary() -> (u8, u8, u8) {
let c = theme::current().color(tokens::TEXT_SECONDARY);
(c.r, c.g, c.b)
}
pub fn text_dim() -> (u8, u8, u8) {
let c = theme::current().color(tokens::TEXT_DIM);
(c.r, c.g, c.b)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
pub struct Review {
#[serde(default)]
pub summary: String,
#[serde(default)]
pub metadata: ReviewMetadata,
#[serde(default)]
pub findings: Vec<Finding>,
#[serde(default)]
pub stats: ReviewStats,
#[serde(default, skip_serializing_if = "is_false")]
#[schemars(skip)]
pub parse_failed: bool,
}
impl Review {
#[must_use]
pub fn from_unstructured(text: &str) -> Self {
Self {
summary: format!(
"**Review parsing failed; raw model output below.**\n\n```text\n{}\n```",
escape_fenced_code(text)
),
metadata: ReviewMetadata::default(),
findings: Vec::new(),
stats: ReviewStats::default(),
parse_failed: true,
}
}
#[must_use]
pub fn raw_content(&self) -> String {
self.to_markdown()
}
#[must_use]
pub fn to_markdown(&self) -> String {
let mut output = String::new();
writeln!(output, "# Code Review").expect("write to string should not fail");
if !self.summary.trim().is_empty() {
writeln!(output, "\n## Summary\n\n{}", self.summary.trim())
.expect("write to string should not fail");
}
if self.parse_failed {
return output;
}
self.render_metadata(&mut output);
let visible_findings = self.visible_findings();
let stats = self.visible_stats();
writeln!(
output,
"\n## Findings\n\nReviewed {} file(s). Found {} issue(s): {} critical, {} high, {} medium, {} low.",
stats.files_reviewed,
stats.findings_count,
stats.critical_count,
stats.high_count,
stats.medium_count,
stats.low_count
)
.expect("write to string should not fail");
if visible_findings.is_empty() {
output.push_str("\nNo blocking issues found.\n");
return output;
}
for severity in [
Severity::Critical,
Severity::High,
Severity::Medium,
Severity::Low,
] {
let findings: Vec<&Finding> = visible_findings
.iter()
.copied()
.filter(|finding| finding.severity == severity)
.collect();
if findings.is_empty() {
continue;
}
writeln!(output, "\n### {severity}").expect("write to string should not fail");
for finding in findings {
writeln!(
output,
"\n- [{severity}] **{} in `{}`**",
finding.title,
finding.location()
)
.expect("write to string should not fail");
writeln!(
output,
" Category: {}. Confidence: {}%.",
finding.category,
finding.confidence_score()
)
.expect("write to string should not fail");
writeln!(output, " {}", finding.body.trim())
.expect("write to string should not fail");
if let Some(fix) = finding
.suggested_fix
.as_deref()
.filter(|fix| !fix.is_empty())
{
writeln!(output, " **Fix**: {}", fix.trim())
.expect("write to string should not fail");
}
if !finding.evidence.is_empty() {
let evidence = finding
.evidence
.iter()
.map(EvidenceRef::label)
.collect::<Vec<_>>()
.join(", ");
writeln!(output, " Evidence: {evidence}")
.expect("write to string should not fail");
}
}
}
output
}
#[must_use]
pub fn format(&self) -> String {
render_markdown_for_terminal(&self.to_markdown())
}
#[must_use]
pub fn effective_stats(&self) -> ReviewStats {
ReviewStats::from_findings(self.stats.files_reviewed, &self.findings)
}
#[must_use]
pub fn visible_findings(&self) -> Vec<&Finding> {
self.visible_findings_at(DEFAULT_MIN_FINDING_CONFIDENCE)
}
#[must_use]
pub fn visible_findings_at(&self, min_confidence: u8) -> Vec<&Finding> {
self.findings
.iter()
.filter(|finding| finding.confidence_score() >= min_confidence)
.collect()
}
#[must_use]
pub fn visible_stats(&self) -> ReviewStats {
self.visible_stats_at(DEFAULT_MIN_FINDING_CONFIDENCE)
}
#[must_use]
pub fn visible_stats_at(&self, min_confidence: u8) -> ReviewStats {
let visible_findings = self.visible_findings_at(min_confidence);
let mut stats = ReviewStats {
files_reviewed: self.stats.files_reviewed,
findings_count: visible_findings.len(),
..ReviewStats::default()
};
for finding in visible_findings {
match finding.severity {
Severity::Critical => stats.critical_count += 1,
Severity::High => stats.high_count += 1,
Severity::Medium => stats.medium_count += 1,
Severity::Low => stats.low_count += 1,
}
}
stats
}
fn render_metadata(&self, output: &mut String) {
if self.metadata.is_empty() {
return;
}
writeln!(output, "\n## Review Coverage").expect("write to string should not fail");
if let Some(risk_level) = self.metadata.risk_level {
writeln!(output, "\nRisk: {risk_level}").expect("write to string should not fail");
}
if let Some(strategy) = trimmed_non_empty(&self.metadata.strategy) {
writeln!(output, "\nStrategy: {strategy}").expect("write to string should not fail");
}
let specialist_passes = self
.metadata
.specialist_passes
.iter()
.filter_map(|pass| trimmed_non_empty(pass))
.collect::<Vec<_>>();
if !specialist_passes.is_empty() {
writeln!(output, "\nSpecialist passes:").expect("write to string should not fail");
for pass in specialist_passes {
writeln!(output, "- {pass}").expect("write to string should not fail");
}
}
let coverage_notes = self
.metadata
.coverage_notes
.iter()
.filter_map(|note| trimmed_non_empty(note))
.collect::<Vec<_>>();
if !coverage_notes.is_empty() {
writeln!(output, "\nCoverage notes:").expect("write to string should not fail");
for note in coverage_notes {
writeln!(output, "- {note}").expect("write to string should not fail");
}
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct ReviewMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub risk_level: Option<RiskLevel>,
#[serde(default, skip_serializing_if = "str_is_blank")]
pub strategy: String,
#[serde(default, skip_serializing_if = "string_vec_is_blank")]
pub specialist_passes: Vec<String>,
#[serde(default, skip_serializing_if = "string_vec_is_blank")]
pub coverage_notes: Vec<String>,
}
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RiskLevel {
Critical,
High,
Medium,
Low,
}
impl RiskLevel {
fn from_model_value(value: &str) -> Option<Self> {
match value.trim().to_lowercase().as_str() {
"critical" => Some(Self::Critical),
"high" => Some(Self::High),
"medium" => Some(Self::Medium),
"low" => Some(Self::Low),
_ => None,
}
}
}
impl<'de> Deserialize<'de> for RiskLevel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::from_model_value(&value).ok_or_else(|| de::Error::custom("invalid risk level"))
}
}
impl fmt::Display for RiskLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Critical => write!(f, "critical"),
Self::High => write!(f, "high"),
Self::Medium => write!(f, "medium"),
Self::Low => write!(f, "low"),
}
}
}
impl ReviewMetadata {
#[must_use]
pub fn is_empty(&self) -> bool {
self.risk_level.is_none()
&& str_is_blank(&self.strategy)
&& string_vec_is_blank(&self.specialist_passes)
&& string_vec_is_blank(&self.coverage_notes)
}
}
fn trimmed_non_empty(value: &str) -> Option<&str> {
let value = value.trim();
(!value.is_empty()).then_some(value)
}
fn str_is_blank(value: &str) -> bool {
value.trim().is_empty()
}
fn string_vec_is_blank(values: &[String]) -> bool {
values.iter().all(|value| str_is_blank(value))
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
pub struct Finding {
pub id: FindingId,
pub severity: Severity,
#[serde(deserialize_with = "deserialize_confidence")]
pub confidence: u8,
pub file: PathBuf,
pub start_line: u32,
pub end_line: u32,
pub category: Category,
pub title: String,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub suggested_fix: Option<String>,
#[serde(default)]
pub evidence: Vec<EvidenceRef>,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_false(value: &bool) -> bool {
!*value
}
fn escape_fenced_code(text: &str) -> String {
text.replace("```", "`\\`\\`")
}
impl Finding {
#[must_use]
pub fn location(&self) -> String {
let file = self.file.display();
let start = self.start_line.min(self.end_line);
let end = self.start_line.max(self.end_line);
if start == end {
format!("{file}:{start}")
} else {
format!("{file}:{start}-{end}")
}
}
#[must_use]
pub fn raw_inline_body(&self) -> String {
let mut body = format!(
"[{}] **{}**\n\nLocation: `{}`\n\nCategory: {}\n\n{}\n\nConfidence: {}%",
self.severity,
self.title,
self.location(),
self.category,
self.body.trim(),
self.confidence_score()
);
if let Some(fix) = self.suggested_fix.as_deref().filter(|fix| !fix.is_empty()) {
write!(body, "\n\n**Fix**: {}", fix.trim()).expect("write to string should not fail");
}
if !self.evidence.is_empty() {
let evidence = self
.evidence
.iter()
.map(EvidenceRef::label)
.collect::<Vec<_>>()
.join(", ");
write!(body, "\n\nEvidence: {evidence}").expect("write to string should not fail");
}
body
}
#[must_use]
pub fn confidence_score(&self) -> u8 {
self.confidence.min(100)
}
}
fn deserialize_confidence<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: Deserializer<'de>,
{
struct ConfidenceVisitor;
impl Visitor<'_> for ConfidenceVisitor {
type Value = u8;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a confidence value as a number, fraction, or numeric string")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(u8::try_from(value.min(100)).unwrap_or(100))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(u8::try_from(value.clamp(0, 100)).unwrap_or_default())
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
confidence_from_float(value).ok_or_else(|| E::custom("confidence must be finite"))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let value = value.trim().trim_end_matches('%');
value
.parse::<f64>()
.ok()
.and_then(confidence_from_float)
.ok_or_else(|| E::custom("confidence string must be numeric"))
}
}
deserializer.deserialize_any(ConfidenceVisitor)
}
fn confidence_from_float(value: f64) -> Option<u8> {
if !value.is_finite() {
return None;
}
let value = if value > 0.0 && value < 1.0 {
value * 100.0
} else {
value
};
let rounded = value.round().clamp(0.0, 100.0);
(0..=100).find(|candidate| (f64::from(*candidate) - rounded).abs() < f64::EPSILON)
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[serde(transparent)]
pub struct FindingId(pub String);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
pub struct EvidenceRef {
pub file: PathBuf,
pub line: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl EvidenceRef {
#[must_use]
pub fn label(&self) -> String {
let file = self.file.display();
let line = match self.end_line {
Some(end_line) if end_line != self.line => format!("{}-{}", self.line, end_line),
_ => self.line.to_string(),
};
match self.note.as_deref().filter(|note| !note.is_empty()) {
Some(note) => format!("{file}:{line} ({note})"),
None => format!("{file}:{line}"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Critical,
High,
Medium,
Low,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Critical => write!(f, "CRITICAL"),
Self::High => write!(f, "HIGH"),
Self::Medium => write!(f, "MEDIUM"),
Self::Low => write!(f, "LOW"),
}
}
}
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Category {
Security,
Performance,
ErrorHandling,
Complexity,
Abstraction,
Duplication,
Testing,
Style,
ApiContract,
Concurrency,
Documentation,
Other,
}
impl Category {
#[must_use]
pub fn from_model_value(value: &str) -> Self {
let normalized: String = value
.trim()
.chars()
.filter(|character| !matches!(*character, '_' | '-' | ' '))
.flat_map(char::to_lowercase)
.collect();
match normalized.as_str() {
"security" => Self::Security,
"performance" => Self::Performance,
"errorhandling" => Self::ErrorHandling,
"complexity" => Self::Complexity,
"abstraction" => Self::Abstraction,
"duplication" => Self::Duplication,
"testing" => Self::Testing,
"style" => Self::Style,
"apicontract" => Self::ApiContract,
"concurrency" => Self::Concurrency,
"documentation" => Self::Documentation,
_ => Self::Other,
}
}
}
impl<'de> Deserialize<'de> for Category {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(Self::from_model_value(&value))
}
}
impl fmt::Display for Category {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Security => write!(f, "security"),
Self::Performance => write!(f, "performance"),
Self::ErrorHandling => write!(f, "error handling"),
Self::Complexity => write!(f, "complexity"),
Self::Abstraction => write!(f, "abstraction"),
Self::Duplication => write!(f, "duplication"),
Self::Testing => write!(f, "testing"),
Self::Style => write!(f, "style"),
Self::ApiContract => write!(f, "API contract"),
Self::Concurrency => write!(f, "concurrency"),
Self::Documentation => write!(f, "documentation"),
Self::Other => write!(f, "other"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct ReviewStats {
#[serde(default)]
pub files_reviewed: usize,
#[serde(default)]
pub findings_count: usize,
#[serde(default)]
pub critical_count: usize,
#[serde(default)]
pub high_count: usize,
#[serde(default)]
pub medium_count: usize,
#[serde(default)]
pub low_count: usize,
}
impl ReviewStats {
#[must_use]
pub fn from_findings(files_reviewed: usize, findings: &[Finding]) -> Self {
let mut stats = Self {
files_reviewed,
findings_count: findings.len(),
..Self::default()
};
for finding in findings {
match finding.severity {
Severity::Critical => stats.critical_count += 1,
Severity::High => stats.high_count += 1,
Severity::Medium => stats.medium_count += 1,
Severity::Low => stats.low_count += 1,
}
}
stats
}
}
#[allow(clippy::too_many_lines)]
#[must_use]
pub fn render_markdown_for_terminal(markdown: &str) -> String {
let mut output = String::new();
let mut in_code_block = false;
let mut code_block_content = String::new();
for line in markdown.lines() {
if line.starts_with("```") {
if in_code_block {
let dim = colors::text_secondary();
for code_line in code_block_content.lines() {
writeln!(output, " {}", code_line.truecolor(dim.0, dim.1, dim.2))
.expect("write to string should not fail");
}
code_block_content.clear();
in_code_block = false;
} else {
in_code_block = true;
}
continue;
}
if in_code_block {
code_block_content.push_str(line);
code_block_content.push('\n');
continue;
}
if let Some(header) = line.strip_prefix("### ") {
let cyan = colors::accent_secondary();
let dim = colors::text_dim();
writeln!(
output,
"\n{} {} {}",
"─".truecolor(cyan.0, cyan.1, cyan.2),
style_header_text(header)
.truecolor(cyan.0, cyan.1, cyan.2)
.bold(),
"─"
.repeat(30usize.saturating_sub(header.len()))
.truecolor(dim.0, dim.1, dim.2)
)
.expect("write to string should not fail");
} else if let Some(header) = line.strip_prefix("## ") {
let purple = colors::accent_primary();
let dim = colors::text_dim();
writeln!(
output,
"\n{} {} {}",
"─".truecolor(purple.0, purple.1, purple.2),
style_header_text(header)
.truecolor(purple.0, purple.1, purple.2)
.bold(),
"─"
.repeat(32usize.saturating_sub(header.len()))
.truecolor(dim.0, dim.1, dim.2)
)
.expect("write to string should not fail");
} else if let Some(header) = line.strip_prefix("# ") {
let purple = colors::accent_primary();
let cyan = colors::accent_secondary();
writeln!(
output,
"{} {} {}",
"━━━".truecolor(purple.0, purple.1, purple.2),
style_header_text(header)
.truecolor(cyan.0, cyan.1, cyan.2)
.bold(),
"━━━".truecolor(purple.0, purple.1, purple.2)
)
.expect("write to string should not fail");
}
else if let Some(content) = line.strip_prefix("- ") {
let coral = colors::accent_tertiary();
let styled = style_line_content(content);
writeln!(
output,
" {} {}",
"•".truecolor(coral.0, coral.1, coral.2),
styled
)
.expect("write to string should not fail");
} else if let Some(content) = line.strip_prefix("* ") {
let coral = colors::accent_tertiary();
let styled = style_line_content(content);
writeln!(
output,
" {} {}",
"•".truecolor(coral.0, coral.1, coral.2),
styled
)
.expect("write to string should not fail");
}
else if line.chars().next().is_some_and(|c| c.is_ascii_digit()) && line.contains(". ") {
if let Some((num, rest)) = line.split_once(". ") {
let coral = colors::accent_tertiary();
let styled = style_line_content(rest);
writeln!(
output,
" {} {}",
format!("{}.", num)
.truecolor(coral.0, coral.1, coral.2)
.bold(),
styled
)
.expect("write to string should not fail");
}
}
else if line.trim().is_empty() {
output.push('\n');
}
else {
let styled = style_line_content(line);
writeln!(output, "{styled}").expect("write to string should not fail");
}
}
output
}
fn style_header_text(text: &str) -> String {
text.to_uppercase()
}
#[allow(clippy::too_many_lines)]
fn style_line_content(content: &str) -> String {
let mut result = String::new();
let mut chars = content.chars().peekable();
let mut current_text = String::new();
let text_color = colors::text_secondary();
let error_color = colors::error();
let warning_color = colors::warning();
let coral_color = colors::accent_tertiary();
let cyan_color = colors::accent_secondary();
while let Some(ch) = chars.next() {
match ch {
'[' => {
if !current_text.is_empty() {
result.push_str(
¤t_text
.truecolor(text_color.0, text_color.1, text_color.2)
.to_string(),
);
current_text.clear();
}
let mut badge = String::new();
for c in chars.by_ref() {
if c == ']' {
break;
}
badge.push(c);
}
let badge_upper = badge.to_uppercase();
let styled_badge = match badge_upper.as_str() {
"CRITICAL" => format!(
"[{}]",
"CRITICAL"
.truecolor(error_color.0, error_color.1, error_color.2)
.bold()
),
"HIGH" => format!(
"[{}]",
"HIGH"
.truecolor(error_color.0, error_color.1, error_color.2)
.bold()
),
"MEDIUM" => format!(
"[{}]",
"MEDIUM"
.truecolor(warning_color.0, warning_color.1, warning_color.2)
.bold()
),
"LOW" => format!(
"[{}]",
"LOW"
.truecolor(coral_color.0, coral_color.1, coral_color.2)
.bold()
),
_ => format!(
"[{}]",
badge.truecolor(cyan_color.0, cyan_color.1, cyan_color.2)
),
};
result.push_str(&styled_badge);
}
'*' if chars.peek() == Some(&'*') => {
if !current_text.is_empty() {
result.push_str(
¤t_text
.truecolor(text_color.0, text_color.1, text_color.2)
.to_string(),
);
current_text.clear();
}
chars.next();
let mut bold = String::new();
while let Some(c) = chars.next() {
if c == '*' && chars.peek() == Some(&'*') {
chars.next(); break;
}
bold.push(c);
}
result.push_str(
&bold
.truecolor(cyan_color.0, cyan_color.1, cyan_color.2)
.bold()
.to_string(),
);
}
'`' => {
if !current_text.is_empty() {
result.push_str(
¤t_text
.truecolor(text_color.0, text_color.1, text_color.2)
.to_string(),
);
current_text.clear();
}
let mut code = String::new();
for c in chars.by_ref() {
if c == '`' {
break;
}
code.push(c);
}
result.push_str(
&code
.truecolor(warning_color.0, warning_color.1, warning_color.2)
.to_string(),
);
}
_ => {
current_text.push(ch);
}
}
}
if !current_text.is_empty() {
result.push_str(
¤t_text
.truecolor(text_color.0, text_color.1, text_color.2)
.to_string(),
);
}
result
}