use std::collections::BTreeMap;
use std::io::Write;
use std::path::Path;
use anyhow::Result;
use serde_json::json;
use crate::analysis::findings::Finding;
use crate::analysis::result::{FailureReason, ProviderFailure};
use crate::cli::OutputFormat;
use crate::cli::check::{CheckOutcome, ProviderUse};
use crate::cli::render::{finding_line, write_failures};
pub fn render(outcome: &CheckOutcome, format: OutputFormat) -> Result<()> {
render_to(&mut std::io::stdout().lock(), outcome, format)
}
pub fn render_to<W: Write>(
out: &mut W,
outcome: &CheckOutcome,
format: OutputFormat,
) -> Result<()> {
match format {
OutputFormat::Text => render_text(out, outcome),
OutputFormat::Json => render_json(out, outcome),
}
}
fn render_text<W: Write>(out: &mut W, outcome: &CheckOutcome) -> Result<()> {
let mut by_position: BTreeMap<(&str, u32, usize), (String, Vec<String>)> = BTreeMap::new();
for (idx, (source, f)) in tagged(outcome).enumerate() {
by_position.insert(
(f.file_path.as_str(), f.line, idx),
(finding_line(Some(source), f), finding_details(f)),
);
}
let clean = by_position.is_empty() && outcome.failures.is_empty();
for (line, details) in by_position.values() {
writeln!(out, "{line}")?;
for detail in details {
writeln!(out, "{detail}")?;
}
}
write_failures(out, &outcome.failures, !by_position.is_empty())?;
write_provider_block(out, &outcome.provider_uses)?;
if outcome.retry_push {
writeln!(out)?;
writeln!(
out,
"Review cache warmed. Run git push again; the cached review will be fast."
)?;
} else if clean {
writeln!(out, "No issues found.")?;
}
Ok(())
}
fn finding_details(finding: &Finding) -> Vec<String> {
let mut details = Vec::new();
if let Some(suggestion) = &finding.suggestion {
details.push(format!(" suggestion: {suggestion}"));
}
if let Some(fingerprint) = &finding.fingerprint {
details.push(format!(" acknowledge: drep acknowledge {fingerprint}"));
}
details
}
fn write_provider_block<W: Write>(out: &mut W, uses: &[ProviderUse]) -> Result<()> {
if !uses.iter().any(|use_| use_.index > 0) {
return Ok(());
}
writeln!(out)?;
writeln!(out, "LLM providers used (the chain fell through):")?;
for use_ in uses {
writeln!(
out,
" {}. {} at {}: {} file(s)",
use_.index + 1,
use_.model,
use_.location,
use_.files
)?;
}
Ok(())
}
fn tagged(outcome: &CheckOutcome) -> impl Iterator<Item = (&'static str, &Finding)> {
outcome
.tool_findings
.iter()
.map(|f| ("tool", f))
.chain(outcome.llm_findings.iter().map(|f| ("llm", f)))
}
fn render_json<W: Write>(out: &mut W, outcome: &CheckOutcome) -> Result<()> {
let findings: Vec<_> = tagged(outcome)
.map(|(source, f)| finding_json(source, f))
.collect();
let unanalyzed: Vec<_> = outcome
.failures
.iter()
.map(|(path, reason)| unanalyzed_json(path, reason))
.collect();
let providers: Vec<_> = outcome
.provider_uses
.iter()
.map(provider_use_json)
.collect();
let exit = outcome.exit.code();
let payload = json!({
"findings": findings,
"unanalyzed": unanalyzed,
"providers": providers,
"retry_push": outcome.retry_push,
"exit": exit,
});
serde_json::to_writer_pretty(&mut *out, &payload)?;
writeln!(out)?;
Ok(())
}
fn failure_kind(reason: &FailureReason) -> &'static str {
match reason {
FailureReason::Transport { .. } => "transport",
FailureReason::Backend { .. } => "backend",
FailureReason::Unparseable(_) => "unparseable",
FailureReason::CacheMiss => "cache_miss",
FailureReason::ModelStopped { .. } => "model_stopped",
FailureReason::Truncated => "truncated",
FailureReason::MalformedFinding(_) => "malformed_finding",
FailureReason::ToolUnavailable { .. } => "tool_unavailable",
FailureReason::FileTooLarge { .. } => "file_too_large",
FailureReason::PayloadTooLarge { .. } => "payload_too_large",
FailureReason::Unreadable(_) => "unreadable",
FailureReason::Unsupported { .. } => "unsupported",
FailureReason::ChainFailed(_) => "chain_failed",
}
}
fn unanalyzed_json(path: &Path, reason: &FailureReason) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert("file".to_owned(), json!(path.to_string_lossy()));
if let FailureReason::ChainFailed(failures) = reason {
obj.insert(
"providers".to_owned(),
json!(
failures
.iter()
.map(provider_failure_json)
.collect::<Vec<_>>()
),
);
}
insert_reason(&mut obj, reason);
serde_json::Value::Object(obj)
}
fn provider_failure_json(failure: &ProviderFailure) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert("provider".to_owned(), json!(failure.provider + 1));
obj.insert("model".to_owned(), json!(failure.model));
obj.insert("skipped".to_owned(), json!(failure.skipped));
insert_reason(&mut obj, &failure.reason);
serde_json::Value::Object(obj)
}
fn insert_reason(obj: &mut serde_json::Map<String, serde_json::Value>, reason: &FailureReason) {
obj.insert("kind".to_owned(), json!(failure_kind(reason)));
if let FailureReason::Backend { kind, .. } = reason {
obj.insert("backend_kind".to_owned(), json!(kind.as_str()));
}
if let Some(status) = reason.status() {
obj.insert("status".to_owned(), json!(status));
}
obj.insert("reason".to_owned(), json!(reason.one_line()));
}
fn provider_use_json(use_: &ProviderUse) -> serde_json::Value {
json!({
"provider": use_.index + 1,
"model": use_.model,
"endpoint": use_.location,
"files": use_.files,
})
}
fn finding_json(source: &'static str, f: &Finding) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert("source".to_owned(), json!(source));
obj.insert("kind".to_owned(), json!(f.kind));
obj.insert("severity".to_owned(), json!(f.severity.as_str()));
obj.insert("file".to_owned(), json!(f.file_path));
obj.insert("line".to_owned(), json!(f.line));
if let Some(column) = f.column {
obj.insert("column".to_owned(), json!(column));
}
obj.insert("message".to_owned(), json!(f.message));
obj.insert("suggestion".to_owned(), json!(f.suggestion));
if let Some(fingerprint) = &f.fingerprint {
obj.insert("fingerprint".to_owned(), json!(fingerprint));
}
serde_json::Value::Object(obj)
}