use std::io::{self, Write};
use serde::Serialize;
use slopguard_core::finding::{Finding, ScanResult};
use slopguard_core::metric::Metric;
use slopguard_core::rule::{Category, Language, Rule, Severity};
use crate::output::write_json_pretty;
#[derive(Serialize)]
struct JsonOutput<'a> {
findings: &'a [Finding],
summary: &'a ScanSummary,
stats: &'a JsonStats<'a>,
}
#[derive(Serialize)]
struct JsonStats<'a> {
errors: usize,
warnings: usize,
total: usize,
files_scanned: usize,
baseline_filtered: usize,
#[serde(skip_serializing_if = "Option::is_none")]
diff_base: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
files_changed: Option<usize>,
}
#[derive(Serialize)]
struct ScanSummary {
errors: usize,
warnings: usize,
total: usize,
}
#[derive(Serialize)]
pub struct ListEntry {
pub id: String,
pub language: String,
pub severity: String,
pub category: String,
#[serde(rename = "type")]
pub kind: String,
pub status: String,
}
pub fn rule_kind(rule: &Rule) -> &'static str {
if rule.is_metric() {
"metric"
} else if rule.ai_check.is_some() {
"ai"
} else {
"ast"
}
}
pub fn format_list_json(entries: &[&ListEntry], w: &mut impl Write) -> io::Result<()> {
write_json_pretty(w, &entries)
}
#[derive(Serialize)]
struct ExplainOutput<'a> {
id: &'a str,
language: &'a Language,
severity: &'a Severity,
category: &'a Category,
message: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
note: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
fix: Option<&'a str>,
#[serde(rename = "type")]
kind: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
metric: Option<&'a Metric>,
#[serde(skip_serializing_if = "Option::is_none")]
threshold: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
prompt: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
should_match: Option<&'a [String]>,
#[serde(skip_serializing_if = "Option::is_none")]
should_not_match: Option<&'a [String]>,
}
pub fn format_explain_json(rule: &Rule, w: &mut impl Write) -> io::Result<()> {
let default_category = Category::Correctness;
let output = ExplainOutput {
id: rule.id.as_str(),
language: &rule.language,
severity: &rule.severity,
category: rule.category.as_ref().unwrap_or(&default_category),
message: &rule.message,
note: rule.note.as_deref(),
fix: rule.fix.as_deref(),
kind: rule_kind(rule),
metric: rule.metric.as_ref(),
threshold: rule.threshold,
prompt: rule.ai_check.as_ref().map(|a| a.prompt.as_str()),
should_match: rule.tests.as_ref().map(|t| t.should_match.as_slice()),
should_not_match: rule.tests.as_ref().map(|t| t.should_not_match.as_slice()),
};
write_json_pretty(w, &output)
}
pub fn format_json(result: &ScanResult, w: &mut impl Write) -> io::Result<()> {
let summary = ScanSummary {
errors: result.stats.errors,
warnings: result.stats.warnings,
total: result.stats.total,
};
let stats = JsonStats {
errors: result.stats.errors,
warnings: result.stats.warnings,
total: result.stats.total,
files_scanned: result.stats.files_scanned,
baseline_filtered: result.stats.baseline_filtered,
diff_base: result.stats.diff_base.as_deref(),
files_changed: result.stats.files_changed,
};
let output = JsonOutput {
findings: &result.findings,
summary: &summary,
stats: &stats,
};
write_json_pretty(w, &output)
}