use std::collections::BTreeSet;
use std::fmt;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FindingKey {
pub kind: String,
pub path: String,
pub symbol: String,
pub line: u64,
}
impl fmt::Display for FindingKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}:{}", self.kind, self.path, self.line)?;
if !self.symbol.is_empty() {
write!(f, " {}", self.symbol)?;
}
Ok(())
}
}
pub type KeySet = BTreeSet<FindingKey>;
const DEAD_CODE_NON_FINDING_ARRAYS: &[&str] = &["workspace_diagnostics", "next_steps"];
const SYMBOL_FIELDS: &[&str] = &[
"export_name",
"package_name",
"member_name",
"name",
"specifier",
"entry_name",
"catalog_name",
];
pub const DUPLICATION_KIND: &str = "code-duplication";
pub const COMPLEXITY_KIND: &str = "complexity";
pub fn dead_code_keys(envelope: &Value) -> KeySet {
dead_code_findings(envelope)
.map(|(kind, item)| dead_code_key(kind, item))
.collect()
}
fn dead_code_findings(envelope: &Value) -> impl Iterator<Item = (&str, &Value)> {
envelope
.as_object()
.into_iter()
.flatten()
.filter(|(kind, _)| !DEAD_CODE_NON_FINDING_ARRAYS.contains(&kind.as_str()))
.filter_map(|(kind, value)| Some((kind.as_str(), value.as_array()?)))
.flat_map(|(kind, items)| {
items
.iter()
.filter(|item| item.is_object())
.map(move |item| (kind, item))
})
}
pub type IdentifiedFinding = (FindingKey, Option<String>);
pub fn dead_code_finding_ids(envelope: &Value) -> Vec<IdentifiedFinding> {
let mut findings: Vec<IdentifiedFinding> = dead_code_findings(envelope)
.map(|(kind, item)| {
(
dead_code_key(kind, item),
item["finding_id"].as_str().map(str::to_string),
)
})
.collect();
findings.sort();
findings
}
pub fn security_finding_ids(envelope: &Value) -> Vec<IdentifiedFinding> {
let mut findings: Vec<IdentifiedFinding> = envelope["security_findings"]
.as_array()
.into_iter()
.flatten()
.map(|item| {
let rule = item["category"]
.as_str()
.or_else(|| item["kind"].as_str())
.unwrap_or_default();
(
FindingKey {
kind: format!("security/{rule}"),
path: item["path"].as_str().unwrap_or_default().to_string(),
symbol: format!("col {}", item["col"].as_u64().unwrap_or(0)),
line: item["line"].as_u64().unwrap_or(0),
},
item["finding_id"]
.as_str()
.filter(|id| !id.is_empty())
.map(str::to_string),
)
})
.collect();
findings.sort();
findings
}
fn dead_code_key(kind: &str, item: &Value) -> FindingKey {
if let Some(key) = package_cycle_key(kind, item) {
return key;
}
let path = item["path"]
.as_str()
.map_or_else(|| joined_strings(&item["files"]), str::to_string);
let symbol = SYMBOL_FIELDS
.iter()
.find_map(|field| item[*field].as_str())
.map(|symbol| match item["parent_name"].as_str() {
Some(parent) => format!("{parent}.{symbol}"),
None => symbol.to_string(),
})
.or_else(|| suppression_symbol(item))
.unwrap_or_default();
FindingKey {
kind: kind.to_string(),
path,
symbol,
line: item["line"].as_u64().unwrap_or(0),
}
}
fn package_cycle_key(kind: &str, item: &Value) -> Option<FindingKey> {
let packages = item["packages"].as_array()?;
let mut files: Vec<&str> = item["edges"]
.as_array()
.into_iter()
.flatten()
.filter_map(|edge| edge["path"].as_str())
.collect();
files.sort_unstable();
files.dedup();
Some(FindingKey {
kind: kind.to_string(),
path: files.join(" -> "),
symbol: packages
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join(" -> "),
line: 0,
})
}
fn suppression_symbol(item: &Value) -> Option<String> {
let origin = item["origin"].as_object()?;
let target = origin
.get("issue_kind")
.or_else(|| origin.get("export_name"))
.and_then(Value::as_str)
.unwrap_or_default();
let scope = if origin.get("is_file_level").and_then(Value::as_bool) == Some(true) {
"file"
} else {
"line"
};
let missing_reason = item["missing_reason"].as_bool() == Some(true);
Some(format!(
"{}:{scope}:{target}{}",
origin
.get("type")
.and_then(Value::as_str)
.unwrap_or_default(),
if missing_reason {
":missing-reason"
} else {
""
}
))
}
fn joined_strings(value: &Value) -> String {
value.as_array().map_or_else(String::new, |items| {
items
.iter()
.filter_map(|item| item.as_str().or_else(|| item["path"].as_str()))
.collect::<Vec<_>>()
.join(" -> ")
})
}
pub fn dupes_keys(envelope: &Value) -> KeySet {
let mut keys = KeySet::new();
for group in envelope["clone_groups"].as_array().into_iter().flatten() {
let mut instances: Vec<(String, u64, u64, u64, u64)> = group["instances"]
.as_array()
.into_iter()
.flatten()
.map(|instance| {
let number = |field: &str| instance[field].as_u64().unwrap_or(0);
(
instance["file"].as_str().unwrap_or_default().to_string(),
number("start_line"),
number("end_line"),
number("start_col"),
number("end_col"),
)
})
.collect();
instances.sort();
let Some(first) = instances.first() else {
continue;
};
let mut files: Vec<&str> = instances.iter().map(|(file, ..)| file.as_str()).collect();
files.dedup();
keys.insert(FindingKey {
kind: DUPLICATION_KIND.to_string(),
path: files.join(" -> "),
symbol: instances
.iter()
.map(|(file, start, end, start_col, end_col)| {
format!("{file}:{start}-{end}@{start_col}-{end_col}")
})
.collect::<Vec<_>>()
.join(" | "),
line: first.1,
});
}
keys
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CloneInstance<'a> {
pub path: &'a str,
pub start_line: u64,
pub end_line: u64,
pub start_col: u64,
pub end_col: u64,
}
pub fn clone_instances(symbol: &str) -> impl Iterator<Item = Option<CloneInstance<'_>>> {
symbol.split(" | ").map(|instance| {
let (location, columns) = instance.rsplit_once('@')?;
let (path, lines) = location.rsplit_once(':')?;
let (start_line, end_line) = lines.split_once('-')?;
let (start_col, end_col) = columns.split_once('-')?;
Some(CloneInstance {
path,
start_line: start_line.parse().ok()?,
end_line: end_line.parse().ok()?,
start_col: start_col.parse().ok()?,
end_col: end_col.parse().ok()?,
})
})
}
pub fn health_keys(envelope: &Value) -> KeySet {
envelope["findings"]
.as_array()
.into_iter()
.flatten()
.map(|finding| FindingKey {
kind: COMPLEXITY_KIND.to_string(),
path: finding["path"].as_str().unwrap_or_default().to_string(),
symbol: finding["name"].as_str().unwrap_or_default().to_string(),
line: finding["line"].as_u64().unwrap_or(0),
})
.collect()
}
pub fn combined_keys(envelope: &Value) -> SectionKeys {
SectionKeys {
dead_code: dead_code_keys(&envelope["check"]),
dupes: dupes_keys(&envelope["dupes"]),
health: health_keys(&envelope["health"]),
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SectionKeys {
pub dead_code: KeySet,
pub dupes: KeySet,
pub health: KeySet,
}
impl SectionKeys {
pub fn all(&self) -> KeySet {
self.dead_code
.iter()
.chain(&self.dupes)
.chain(&self.health)
.cloned()
.collect()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AuditKeys {
pub introduced: KeySet,
pub inherited: KeySet,
pub verdict: String,
}
type Normalizer = fn(&Value) -> KeySet;
pub fn audit_keys(envelope: &Value) -> AuditKeys {
let mut keys = AuditKeys {
verdict: envelope["verdict"].as_str().unwrap_or_default().to_string(),
..AuditKeys::default()
};
let sections: [(&str, Normalizer); 3] = [
("dead_code", dead_code_keys),
("duplication", dupes_keys),
("complexity", health_keys),
];
for (section, normalize) in sections {
let Some(map) = envelope[section].as_object() else {
continue;
};
for (field, value) in map {
let Some(items) = value.as_array() else {
continue;
};
for item in items {
let mut single = serde_json::Map::new();
single.insert(field.clone(), Value::Array(vec![item.clone()]));
let target = if item["introduced"].as_bool() == Some(false) {
&mut keys.inherited
} else {
&mut keys.introduced
};
target.extend(normalize(&Value::Object(single)));
}
}
}
keys
}
pub fn envelope_keys(envelope: &Value) -> KeySet {
match envelope["kind"].as_str() {
Some("dead-code" | "architecture") => dead_code_keys(envelope),
Some("dupes") => dupes_keys(envelope),
Some("health") => health_keys(envelope),
Some("combined") => combined_keys(envelope).all(),
Some("audit") => {
let keys = audit_keys(envelope);
keys.introduced.union(&keys.inherited).cloned().collect()
}
other => panic!("no normalizer for envelope kind {other:?}: {envelope}"),
}
}
pub fn mcp_result_envelope(result: &Value) -> Value {
assert_ne!(result["isError"], true, "MCP tool call failed: {result}");
let text = result["content"][0]["text"]
.as_str()
.unwrap_or_else(|| panic!("MCP result has no text content: {result}"));
serde_json::from_str(text)
.unwrap_or_else(|err| panic!("MCP result text is not JSON ({err}): {text}"))
}
pub fn render(keys: &KeySet) -> String {
if keys.is_empty() {
return " (none)".to_string();
}
keys.iter()
.map(|key| format!(" {key}"))
.collect::<Vec<_>>()
.join("\n")
}