use crate::adapters::analyzers::iosp::{
compute_severity, CallOccurrence, Classification, FunctionAnalysis, LogicOccurrence,
};
use crate::report::sarif::*;
use crate::report::Summary;
fn make_result(name: &str, classification: Classification) -> FunctionAnalysis {
let severity = compute_severity(&classification);
FunctionAnalysis {
name: name.to_string(),
file: "test.rs".to_string(),
line: 1,
classification,
parent_type: None,
suppressed: false,
complexity: None,
qualified_name: name.to_string(),
severity,
cognitive_warning: false,
cyclomatic_warning: false,
nesting_depth_warning: false,
function_length_warning: false,
unsafe_warning: false,
error_handling_warning: false,
complexity_suppressed: false,
own_calls: vec![],
parameter_count: 0,
is_trait_impl: false,
is_test: false,
effort_score: None,
}
}
fn make_analysis(results: Vec<FunctionAnalysis>) -> AnalysisResult {
let summary = Summary::from_results(&results);
let data = crate::app::projection::project_data(&results, None);
let findings = crate::domain::AnalysisFindings {
iosp: crate::app::projection::project_iosp(&results),
..Default::default()
};
AnalysisResult {
results,
summary,
findings,
data,
}
}
#[test]
fn test_print_sarif_emits_no_results_when_clean() {
let analysis = make_analysis(vec![make_result("good_fn", Classification::Integration)]);
let s = build_sarif_string(&analysis);
let v: serde_json::Value = serde_json::from_str(&s).expect("valid SARIF JSON");
let results = &v["runs"][0]["results"];
assert_eq!(
results.as_array().map(Vec::len),
Some(0),
"Integration function should produce no SARIF results; got {s}"
);
}
#[test]
fn test_print_sarif_emits_violation_with_location() {
let analysis = make_analysis(vec![make_result(
"bad_fn",
Classification::Violation {
has_logic: true,
has_own_calls: true,
logic_locations: vec![LogicOccurrence {
kind: "if".into(),
line: 5,
}],
call_locations: vec![CallOccurrence {
name: "helper".into(),
line: 6,
}],
},
)]);
let s = build_sarif_string(&analysis);
let v: serde_json::Value = serde_json::from_str(&s).expect("valid SARIF JSON");
let results = v["runs"][0]["results"].as_array().expect("results array");
assert!(
!results.is_empty(),
"violation must produce SARIF result; got {s}"
);
let r = &results[0];
let physical = &r["locations"][0]["physicalLocation"];
assert_eq!(physical["artifactLocation"]["uri"], "test.rs");
assert_eq!(physical["region"]["startLine"], 1);
let rule_id = r["ruleId"].as_str().unwrap_or("");
assert!(!rule_id.is_empty(), "rule_id must be set; got {s}");
}
#[test]
fn test_print_sarif_severity_for_many_violations_is_error_or_warning() {
let analysis = make_analysis(vec![make_result(
"complex_fn",
Classification::Violation {
has_logic: true,
has_own_calls: true,
logic_locations: vec![
LogicOccurrence {
kind: "if".into(),
line: 1,
},
LogicOccurrence {
kind: "match".into(),
line: 2,
},
LogicOccurrence {
kind: "for".into(),
line: 3,
},
],
call_locations: vec![
CallOccurrence {
name: "a".into(),
line: 4,
},
CallOccurrence {
name: "b".into(),
line: 5,
},
CallOccurrence {
name: "c".into(),
line: 6,
},
],
},
)]);
let s = build_sarif_string(&analysis);
let v: serde_json::Value = serde_json::from_str(&s).expect("valid SARIF JSON");
let level = v["runs"][0]["results"][0]["level"].as_str().unwrap_or("");
assert!(
matches!(level, "warning" | "error"),
"3+3 violation must map to warning or error level; got `{level}` in {s}"
);
}
#[test]
fn test_print_sarif_suppressed_skipped() {
let mut func = make_result(
"suppressed_fn",
Classification::Violation {
has_logic: true,
has_own_calls: true,
logic_locations: vec![LogicOccurrence {
kind: "if".into(),
line: 1,
}],
call_locations: vec![CallOccurrence {
name: "f".into(),
line: 2,
}],
},
);
func.suppressed = true;
let analysis = make_analysis(vec![func]);
print_sarif(&analysis);
}
#[test]
fn test_print_sarif_multiple_violations() {
let analysis = make_analysis(vec![
make_result(
"bad1",
Classification::Violation {
has_logic: true,
has_own_calls: true,
logic_locations: vec![LogicOccurrence {
kind: "if".into(),
line: 1,
}],
call_locations: vec![CallOccurrence {
name: "a".into(),
line: 2,
}],
},
),
make_result(
"bad2",
Classification::Violation {
has_logic: true,
has_own_calls: true,
logic_locations: vec![LogicOccurrence {
kind: "while".into(),
line: 10,
}],
call_locations: vec![CallOccurrence {
name: "b".into(),
line: 12,
}],
},
),
]);
print_sarif(&analysis);
}
#[test]
fn sarif_reporter_emits_orphan_results_via_snapshot_view() {
use crate::domain::findings::OrphanSuppression;
let mut analysis = make_analysis(vec![]);
analysis.findings.orphan_suppressions = vec![OrphanSuppression {
file: "src/foo.rs".into(),
line: 42,
dimensions: vec![crate::findings::Dimension::Srp],
reason: Some("legacy marker".into()),
}];
let value = build_sarif_value(&analysis);
let results = value["runs"][0]["results"]
.as_array()
.expect("results array");
let orphan = results
.iter()
.find(|r| r["ruleId"] == "ORPHAN-001")
.expect("orphan result in SARIF output");
assert_eq!(orphan["level"], "warning");
assert_eq!(
orphan["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"src/foo.rs"
);
assert_eq!(
orphan["locations"][0]["physicalLocation"]["region"]["startLine"],
42
);
let msg = orphan["message"]["text"].as_str().expect("message text");
assert!(
msg.contains("srp"),
"message should name suppressed dim: {msg}"
);
assert!(
msg.contains("legacy marker"),
"message should carry reason: {msg}"
);
}
#[test]
fn sarif_rules_include_orphan_suppression() {
let analysis = make_analysis(vec![]);
let value = build_sarif_value(&analysis);
let rules = value["runs"][0]["tool"]["driver"]["rules"]
.as_array()
.expect("rules array");
let orphan_rule = rules
.iter()
.find(|r| r["id"] == "ORPHAN-001")
.expect("ORPHAN-001 rule present in tool.driver.rules");
let desc = orphan_rule["shortDescription"]["text"]
.as_str()
.expect("shortDescription text");
assert!(
desc.to_lowercase().contains("orphan") || desc.to_lowercase().contains("stale"),
"rule description should name the orphan concept: {desc}"
);
}
fn make_arch_finding(rule_id: &str, severity: crate::domain::Severity) -> crate::domain::Finding {
crate::domain::Finding {
file: "src/cli/handlers.rs".to_string(),
line: 17,
column: 0,
dimension: crate::findings::Dimension::Architecture,
rule_id: rule_id.to_string(),
severity,
message: format!("test message for {rule_id}"),
suppressed: false,
}
}
#[test]
fn sarif_emits_architecture_call_parity_finding() {
let mut analysis = make_analysis(vec![]);
analysis.findings.architecture = vec![crate::domain::findings::ArchitectureFinding {
common: make_arch_finding(
"architecture/call_parity/no_delegation",
crate::domain::Severity::Medium,
),
}];
let value = build_sarif_value(&analysis);
let results = value["runs"][0]["results"]
.as_array()
.expect("results array");
let hit = results
.iter()
.find(|r| r["ruleId"] == "architecture/call_parity/no_delegation")
.expect("call_parity finding emitted in SARIF");
assert_eq!(hit["level"], "warning");
assert_eq!(
hit["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"src/cli/handlers.rs"
);
assert_eq!(
hit["locations"][0]["physicalLocation"]["region"]["startLine"],
17
);
}
#[test]
fn sarif_maps_architecture_severities() {
let mut analysis = make_analysis(vec![]);
analysis.findings.architecture = vec![
crate::domain::findings::ArchitectureFinding {
common: make_arch_finding(
"architecture/call_parity/multi_touchpoint",
crate::domain::Severity::Low,
),
},
crate::domain::findings::ArchitectureFinding {
common: make_arch_finding(
"architecture/call_parity/missing_adapter",
crate::domain::Severity::Medium,
),
},
crate::domain::findings::ArchitectureFinding {
common: make_arch_finding(
"architecture/trait_contract/object_safety",
crate::domain::Severity::High,
),
},
];
let value = build_sarif_value(&analysis);
let results = value["runs"][0]["results"]
.as_array()
.expect("results array");
let level_for = |rid: &str| -> &str {
results
.iter()
.find(|r| r["ruleId"] == rid)
.unwrap_or_else(|| panic!("missing {rid}"))["level"]
.as_str()
.expect("level string")
};
assert_eq!(
level_for("architecture/call_parity/multi_touchpoint"),
"note"
);
assert_eq!(
level_for("architecture/call_parity/missing_adapter"),
"warning"
);
assert_eq!(
level_for("architecture/trait_contract/object_safety"),
"error"
);
}
#[test]
fn sarif_skips_suppressed_architecture_findings() {
let mut analysis = make_analysis(vec![]);
let mut suppressed = make_arch_finding(
"architecture/call_parity/no_delegation",
crate::domain::Severity::Medium,
);
suppressed.suppressed = true;
analysis.findings.architecture =
vec![crate::domain::findings::ArchitectureFinding { common: suppressed }];
let value = build_sarif_value(&analysis);
let results = value["runs"][0]["results"]
.as_array()
.expect("results array");
assert!(
!results
.iter()
.any(|r| r["ruleId"] == "architecture/call_parity/no_delegation"),
"suppressed architecture finding must not appear in SARIF"
);
}