use guibiao::{Coverage, Outcome, Report, Severity};
use super::term_color::Style;
pub(crate) fn report(outcome: &Outcome) {
match outcome {
Outcome::Clean => eprintln!("Tianheng: clean — no boundary violated"),
Outcome::Violations(report) => report_violations(report),
Outcome::ConstitutionError(message) => {
let style = Style::detect();
eprintln!(
"{}",
style.error(&format!("Tianheng constitution error: {message}"))
);
}
_ => {}
}
}
pub(crate) fn report_violations(report: &Report) {
eprint!("{}", violations_text_styled(report, Style::detect()));
}
#[cfg(test)]
pub(crate) fn violations_text(report: &Report) -> String {
violations_text_styled(report, Style::PLAIN)
}
pub(crate) fn violations_text_styled(report: &Report, style: Style) -> String {
use std::fmt::Write as _;
if report.violations.is_empty() {
return "Tianheng: clean — no boundary violated\n".to_string();
}
let baselined = report.violations.iter().filter(|v| v.baselined).count();
let mut shown: Vec<_> = report.violations.iter().filter(|v| !v.baselined).collect();
shown.sort_by(|a, b| {
(a.target.as_str(), a.rule.as_str()).cmp(&(b.target.as_str(), b.rule.as_str()))
});
let mut out = String::new();
for violation in shown {
let (raw_header, reaction) = match violation.severity {
Severity::Enforce => ("Tianheng violation", "CI failed."),
Severity::Warn => ("Tianheng advisory", "warning only — CI not failed."),
_ => ("Tianheng advisory", "warning only — CI not failed."),
};
let header = if violation.severity == Severity::Enforce {
style.enforce(raw_header)
} else {
style.warn(raw_header)
};
writeln!(out).unwrap();
writeln!(out, "{header}").unwrap();
writeln!(out).unwrap();
writeln!(out, "Reason:\n {}", style.reason(&violation.reason)).unwrap();
writeln!(out, "Boundary:\n {}", violation.target).unwrap();
writeln!(out, "Rule:\n {}", violation.rule).unwrap();
writeln!(out, "Found:\n {}", violation.finding).unwrap();
if let Some(file) = &violation.file {
writeln!(out, "File:\n {file}").unwrap();
}
if let Some(anchor) = &violation.anchor {
writeln!(out, "Anchor:\n {anchor}").unwrap();
}
if let Some(polarity) = violation.polarity {
writeln!(out, "Repair:\n {}", polarity.as_str()).unwrap();
}
writeln!(out, "Reaction:\n {reaction}").unwrap();
}
if baselined > 0 {
writeln!(
out,
"Tianheng: {baselined} pre-existing violation(s) suppressed by baseline"
)
.unwrap();
}
out
}
pub(crate) fn report_coverage(coverage: &Coverage, warn_uncovered: bool) {
eprint!("{}", coverage_report(coverage, warn_uncovered));
}
pub(crate) fn coverage_report(coverage: &Coverage, warn_uncovered: bool) -> String {
let uncovered = coverage.uncovered.len();
if uncovered == 0 {
return format!(
"Tianheng: coverage — all {} workspace crate(s) have a boundary\n",
coverage.total
);
}
let mut out = format!(
"Tianheng: coverage — {uncovered} of {} workspace crate(s) have no boundary\n",
coverage.total
);
if warn_uncovered {
for crate_name in &coverage.uncovered {
out.push_str("\nTianheng advisory\n\n");
out.push_str(&format!("Uncovered crate:\n {crate_name}\n"));
out.push_str("Reason:\n no boundary governs this workspace crate\n");
out.push_str("Reaction:\n warning only — CI not failed.\n");
}
}
out
}
pub(crate) fn report_sarif(outcome: &Outcome) -> String {
use serde_json::{Value, json};
let mut results: Vec<Value> = Vec::new();
let mut invocations: Vec<Value> = Vec::new();
match outcome {
Outcome::Violations(report) => {
for v in report.violations.iter().filter(|v| !v.baselined) {
let level = match v.severity {
Severity::Enforce => "error",
_ => "warning",
};
let mut result = json!({
"ruleId": v.rule,
"level": level,
"message": { "text": format!("{} (found: {})", v.reason, v.finding) },
});
result["partialFingerprints"] = json!({
"tianhengViolationId/v1": format!("{}\u{1f}{}\u{1f}{}", v.target, v.rule, v.finding),
});
if let Some(file) = &v.file {
result["locations"] = json!([{
"physicalLocation": { "artifactLocation": { "uri": file } }
}]);
}
let mut properties = serde_json::Map::new();
if let Some(anchor) = &v.anchor {
properties.insert("anchor".to_string(), json!(anchor));
}
if let Some(polarity) = v.polarity {
properties.insert("polarity".to_string(), json!(polarity.as_str()));
}
if !properties.is_empty() {
result["properties"] = Value::Object(properties);
}
results.push(result);
}
}
Outcome::ConstitutionError(message) => {
invocations.push(json!({
"executionSuccessful": false,
"toolExecutionNotifications": [{
"level": "error",
"message": { "text": message },
}],
}));
}
_ => {}
}
let mut run = json!({
"tool": { "driver": { "name": "tianheng" } },
"results": results,
});
if !invocations.is_empty() {
run["invocations"] = Value::Array(invocations);
}
let doc = json!({
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [run],
});
serde_json::to_string_pretty(&doc).expect("a serde_json::Value is always serializable")
}