mod rules;
use rules::{complexity_rule, coupling_rule, dry_rule, sarif_rules, srp_rule, tq_rule};
use serde_json::{json, Value};
use crate::domain::analysis_data::{FunctionRecord, ModuleCouplingRecord};
use crate::domain::findings::{
ArchitectureFinding, ComplexityFinding, ComplexityFindingKind, CouplingFinding,
CouplingFindingDetails, DryFinding, DryFindingDetails, DryFindingKind, IospFinding,
OrphanSuppression, SrpFinding, SrpFindingDetails, SrpFindingKind, TqFinding, TqFindingKind,
};
use crate::ports::reporter::{ReporterImpl, Snapshot};
use crate::ports::Reporter;
use crate::report::{AnalysisResult, Summary};
pub struct SarifResultRow {
pub(crate) rule_id: String,
pub(crate) finding: crate::domain::Finding,
}
pub struct SarifReporter<'a> {
pub(crate) summary: &'a Summary,
}
impl<'a> ReporterImpl for SarifReporter<'a> {
type Output = String;
type IospView = Vec<SarifResultRow>;
type ComplexityView = Vec<SarifResultRow>;
type DryView = Vec<SarifResultRow>;
type SrpView = Vec<SarifResultRow>;
type CouplingView = Vec<SarifResultRow>;
type TestQualityView = Vec<SarifResultRow>;
type ArchitectureView = Vec<SarifResultRow>;
type OrphanView = Vec<Value>;
type IospDataView = ();
type ComplexityDataView = ();
type CouplingDataView = ();
fn build_iosp(&self, findings: &[IospFinding]) -> Vec<SarifResultRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| row_from_common(&f.common, &f.common.rule_id))
.collect()
}
fn build_complexity(&self, findings: &[ComplexityFinding]) -> Vec<SarifResultRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| row_from_common(&f.common, complexity_rule(f.kind)))
.collect()
}
fn build_dry(&self, findings: &[DryFinding]) -> Vec<SarifResultRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| row_from_common(&f.common, dry_rule(f)))
.collect()
}
fn build_srp(&self, findings: &[SrpFinding]) -> Vec<SarifResultRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| row_from_common(&f.common, srp_rule(f)))
.collect()
}
fn build_coupling(&self, findings: &[CouplingFinding]) -> Vec<SarifResultRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| row_from_common(&f.common, coupling_rule(f)))
.collect()
}
fn build_test_quality(&self, findings: &[TqFinding]) -> Vec<SarifResultRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| row_from_common(&f.common, tq_rule(&f.kind)))
.collect()
}
fn build_architecture(&self, findings: &[ArchitectureFinding]) -> Vec<SarifResultRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| row_from_common(&f.common, &f.common.rule_id))
.collect()
}
fn build_orphans(&self, suppressions: &[OrphanSuppression]) -> Vec<Value> {
orphan_suppression_results(suppressions)
}
fn build_iosp_data(&self, _: &[FunctionRecord]) {}
fn build_complexity_data(&self, _: &[FunctionRecord]) {}
fn build_coupling_data(&self, _: &[ModuleCouplingRecord]) {}
fn publish(&self, snapshot: Snapshot<Self>) -> String {
let Snapshot {
iosp,
complexity,
dry,
srp,
coupling,
test_quality,
architecture,
orphans,
iosp_data: (),
complexity_data: (),
coupling_data: (),
} = snapshot;
let chunks = [
iosp,
complexity,
dry,
srp,
coupling,
test_quality,
architecture,
];
let total_rows: usize = chunks.iter().map(|c| c.len()).sum();
let mut all_rows: Vec<SarifResultRow> = Vec::with_capacity(total_rows);
for chunk in chunks {
all_rows.extend(chunk);
}
let rules = build_rules_for(&all_rows);
let cap = all_rows.len() + orphans.len() + 1;
let mut sarif_results: Vec<Value> = Vec::with_capacity(cap);
sarif_results.extend(all_rows.into_iter().map(row_to_sarif_value));
sarif_results.extend(orphans);
sarif_results.extend(suppression_ratio_result(self.summary));
let envelope = json!({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "rustqual",
"informationUri": "https://github.com/SaschaOnTour/rustqual",
"rules": rules,
}
},
"results": sarif_results,
}]
});
serde_json::to_string_pretty(&envelope)
.unwrap_or_else(|e| format!("{{\"error\":\"SARIF serialization failed: {e}\"}}"))
}
}
fn build_rules_for(rows: &[SarifResultRow]) -> Vec<Value> {
let mut rules = sarif_rules();
let mut registered: std::collections::HashSet<String> = rules
.iter()
.filter_map(|v| v["id"].as_str().map(|s| s.to_string()))
.collect();
for row in rows {
if registered.insert(row.rule_id.clone()) {
rules.push(json!({
"id": row.rule_id,
"shortDescription": { "text": row.rule_id.clone() }
}));
}
}
rules
}
fn row_from_common(common: &crate::domain::Finding, rule_id: &str) -> SarifResultRow {
SarifResultRow {
rule_id: rule_id.to_string(),
finding: common.clone(),
}
}
fn row_to_sarif_value(r: SarifResultRow) -> Value {
let level = r.finding.severity.levels().sarif;
if r.finding.file.is_empty() {
json!({
"ruleId": r.rule_id,
"level": level,
"message": { "text": r.finding.message },
"locations": []
})
} else {
json!({
"ruleId": r.rule_id,
"level": level,
"message": { "text": r.finding.message },
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": r.finding.file },
"region": { "startLine": r.finding.line }
}
}]
})
}
}
fn orphan_suppression_results(orphans: &[OrphanSuppression]) -> Vec<Value> {
orphans
.iter()
.map(|w| {
let dims: String = if w.dimensions.is_empty() {
"all dims (wildcard)".to_string()
} else {
w.dimensions
.iter()
.map(|d| format!("{d}"))
.collect::<Vec<_>>()
.join(",")
};
let message = match &w.reason {
Some(r) => format!(
"Stale qual:allow({dims}) marker — no finding in window. Reason was: {r}"
),
None => format!("Stale qual:allow({dims}) marker — no finding in window."),
};
json!({
"ruleId": "ORPHAN-001",
"level": "warning",
"message": { "text": message },
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": w.file },
"region": { "startLine": w.line }
}
}]
})
})
.collect()
}
fn suppression_ratio_result(summary: &Summary) -> Vec<Value> {
if !summary.suppression_ratio_exceeded {
return vec![];
}
vec![json!({
"ruleId": "SUP-001",
"level": "note",
"message": {
"text": format!(
"Suppression ratio exceeded: {} suppressions (qual:allow + #[allow]) of {} functions",
summary.all_suppressions, summary.total,
)
},
"locations": []
})]
}
pub fn print_sarif(analysis: &AnalysisResult) {
println!("{}", build_sarif_string(analysis));
}
pub fn build_sarif_string(analysis: &AnalysisResult) -> String {
let reporter = SarifReporter {
summary: &analysis.summary,
};
reporter.render(&analysis.findings, &analysis.data)
}
pub fn build_sarif_value(analysis: &AnalysisResult) -> Value {
serde_json::from_str(&build_sarif_string(analysis))
.unwrap_or_else(|e| json!({ "error": format!("SARIF parse failed: {e}") }))
}
#[cfg(test)]
mod tests;