use mir_issues::{Issue, Severity};
pub fn format_junit(issues: &[&Issue]) -> String {
use std::collections::HashMap;
let mut by_file: HashMap<&str, Vec<&Issue>> = HashMap::new();
for issue in issues {
by_file
.entry(issue.location.file.as_ref())
.or_default()
.push(issue);
}
let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
let total_failures: usize = issues
.iter()
.filter(|i| i.severity == Severity::Error)
.count();
out.push_str(&format!(
"<testsuites name=\"mir\" tests=\"{}\" failures=\"{}\">\n",
issues.len(),
total_failures,
));
let mut files: Vec<&str> = by_file.keys().copied().collect();
files.sort_unstable();
for file in files {
let file_issues = &by_file[file];
let failures = file_issues
.iter()
.filter(|i| i.severity == Severity::Error)
.count();
out.push_str(&format!(
" <testsuite name=\"{}\" tests=\"{}\" failures=\"{}\">\n",
xml_escape(file),
file_issues.len(),
failures,
));
for issue in file_issues.iter() {
let name = issue.kind.name();
let msg = issue.kind.message();
let severity = match issue.severity {
Severity::Error => "failure",
Severity::Warning => "warning",
Severity::Info => "notice",
};
out.push_str(&format!(
" <testcase name=\"{}\" classname=\"{}\">\n",
xml_escape(name),
xml_escape(file),
));
out.push_str(&format!(
" <{} message=\"{}\" type=\"{}\">{}</{}>\n",
severity,
xml_escape(&msg),
xml_escape(name),
xml_escape(&format!(
"{}:{}:{} {} {}: {}",
file, issue.location.line, issue.location.col_start, issue.severity, name, msg
)),
severity,
));
out.push_str(" </testcase>\n");
}
out.push_str(" </testsuite>\n");
}
out.push_str("</testsuites>\n");
out
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn fnv1a(data: &str) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for byte in data.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x00000100000001b3);
}
hash
}
pub fn format_sarif(issues: &[&Issue]) -> String {
let mut rule_map: std::collections::HashMap<String, Severity> =
std::collections::HashMap::new();
for issue in issues {
rule_map
.entry(issue.kind.name().to_string())
.or_insert_with(|| issue.kind.default_severity());
}
let mut rule_ids: Vec<String> = rule_map.keys().cloned().collect();
rule_ids.sort_unstable();
let rules_json: Vec<serde_json::Value> = rule_ids
.iter()
.map(|id| {
let level = match rule_map[id] {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "note",
};
let tag = if id.starts_with("Tainted") {
"security"
} else {
"maintainability"
};
serde_json::json!({
"id": id,
"name": id,
"shortDescription": { "text": id },
"helpUri": "https://github.com/jorgsowa/mir",
"defaultConfiguration": { "level": level },
"properties": { "tags": [tag] },
})
})
.collect();
let results_json: Vec<serde_json::Value> = issues
.iter()
.map(|issue| {
let level = match issue.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "note",
};
let fingerprint_input = format!(
"{}:{}",
issue.kind.name(),
issue.snippet.as_deref().unwrap_or("")
);
let fingerprint = format!("{:016x}", fnv1a(&fingerprint_input));
let rank = match issue.severity {
Severity::Error => 90.0_f64,
Severity::Warning => 95.0,
Severity::Info => 99.0,
};
serde_json::json!({
"ruleId": issue.kind.name(),
"level": level,
"rank": rank,
"message": { "text": issue.kind.message() },
"partialFingerprints": {
"primaryLocationLineHash": fingerprint,
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": issue.location.file.as_ref(),
"uriBaseId": "%SRCROOT%",
},
"region": {
"startLine": issue.location.line,
"endLine": issue.location.line_end,
"startColumn": issue.location.col_start + 1,
"endColumn": issue.location.col_end + 1,
}
}
}]
})
})
.collect();
let sarif = serde_json::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": "mir",
"informationUri": "https://github.com/jorgsowa/mir",
"rules": rules_json,
}
},
"results": results_json,
}]
});
serde_json::to_string_pretty(&sarif).unwrap_or_else(|_| "{}".to_string())
}