use crate::output::OutputFormatter;
use crate::rule::LintWarning;
use serde_json::json;
pub struct SarifFormatter;
impl Default for SarifFormatter {
fn default() -> Self {
Self
}
}
impl SarifFormatter {
pub fn new() -> Self {
Self
}
}
impl OutputFormatter for SarifFormatter {
fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
let results: Vec<_> = warnings
.iter()
.map(|warning| {
let rule_id = warning.rule_name.as_deref().unwrap_or("unknown");
let level = match warning.severity {
crate::rule::Severity::Error => "error",
crate::rule::Severity::Warning => "warning",
crate::rule::Severity::Info => "note",
};
json!({
"ruleId": rule_id,
"level": level,
"message": {
"text": warning.message
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": file_path
},
"region": {
"startLine": warning.line,
"startColumn": warning.column
}
}
}]
})
})
.collect();
let sarif_doc = json!({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "rumdl",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": "https://github.com/rvben/rumdl"
}
},
"results": results
}]
});
serde_json::to_string_pretty(&sarif_doc).unwrap_or_else(|_| r#"{"version":"2.1.0","runs":[]}"#.to_string())
}
}
pub fn format_sarif_report(all_warnings: &[(String, Vec<LintWarning>)]) -> String {
let mut results = Vec::new();
let mut rules = std::collections::HashMap::new();
for (file_path, warnings) in all_warnings {
for warning in warnings {
let rule_id = warning.rule_name.as_deref().unwrap_or("unknown");
rules.entry(rule_id).or_insert_with(|| {
json!({
"id": rule_id,
"name": rule_id,
"shortDescription": {
"text": format!("Markdown rule {}", rule_id)
},
"fullDescription": {
"text": format!("Markdown linting rule {}", rule_id)
}
})
});
let level = match warning.severity {
crate::rule::Severity::Error => "error",
crate::rule::Severity::Warning => "warning",
crate::rule::Severity::Info => "note",
};
let result = json!({
"ruleId": rule_id,
"level": level,
"message": {
"text": warning.message
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": file_path
},
"region": {
"startLine": warning.line,
"startColumn": warning.column
}
}
}]
});
results.push(result);
}
}
let sarif_doc = json!({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "rumdl",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": "https://github.com/rvben/rumdl",
"rules": rules.values().cloned().collect::<Vec<_>>()
}
},
"results": results
}]
});
serde_json::to_string_pretty(&sarif_doc).unwrap_or_else(|_| r#"{"version":"2.1.0","runs":[]}"#.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::MarkdownFlavor;
use crate::lint_context::LintContext;
use crate::rule::{Fix, Rule, Severity};
use crate::rules::MD032BlanksAroundLists;
use serde_json::Value;
use std::path::PathBuf;
#[test]
fn test_sarif_formatter_default() {
let _formatter = SarifFormatter;
}
#[test]
fn test_sarif_formatter_new() {
let _formatter = SarifFormatter::new();
}
#[test]
fn test_format_warnings_empty() {
let formatter = SarifFormatter::new();
let warnings = vec![];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
assert_eq!(sarif["version"], "2.1.0");
assert_eq!(
sarif["$schema"],
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
);
assert_eq!(sarif["runs"][0]["results"].as_array().unwrap().len(), 0);
}
#[test]
fn test_format_single_warning() {
let formatter = SarifFormatter::new();
let warnings = vec![LintWarning {
line: 10,
column: 5,
end_line: 10,
end_column: 15,
rule_name: Some("MD001".to_string()),
message: "Heading levels should only increment by one level at a time".to_string(),
severity: Severity::Warning,
fix: None,
}];
let output = formatter.format_warnings(&warnings, "README.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
let result = &results[0];
assert_eq!(result["ruleId"], "MD001");
assert_eq!(result["level"], "warning");
assert_eq!(
result["message"]["text"],
"Heading levels should only increment by one level at a time"
);
assert_eq!(
result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"README.md"
);
assert_eq!(result["locations"][0]["physicalLocation"]["region"]["startLine"], 10);
assert_eq!(result["locations"][0]["physicalLocation"]["region"]["startColumn"], 5);
}
#[test]
fn test_format_single_warning_with_fix() {
let formatter = SarifFormatter::new();
let warnings = vec![LintWarning {
line: 10,
column: 5,
end_line: 10,
end_column: 15,
rule_name: Some("MD001".to_string()),
message: "Heading levels should only increment by one level at a time".to_string(),
severity: Severity::Warning,
fix: Some(Fix {
range: 100..110,
replacement: "## Heading".to_string(),
}),
}];
let output = formatter.format_warnings(&warnings, "README.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0]["ruleId"], "MD001");
}
#[test]
fn test_format_multiple_warnings() {
let formatter = SarifFormatter::new();
let warnings = vec![
LintWarning {
line: 5,
column: 1,
end_line: 5,
end_column: 10,
rule_name: Some("MD001".to_string()),
message: "First warning".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 10,
column: 3,
end_line: 10,
end_column: 20,
rule_name: Some("MD013".to_string()),
message: "Second warning".to_string(),
severity: Severity::Error,
fix: None,
},
];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0]["ruleId"], "MD001");
assert_eq!(results[0]["level"], "warning");
assert_eq!(results[0]["locations"][0]["physicalLocation"]["region"]["startLine"], 5);
assert_eq!(results[1]["ruleId"], "MD013");
assert_eq!(results[1]["level"], "error");
assert_eq!(
results[1]["locations"][0]["physicalLocation"]["region"]["startLine"],
10
);
}
#[test]
fn test_format_warning_unknown_rule() {
let formatter = SarifFormatter::new();
let warnings = vec![LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: None,
message: "Unknown rule warning".to_string(),
severity: Severity::Warning,
fix: None,
}];
let output = formatter.format_warnings(&warnings, "file.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results[0]["ruleId"], "unknown");
}
#[test]
fn test_tool_information() {
let formatter = SarifFormatter::new();
let warnings = vec![];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let driver = &sarif["runs"][0]["tool"]["driver"];
assert_eq!(driver["name"], "rumdl");
assert_eq!(driver["version"], env!("CARGO_PKG_VERSION"));
assert_eq!(driver["informationUri"], "https://github.com/rvben/rumdl");
}
#[test]
fn test_sarif_report_empty() {
let warnings = vec![];
let output = format_sarif_report(&warnings);
let sarif: Value = serde_json::from_str(&output).unwrap();
assert_eq!(sarif["version"], "2.1.0");
assert_eq!(sarif["runs"][0]["results"].as_array().unwrap().len(), 0);
}
#[test]
fn test_sarif_report_single_file() {
let warnings = vec![(
"test.md".to_string(),
vec![LintWarning {
line: 10,
column: 5,
end_line: 10,
end_column: 15,
rule_name: Some("MD001".to_string()),
message: "Test warning".to_string(),
severity: Severity::Warning,
fix: None,
}],
)];
let output = format_sarif_report(&warnings);
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(
results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"test.md"
);
let rules = sarif["runs"][0]["tool"]["driver"]["rules"].as_array().unwrap();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0]["id"], "MD001");
}
#[test]
fn test_sarif_report_multiple_files() {
let warnings = vec![
(
"file1.md".to_string(),
vec![LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Warning in file 1".to_string(),
severity: Severity::Warning,
fix: None,
}],
),
(
"file2.md".to_string(),
vec![
LintWarning {
line: 5,
column: 1,
end_line: 5,
end_column: 10,
rule_name: Some("MD013".to_string()),
message: "Warning 1 in file 2".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 10,
column: 1,
end_line: 10,
end_column: 10,
rule_name: Some("MD022".to_string()),
message: "Warning 2 in file 2".to_string(),
severity: Severity::Error,
fix: None,
},
],
),
];
let output = format_sarif_report(&warnings);
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0]["level"], "warning"); assert_eq!(results[1]["level"], "warning"); assert_eq!(results[2]["level"], "error");
let rules = sarif["runs"][0]["tool"]["driver"]["rules"].as_array().unwrap();
assert_eq!(rules.len(), 3);
let rule_ids: Vec<&str> = rules.iter().map(|r| r["id"].as_str().unwrap()).collect();
assert!(rule_ids.contains(&"MD001"));
assert!(rule_ids.contains(&"MD013"));
assert!(rule_ids.contains(&"MD022"));
}
#[test]
fn test_rule_deduplication() {
let warnings = vec![(
"test.md".to_string(),
vec![
LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "First MD001".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 10,
column: 1,
end_line: 10,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Second MD001".to_string(),
severity: Severity::Warning,
fix: None,
},
],
)];
let output = format_sarif_report(&warnings);
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 2);
let rules = sarif["runs"][0]["tool"]["driver"]["rules"].as_array().unwrap();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0]["id"], "MD001");
}
#[test]
fn test_severity_mapping() {
let formatter = SarifFormatter::new();
let warnings = vec![
LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Warning severity".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 2,
column: 1,
end_line: 2,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "Error severity".to_string(),
severity: Severity::Error,
fix: None,
},
];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results[0]["level"], "warning"); assert_eq!(results[1]["level"], "error"); }
#[test]
fn test_sarif_report_severity_mapping() {
let warnings = vec![
(
"file1.md".to_string(),
vec![LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Warning".to_string(),
severity: Severity::Warning,
fix: None,
}],
),
(
"file2.md".to_string(),
vec![LintWarning {
line: 5,
column: 1,
end_line: 5,
end_column: 10,
rule_name: Some("MD032".to_string()),
message: "Error".to_string(),
severity: Severity::Error,
fix: None,
}],
),
];
let output = format_sarif_report(&warnings);
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0]["level"], "warning");
assert_eq!(results[1]["level"], "error");
}
#[test]
fn test_special_characters_in_message() {
let formatter = SarifFormatter::new();
let warnings = vec![LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
severity: Severity::Warning,
fix: None,
}];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(
results[0]["message"]["text"],
"Warning with \"quotes\" and 'apostrophes' and \n newline"
);
}
#[test]
fn test_special_characters_in_file_path() {
let formatter = SarifFormatter::new();
let warnings = vec![LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Test".to_string(),
severity: Severity::Warning,
fix: None,
}];
let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(
results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"path/with spaces/and-dashes.md"
);
}
#[test]
fn test_sarif_schema_version() {
let formatter = SarifFormatter::new();
let warnings = vec![];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
assert_eq!(
sarif["$schema"],
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
);
assert_eq!(sarif["version"], "2.1.0");
}
#[test]
fn test_md032_integration_produces_warning_level() {
let content = "# Heading\n- List item without blank line before";
let rule = MD032BlanksAroundLists::default();
let ctx = LintContext::new(content, MarkdownFlavor::Standard, Some(PathBuf::from("test.md")));
let warnings = rule.check(&ctx).expect("MD032 check should succeed");
assert!(!warnings.is_empty(), "MD032 should flag list without blank line");
let formatter = SarifFormatter::new();
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert!(
results.iter().any(|r| r["level"] == "warning"),
"MD032 violations should produce 'warning' level in SARIF output"
);
assert!(
results.iter().any(|r| r["ruleId"] == "MD032"),
"Results should include MD032 rule"
);
}
#[test]
fn test_all_warnings_no_errors() {
let formatter = SarifFormatter::new();
let warnings = vec![
LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "First warning".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 2,
column: 1,
end_line: 2,
end_column: 5,
rule_name: Some("MD013".to_string()),
message: "Second warning".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 3,
column: 1,
end_line: 3,
end_column: 5,
rule_name: Some("MD041".to_string()),
message: "Third warning".to_string(),
severity: Severity::Warning,
fix: None,
},
];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r["level"] == "warning"));
assert!(!results.iter().any(|r| r["level"] == "error"));
}
#[test]
fn test_all_errors_no_warnings() {
let formatter = SarifFormatter::new();
let warnings = vec![
LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "First error".to_string(),
severity: Severity::Error,
fix: None,
},
LintWarning {
line: 2,
column: 1,
end_line: 2,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "Second error".to_string(),
severity: Severity::Error,
fix: None,
},
];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| r["level"] == "error"));
assert!(!results.iter().any(|r| r["level"] == "warning"));
}
#[test]
fn test_mixed_severities_same_file() {
let formatter = SarifFormatter::new();
let warnings = vec![
LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Warning".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 2,
column: 1,
end_line: 2,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "Error".to_string(),
severity: Severity::Error,
fix: None,
},
LintWarning {
line: 3,
column: 1,
end_line: 3,
end_column: 5,
rule_name: Some("MD013".to_string()),
message: "Warning".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 4,
column: 1,
end_line: 4,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "Error".to_string(),
severity: Severity::Error,
fix: None,
},
];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 4);
assert_eq!(results[0]["level"], "warning"); assert_eq!(results[1]["level"], "error"); assert_eq!(results[2]["level"], "warning"); assert_eq!(results[3]["level"], "error");
let warning_count = results.iter().filter(|r| r["level"] == "warning").count();
let error_count = results.iter().filter(|r| r["level"] == "error").count();
assert_eq!(warning_count, 2);
assert_eq!(error_count, 2);
}
#[test]
fn test_rule_deduplication_preserves_severity() {
let warnings = vec![(
"test.md".to_string(),
vec![
LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "First MD032 error".to_string(),
severity: Severity::Error,
fix: None,
},
LintWarning {
line: 5,
column: 1,
end_line: 5,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "Second MD032 error".to_string(),
severity: Severity::Error,
fix: None,
},
LintWarning {
line: 10,
column: 1,
end_line: 10,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "Third MD032 error".to_string(),
severity: Severity::Error,
fix: None,
},
],
)];
let output = format_sarif_report(&warnings);
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r["level"] == "error"));
assert!(results.iter().all(|r| r["ruleId"] == "MD032"));
let rules = sarif["runs"][0]["tool"]["driver"]["rules"].as_array().unwrap();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0]["id"], "MD032");
assert!(rules[0].get("defaultConfiguration").is_none());
}
#[test]
fn test_sarif_output_valid_json_schema() {
let formatter = SarifFormatter::new();
let warnings = vec![LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Test".to_string(),
severity: Severity::Warning,
fix: None,
}];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).expect("SARIF output must be valid JSON");
assert!(sarif.get("version").is_some(), "Must have version field");
assert!(sarif.get("$schema").is_some(), "Must have $schema field");
assert!(sarif.get("runs").is_some(), "Must have runs field");
let runs = sarif["runs"].as_array().expect("runs must be an array");
assert!(!runs.is_empty(), "Must have at least one run");
let run = &runs[0];
assert!(run.get("tool").is_some(), "Run must have tool field");
assert!(run.get("results").is_some(), "Run must have results field");
assert!(run["tool"].get("driver").is_some(), "Tool must have driver field");
assert!(run["results"].is_array(), "Results must be an array");
let results = run["results"].as_array().unwrap();
for result in results {
assert!(result.get("ruleId").is_some(), "Result must have ruleId");
assert!(result.get("level").is_some(), "Result must have level");
assert!(result.get("message").is_some(), "Result must have message");
assert!(result.get("locations").is_some(), "Result must have locations");
let level = result["level"].as_str().unwrap();
assert!(
matches!(level, "warning" | "error" | "note" | "none" | "open"),
"Level must be valid SARIF level, got: {level}"
);
}
}
#[test]
fn test_default_configuration_removed() {
let warnings = vec![(
"test.md".to_string(),
vec![
LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("MD001".to_string()),
message: "Warning".to_string(),
severity: Severity::Warning,
fix: None,
},
LintWarning {
line: 2,
column: 1,
end_line: 2,
end_column: 5,
rule_name: Some("MD032".to_string()),
message: "Error".to_string(),
severity: Severity::Error,
fix: None,
},
],
)];
let output = format_sarif_report(&warnings);
let sarif: Value = serde_json::from_str(&output).unwrap();
let rules = sarif["runs"][0]["tool"]["driver"]["rules"].as_array().unwrap();
assert_eq!(rules.len(), 2);
for rule in rules {
assert!(
rule.get("defaultConfiguration").is_none(),
"Rule {} should not have defaultConfiguration (it's instance-specific, not rule-specific)",
rule["id"]
);
}
}
#[test]
fn test_unknown_rule_with_error_severity() {
let formatter = SarifFormatter::new();
let warnings = vec![LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: None,
message: "Unknown error".to_string(),
severity: Severity::Error,
fix: None,
}];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0]["ruleId"], "unknown");
assert_eq!(results[0]["level"], "error"); }
#[test]
fn test_exhaustive_severity_mapping() {
let formatter = SarifFormatter::new();
let all_severities = vec![(Severity::Warning, "warning"), (Severity::Error, "error")];
for (severity, expected_level) in all_severities {
let warnings = vec![LintWarning {
line: 1,
column: 1,
end_line: 1,
end_column: 5,
rule_name: Some("TEST".to_string()),
message: format!("Test {severity:?}"),
severity,
fix: None,
}];
let output = formatter.format_warnings(&warnings, "test.md");
let sarif: Value = serde_json::from_str(&output).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(
results[0]["level"], expected_level,
"Severity::{severity:?} should map to SARIF level '{expected_level}'"
);
}
}
}