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 {
let mut keys = KeySet::new();
let Some(map) = envelope.as_object() else {
return keys;
};
for (kind, value) in map {
if DEAD_CODE_NON_FINDING_ARRAYS.contains(&kind.as_str()) {
continue;
}
let Some(items) = value.as_array() else {
continue;
};
for item in items.iter().filter(|item| item.is_object()) {
keys.insert(dead_code_key(kind, item));
}
}
keys
}
fn dead_code_key(kind: &str, item: &Value) -> FindingKey {
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 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") => 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")
}