use crate::Result;
use crate::analyses::hotspots::HotspotRow;
use crate::hashing::sha256_prefixed;
use crate::quality_gates::GateViolation;
use crate::quality_gates::evidence::EvidenceCommit;
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
pub const SARIF_SCHEMA_URL: &str = "https://json.schemastore.org/sarif-2.1.0.json";
pub const TOOL_INFO_URI: &str = "https://github.com/emrecdr/codelore";
const RULE_ID: &str = "CODELORE-HOTSPOT";
const AUTOMATION_ID_PREFIX: &str = "codelore/hotspots/run";
const CODELORE_RESEARCH_FOUNDATIONS_URL: &str =
"https://github.com/emrecdr/codelore/blob/main/docs/research-foundations.md";
const URI_PATH_ENCODE: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
pub(super) fn percent_encode_path(p: &str) -> String {
utf8_percent_encode(p, URI_PATH_ENCODE).to_string()
}
#[must_use]
pub fn primary_location_line_hash(repo_root: &str, path: &str) -> String {
sha256_prefixed(&[repo_root, path])
}
#[must_use]
pub fn diff_finding_hash(rule: &str, path: &str, discriminant: &str) -> String {
sha256_prefixed(&[rule, path, discriminant])
}
fn run_correlation_id() -> String {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let mut hasher = Sha256::new();
hasher.update(nanos.to_le_bytes());
hasher.update(std::process::id().to_le_bytes());
let digest = hasher.finalize();
hex::encode(&digest[..8])
}
fn automation_id_for(prefix: &str) -> String {
format!("{prefix}/{}", run_correlation_id())
}
pub fn write_hotspots_sarif<W: Write>(
rows: &[HotspotRow],
repo_root: &str,
w: &mut W,
) -> Result<()> {
let doc = build_sarif(rows, repo_root);
serde_json::to_writer_pretty(w, &doc).map_err(|e| super::serde_json_io_err("sarif", &e))?;
Ok(())
}
fn build_sarif(rows: &[HotspotRow], repo_root: &str) -> serde_json::Value {
use serde_json::{Value, json};
let rule = json!({
"id": RULE_ID,
"shortDescription": {
"text": "Behavioral hotspot: high churn × high complexity"
},
"helpUri": "https://codescene.com/docs/guides/technical/hotspots.html",
"properties": {
"tags": ["behavioral", "hotspot"]
}
});
let results: Vec<Value> = rows
.iter()
.map(|row| build_result(row, repo_root))
.collect();
json!({
"$schema": SARIF_SCHEMA_URL,
"version": "2.1.0",
"runs": [{
"automationDetails": {
"id": automation_id_for(AUTOMATION_ID_PREFIX)
},
"tool": {
"driver": {
"name": "codelore",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": TOOL_INFO_URI,
"rules": [rule]
}
},
"results": results
}]
})
}
fn build_result(row: &HotspotRow, repo_root: &str) -> serde_json::Value {
use serde_json::json;
let security_severity = (100.0 - row.cognitive_health) / 10.0;
let level = if security_severity >= 7.0 {
"error"
} else if security_severity >= 4.0 {
"warning"
} else {
"note"
};
let fp = sha256_prefixed(&[repo_root, &row.path]);
let artifact_uri = format!(
"{}/{}",
repo_root.trim_end_matches('/'),
percent_encode_path(row.path.trim_start_matches('/'))
);
let band_str = match row.mi_rank {
Some(rank) if rank.is_finite() => {
Some(crate::analyses::mi::MiBand::from_rank(rank).as_str())
}
_ => None,
};
let message_text = match (row.mi, band_str) {
(Some(v), Some(band)) => format!(
"Hotspot '{}': score={:.3}, cognitive_health={:.1}, revisions={}, cognitive={:.1}, mi={:.1} ({})",
row.path,
row.hotspot_score,
row.cognitive_health,
row.revisions,
row.cognitive,
v,
band
),
(Some(v), None) => format!(
"Hotspot '{}': score={:.3}, cognitive_health={:.1}, revisions={}, cognitive={:.1}, mi={:.1}",
row.path, row.hotspot_score, row.cognitive_health, row.revisions, row.cognitive, v
),
_ => format!(
"Hotspot '{}': score={:.3}, cognitive_health={:.1}, revisions={}, cognitive={:.1}",
row.path, row.hotspot_score, row.cognitive_health, row.revisions, row.cognitive
),
};
let mut properties = json!({
"security-severity": security_severity,
"codelore/revs": row.revisions,
"codelore/cognitive": row.cognitive,
"codelore/cognitivehealth": row.cognitive_health,
"codelore/score": row.hotspot_score,
"tags": ["behavioral", "hotspot"]
});
if let Some(mi) = row.mi {
properties["codelore/mi"] = json!(mi);
}
if let Some(rank) = row.mi_rank
&& rank.is_finite()
{
properties["codelore/mi-rank"] = json!(rank);
}
if let Some(band) = band_str {
properties["codelore/mi-band"] = json!(band);
}
if let Some(pct) = row.ai_pct
&& pct.is_finite()
{
properties["codelore/ai-pct"] = json!(pct);
}
json!({
"ruleId": RULE_ID,
"level": level,
"message": { "text": message_text },
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": artifact_uri }
}
}],
"partialFingerprints": {
"primaryLocationLineHash": fp
},
"properties": properties
})
}
use crate::analyses::clones::ClonesRow;
const CLONE_RULE_ID: &str = "CODELORE-CLONE";
const CLONE_AUTOMATION_ID_PREFIX: &str = "codelore/clones/run";
pub fn write_clones_sarif<W: Write>(rows: &[ClonesRow], repo_root: &str, w: &mut W) -> Result<()> {
let doc = build_clones_sarif(rows, repo_root);
serde_json::to_writer_pretty(w, &doc)
.map_err(|e| super::serde_json_io_err("clones sarif", &e))?;
Ok(())
}
fn build_clones_sarif(rows: &[ClonesRow], repo_root: &str) -> serde_json::Value {
use serde_json::{Value, json};
use std::collections::BTreeMap;
let mut families: BTreeMap<u32, Vec<&ClonesRow>> = BTreeMap::new();
for row in rows {
families.entry(row.clone_group_id).or_default().push(row);
}
let rule = json!({
"id": CLONE_RULE_ID,
"shortDescription": {
"text": "Code clone family (Type 1 + Type 2 via AST structural hashing)"
},
"fullDescription": {
"text": "A group of functions whose AST structure is identical after \
normalizing identifiers and literals. Type 1 = exact; Type 2 = \
renamed/parameterized. See CODELORE-LIVE-CLONE for the \
higher-severity intersection with change-coupling."
},
"helpUri": CODELORE_RESEARCH_FOUNDATIONS_URL,
"properties": {
"precision": "medium",
"tags": ["behavioral", "clone", "type-1", "type-2"]
}
});
let results: Vec<Value> = families
.into_iter()
.map(|(group_id, members)| build_clones_result(group_id, &members, repo_root))
.collect();
json!({
"$schema": SARIF_SCHEMA_URL,
"version": "2.1.0",
"runs": [{
"automationDetails": { "id": automation_id_for(CLONE_AUTOMATION_ID_PREFIX) },
"tool": {
"driver": {
"name": "codelore",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": TOOL_INFO_URI,
"rules": [rule]
}
},
"results": results
}]
})
}
fn build_clones_result(
group_id: u32,
members: &[&ClonesRow],
repo_root: &str,
) -> serde_json::Value {
use serde_json::{Value, json};
let family_size = members.len();
#[allow(clippy::cast_precision_loss)]
let security_severity = (3.0_f64 + family_size as f64).min(6.0);
let level = if family_size >= 5 { "warning" } else { "note" };
let fingerprint = members.first().map_or("", |m| m.fingerprint.as_str());
let locations: Vec<Value> = members
.iter()
.map(|m| {
let artifact_uri = format!(
"{}/{}",
repo_root.trim_end_matches('/'),
percent_encode_path(m.entity.trim_start_matches('/'))
);
json!({
"physicalLocation": {
"artifactLocation": { "uri": artifact_uri },
"region": {
"startLine": m.start_line,
"endLine": m.end_line
}
},
"message": { "text": format!("function: {}", m.function) }
})
})
.collect();
let names = members
.iter()
.map(|m| m.function.as_str())
.collect::<Vec<_>>()
.join(", ");
json!({
"ruleId": CLONE_RULE_ID,
"level": level,
"message": {
"text": format!(
"Clone family of {} functions (similarity {:.2}, {} structural nodes): {}",
family_size,
members.first().map_or(1.0, |m| m.similarity),
members.first().map_or(0, |m| m.node_count),
names
)
},
"locations": locations,
"partialFingerprints": {
"cloneGroupFingerprint/v1": fingerprint,
"cloneGroupId/v1": format!("{group_id}")
},
"properties": {
"security-severity": security_severity,
"codelore/clone-group-id": group_id,
"codelore/family-size": family_size,
"codelore/similarity": members.first().map_or(1.0, |m| m.similarity),
"codelore/node-count": members.first().map_or(0, |m| m.node_count),
"tags": ["behavioral", "clone", "type-1", "type-2"]
}
})
}
use crate::analyses::clone_coupling::CloneCouplingRow;
const LIVE_CLONE_RULE_ID: &str = "CODELORE-LIVE-CLONE";
const LIVE_CLONE_AUTOMATION_ID_PREFIX: &str = "codelore/clone-coupling/run";
pub fn write_clone_coupling_sarif<W: Write>(
rows: &[CloneCouplingRow],
repo_root: &str,
w: &mut W,
) -> Result<()> {
let doc = build_clone_coupling_sarif(rows, repo_root);
serde_json::to_writer_pretty(w, &doc)
.map_err(|e| super::serde_json_io_err("clone-coupling sarif", &e))?;
Ok(())
}
fn build_clone_coupling_sarif(rows: &[CloneCouplingRow], repo_root: &str) -> serde_json::Value {
use serde_json::{Value, json};
let rule = json!({
"id": LIVE_CLONE_RULE_ID,
"shortDescription": {
"text": "Live clone: cloned function whose copies co-change at Fisher-significant rates"
},
"fullDescription": {
"text": "A pair of cloned functions whose containing files are also \
coupled at Fisher-exact p < 0.05. The combined_score \
(similarity × coupling_degree × (1 − p_value)) ranks how \
actionable the finding is. Live clones are real technical \
debt; dead clones (filtered out) are noise."
},
"helpUri": CODELORE_RESEARCH_FOUNDATIONS_URL,
"properties": {
"precision": "high",
"tags": ["behavioral", "clone", "live-clone", "co-change", "x-ray"]
}
});
let results: Vec<Value> = rows
.iter()
.map(|row| build_live_clone_result(row, repo_root))
.collect();
json!({
"$schema": SARIF_SCHEMA_URL,
"version": "2.1.0",
"runs": [{
"automationDetails": { "id": automation_id_for(LIVE_CLONE_AUTOMATION_ID_PREFIX) },
"tool": {
"driver": {
"name": "codelore",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": TOOL_INFO_URI,
"rules": [rule]
}
},
"results": results
}]
})
}
#[allow(clippy::too_many_lines)] fn build_live_clone_result(row: &CloneCouplingRow, repo_root: &str) -> serde_json::Value {
use serde_json::json;
let (
primary_file,
primary_entity,
primary_start,
primary_end,
secondary_file,
secondary_entity,
secondary_start,
secondary_end,
) = if (row.support_a, &row.file_a) >= (row.support_b, &row.file_b) {
(
&row.file_a,
&row.entity_a,
row.start_line_a,
row.end_line_a,
&row.file_b,
&row.entity_b,
row.start_line_b,
row.end_line_b,
)
} else {
(
&row.file_b,
&row.entity_b,
row.start_line_b,
row.end_line_b,
&row.file_a,
&row.entity_a,
row.start_line_a,
row.end_line_a,
)
};
let mk_uri = |p: &str| {
format!(
"{}/{}",
repo_root.trim_end_matches('/'),
percent_encode_path(p.trim_start_matches('/'))
)
};
let security_severity = (row.combined_score * 10.0).clamp(0.0, 10.0);
let level = if security_severity >= 7.0 {
"error"
} else if security_severity >= 4.0 {
"warning"
} else {
"note"
};
let mut pair = [row.file_a.as_str(), row.file_b.as_str()];
pair.sort_unstable();
let file_pair_hash = sha256_prefixed(&pair);
json!({
"ruleId": LIVE_CLONE_RULE_ID,
"level": level,
"message": {
"text": format!(
"Live clone family {} — {} ({}:{}) and {} ({}:{}) co-change at \
{:.0}% degree (combined_score {:.3}; similarity {:.2}, {} shared revs)",
row.clone_group_id,
primary_entity,
primary_file, primary_start,
secondary_entity,
secondary_file, secondary_start,
row.degree_pct * 100.0,
row.combined_score,
row.similarity,
row.shared_revs,
)
},
"locations": [
{
"physicalLocation": {
"artifactLocation": { "uri": mk_uri(primary_file) },
"region": { "startLine": primary_start, "endLine": primary_end }
},
"message": { "text": format!("primary: {primary_entity}") }
},
{
"physicalLocation": {
"artifactLocation": { "uri": mk_uri(secondary_file) },
"region": { "startLine": secondary_start, "endLine": secondary_end }
},
"message": { "text": format!("partner: {secondary_entity}") }
}
],
"partialFingerprints": {
"cloneGroupFingerprint/v1": row.fingerprint,
"filePairHash/v1": file_pair_hash,
"cloneGroupId/v1": format!("{}", row.clone_group_id)
},
"properties": {
"security-severity": security_severity,
"codelore/clone-group-id": row.clone_group_id,
"codelore/similarity": row.similarity,
"codelore/shared-revs": row.shared_revs,
"codelore/degree-pct": row.degree_pct,
"codelore/p-value": row.p_value,
"codelore/combined-score": row.combined_score,
"tags": ["behavioral", "clone", "live-clone", "co-change", "x-ray"]
}
})
}
const CHECK_AUTOMATION_ID_PREFIX: &str = "codelore/check/run";
pub fn write_check_sarif<W: Write, S: std::hash::BuildHasher>(
violations: &[GateViolation],
evidence: &HashMap<String, Vec<EvidenceCommit>, S>,
repo_root: &Path,
head_sha: &str,
w: &mut W,
) -> Result<()> {
let doc = build_check_sarif(violations, evidence, repo_root, head_sha);
serde_json::to_writer_pretty(w, &doc)
.map_err(|e| super::serde_json_io_err("check sarif", &e))?;
Ok(())
}
fn build_check_sarif<S: std::hash::BuildHasher>(
violations: &[GateViolation],
evidence: &HashMap<String, Vec<EvidenceCommit>, S>,
repo_root: &Path,
head_sha: &str,
) -> serde_json::Value {
use serde_json::{Value, json};
use std::collections::BTreeSet;
let distinct_gates: BTreeSet<&str> = violations.iter().map(|v| v.gate.as_str()).collect();
let rules: Vec<Value> = distinct_gates
.iter()
.map(|gate| {
json!({
"id": gate,
"shortDescription": {
"text": format!("Quality gate: {gate}")
},
"helpUri": "https://github.com/emrecdr/codelore/blob/main/docs/advanced-usage.md#check-gates"
})
})
.collect();
let repo_root_str = repo_root.to_string_lossy();
let results: Vec<Value> = violations
.iter()
.map(|v| build_check_result(v, evidence, &repo_root_str, head_sha))
.collect();
json!({
"$schema": SARIF_SCHEMA_URL,
"version": "2.1.0",
"runs": [{
"automationDetails": {
"id": automation_id_for(CHECK_AUTOMATION_ID_PREFIX)
},
"tool": {
"driver": {
"name": "codelore",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": TOOL_INFO_URI,
"rules": rules
}
},
"results": results
}]
})
}
#[must_use]
pub fn evidence_attachments(
locs: Vec<serde_json::Value>,
) -> (serde_json::Value, serde_json::Value) {
use serde_json::json;
let thread_flow_locs: Vec<serde_json::Value> =
locs.iter().map(|loc| json!({ "location": loc })).collect();
let code_flows = json!([{ "threadFlows": [{ "locations": thread_flow_locs }] }]);
(code_flows, serde_json::Value::Array(locs))
}
#[must_use]
pub fn evidence_location(path: &str, message: &str, with_region: bool) -> serde_json::Value {
use serde_json::json;
let uri = percent_encode_path(path.trim_start_matches('/'));
let physical = if with_region {
json!({
"artifactLocation": { "uri": uri },
"region": { "startLine": 1 }
})
} else {
json!({ "artifactLocation": { "uri": uri } })
};
json!({
"physicalLocation": physical,
"message": { "text": message }
})
}
fn build_check_result<S: std::hash::BuildHasher>(
v: &GateViolation,
evidence: &HashMap<String, Vec<EvidenceCommit>, S>,
repo_root: &str,
head_sha: &str,
) -> serde_json::Value {
use serde_json::json;
let is_repo_wide = crate::quality_gates::evaluators::is_pseudo_path(&v.path);
let uri = if is_repo_wide {
".".to_owned()
} else {
percent_encode_path(v.path.trim_start_matches('/'))
};
let location = json!({ "physicalLocation": { "artifactLocation": { "uri": uri } } });
let gate_fp = sha256_prefixed(&[&v.gate, &v.path, head_sha]);
let primary_fp = primary_location_line_hash(repo_root, &v.path);
let message_text = format!(
"{gate}: {actual} vs threshold {threshold}",
gate = v.gate,
actual = v.actual,
threshold = v.threshold,
);
let chain: &[EvidenceCommit] = if is_repo_wide {
&[]
} else {
evidence.get(&v.path).map_or(&[], Vec::as_slice)
};
let evidence_locs: Vec<serde_json::Value> = chain
.iter()
.map(|c| {
let message = format!(
"{date} {author}: {msg} (+{churn} lines)",
date = c.date,
author = c.author,
msg = c.message_head,
churn = c.churn,
);
evidence_location(&v.path, &message, false)
})
.collect();
let mut result = json!({
"ruleId": v.gate,
"level": "error",
"message": { "text": message_text },
"locations": [location],
"partialFingerprints": {
"gateFinding/v1": gate_fp,
"primaryLocationLineHash": primary_fp
}
});
if !evidence_locs.is_empty() {
let (code_flows, related_locations) = evidence_attachments(evidence_locs);
result["relatedLocations"] = related_locations;
result["codeFlows"] = code_flows;
}
result
}