use crate::ui::constants::*;
use crate::ui::utils::{box_line, divider, empty_box_line, term_width, wrap_text};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct Violation {
pub index: usize,
pub total: usize,
pub severity: String,
pub title: String,
pub invariant_id: String,
pub location: String,
pub cwe: String,
pub message: String,
pub recommendation: String,
pub reference: String,
pub code_snippet: String,
}
pub fn render_violation(violation: &Violation, width: usize) -> String {
let mut output = String::new();
let (icon, apply_color) = match violation.severity.as_str() {
"critical" => (ICON_CRITICAL, color_critical as fn(&str) -> String),
"high" => (ICON_HIGH, color_high as fn(&str) -> String),
"medium" => (ICON_MEDIUM, color_medium as fn(&str) -> String),
"low" => (ICON_LOW, color_low as fn(&str) -> String),
_ => (ICON_LOW, color_low as fn(&str) -> String),
};
let content_width = width.saturating_sub(4);
let severity_badge = format!(" {} ", violation.severity.to_uppercase());
let header_label = format!("{} of {}", violation.index, violation.total);
let top_padding =
content_width.saturating_sub(header_label.len() + 2 + severity_badge.len() + 10);
let top_content = format!(
"─ {} {}{}─ {} ─",
header_label,
"─".repeat(top_padding),
apply_color(&severity_badge),
""
);
output.push_str(&format!(
"{}{}{} \n",
apply_color("╭"),
top_content,
apply_color("╮")
));
output.push_str(&format!("{}\n", empty_box_line(width)));
let title_line = format!(
"{} {}",
apply_color(&format!("{} {}", icon, violation.title)),
color_dim(&violation.invariant_id)
);
output.push_str(&format!("{}\n", box_line(&title_line, width)));
output.push_str(&format!("{}\n", empty_box_line(width)));
let location_label = color_dim("Location");
let location_line = format!("{} {}", location_label, color_value(&violation.location));
output.push_str(&format!("{}\n", box_line(&location_line, width)));
let cwe_label = color_dim("CWE");
let cwe_line = format!("{} {}", cwe_label, color_value(&violation.cwe));
output.push_str(&format!("{}\n", box_line(&cwe_line, width)));
output.push_str(&format!("{}\n", empty_box_line(width)));
let wrapped_message = wrap_text(&violation.message, content_width);
for line in wrapped_message {
output.push_str(&format!("{}\n", box_line(&line, width)));
}
output.push_str(&format!("{}\n", empty_box_line(width)));
let wrapped_rec = wrap_text(&violation.recommendation, content_width - 2);
for (i, line) in wrapped_rec.iter().enumerate() {
let prefix = if i == 0 {
format!("{} ", color_recommendation(ICON_ARROW))
} else {
" ".to_string()
};
output.push_str(&format!(
"{}\n",
box_line(&format!("{}{}", prefix, color_recommendation(line)), width)
));
}
output.push_str(&format!("{}\n", empty_box_line(width)));
let ref_label = color_dim("Reference");
let ref_line = format!("{} {}", ref_label, color_dim(&violation.reference));
output.push_str(&format!("{}\n", box_line(&ref_line, width)));
output.push_str(&empty_box_line(width));
output.push('\n');
if !violation.code_snippet.is_empty() {
let code_label = color_dim("Vulnerable Code");
output.push_str(&format!("{}\n", box_line(&code_label, width)));
let code_lines: Vec<&str> = violation.code_snippet.lines().collect();
for code_line in code_lines {
let highlighted = format!(" {}", color_value(code_line));
output.push_str(&format!("{}\n", box_line(&highlighted, width)));
}
}
output.push_str(&format!(
"{}{}{}\n",
apply_color("╰"),
divider(content_width + 2),
apply_color("╯")
));
output
}
pub fn render_violations(violations: &[Violation]) -> String {
let width = term_width();
let mut output = String::new();
if !violations.is_empty() {
output.push('\n');
let header = format!("Violations ({})", violations.len());
output.push_str(&format!("{}\n\n", color_label(&header)));
for (idx, violation) in violations.iter().enumerate() {
let mut v = violation.clone();
v.index = idx + 1;
v.total = violations.len();
output.push_str(&render_violation(&v, width));
output.push('\n');
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_violation_structure() {
let violation = Violation {
index: 1,
total: 1,
severity: "critical".to_string(),
title: "Test Vulnerability".to_string(),
invariant_id: "test_invariant".to_string(),
location: "test.sol:42".to_string(),
cwe: "CWE-123 · Test CWE".to_string(),
message: "This is a test message".to_string(),
recommendation: "Fix this issue".to_string(),
reference: "https://docs.example.com".to_string(),
code_snippet: " 42 | function transfer() public { }".to_string(),
};
let rendered = render_violation(&violation, 80);
assert!(rendered.contains("CRITICAL"));
assert!(rendered.contains("Test Vulnerability"));
assert!(rendered.contains("test_invariant"));
assert!(rendered.contains("test.sol:42"));
assert!(rendered.contains("CWE-123"));
assert!(rendered.contains("This is a test message"));
assert!(rendered.contains("Fix this issue"));
assert!(rendered.contains("https://docs.example.com"));
assert!(rendered.contains("╭"));
assert!(rendered.contains("╮"));
assert!(rendered.contains("╰"));
assert!(rendered.contains("╯"));
}
#[test]
fn test_render_violation_severity_levels() {
for severity in &["critical", "high", "medium", "low"] {
let violation = Violation {
index: 1,
total: 1,
severity: severity.to_string(),
title: "Test".to_string(),
invariant_id: "test".to_string(),
location: "test.sol:1".to_string(),
cwe: "CWE-1".to_string(),
message: "msg".to_string(),
recommendation: "fix".to_string(),
reference: "ref".to_string(),
code_snippet: String::new(),
};
let rendered = render_violation(&violation, 80);
assert!(rendered.contains(&severity.to_uppercase()));
}
}
#[test]
fn test_render_violations_list() {
let violations = vec![
Violation {
index: 1,
total: 2,
severity: "critical".to_string(),
title: "Issue 1".to_string(),
invariant_id: "inv1".to_string(),
location: "file.sol:1".to_string(),
cwe: "CWE-1".to_string(),
message: "msg1".to_string(),
recommendation: "fix1".to_string(),
reference: "ref1".to_string(),
code_snippet: String::new(),
},
Violation {
index: 2,
total: 2,
severity: "high".to_string(),
title: "Issue 2".to_string(),
invariant_id: "inv2".to_string(),
location: "file.sol:2".to_string(),
cwe: "CWE-2".to_string(),
message: "msg2".to_string(),
recommendation: "fix2".to_string(),
reference: "ref2".to_string(),
code_snippet: String::new(),
},
];
let rendered = render_violations(&violations);
assert!(rendered.contains("Violations (2)"));
assert!(rendered.contains("Issue 1"));
assert!(rendered.contains("Issue 2"));
}
}