use std::collections::{HashMap, HashSet};
use crate::complexity::{Tier1Language, compute_for_file};
use crate::facts::FactsDb;
use crate::facts::ingest::consumer::dedup_entities;
use crate::repo::Repo;
use crate::{CodeLoreError, Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FunctionXrayRow {
pub function: String,
pub change_freq: u32,
pub loc: u32,
pub cyclomatic: Option<i64>,
pub cognitive: Option<i64>,
pub last_changed: String,
}
#[tracing::instrument(name = "function-xray", skip_all, fields(target = target))]
pub fn run_function_xray<R: Repo>(
db: &FactsDb,
repo: &R,
opts: &Options,
target: &str,
) -> Result<Vec<FunctionXrayRow>> {
let head_spans = extract_head_spans(repo, target)?;
if head_spans.is_empty() {
return Ok(Vec::new());
}
let hunk_rows = fetch_hunks_for_path(db, target)?;
let mut freq: HashMap<String, (u32, String)> = head_spans
.iter()
.map(|(name, _, _)| (name.clone(), (0u32, String::new())))
.collect();
let mut counted: HashSet<(String, String)> = HashSet::new();
for (rev, date, new_start, new_lines) in &hunk_rows {
let (new_start, new_lines) = (*new_start, *new_lines);
for (name, start_line, end_line) in &head_spans {
if hunk_overlaps(*start_line, *end_line, new_start, new_lines) {
let key = (name.clone(), rev.clone());
if counted.insert(key) {
let entry = freq.get_mut(name).expect("name always present");
entry.0 += 1;
if date.as_str() > entry.1.as_str() {
entry.1.clone_from(date);
}
}
}
}
}
let metrics = fetch_head_metrics(db, target)?;
let mut rows: Vec<FunctionXrayRow> = head_spans
.iter()
.map(|(name, _, _)| {
let (change_freq, last_changed) = freq.get(name).cloned().unwrap_or((0, String::new()));
let (loc, cyclomatic, cognitive) =
metrics.get(name).copied().unwrap_or((0, None, None));
FunctionXrayRow {
function: name.clone(),
change_freq,
loc,
cyclomatic,
cognitive,
last_changed,
}
})
.collect();
rows.sort_unstable_by(|a, b| {
b.change_freq
.cmp(&a.change_freq)
.then_with(|| a.function.cmp(&b.function))
});
if let Some(limit) = opts.rows_limit {
rows.truncate(limit as usize);
}
Ok(rows)
}
pub(crate) fn hunk_overlaps(
start_line: u32,
end_line: u32,
new_start: u32,
new_lines: u32,
) -> bool {
if new_lines == 0 {
new_start >= start_line && new_start <= end_line
} else {
new_start <= end_line && new_start + new_lines > start_line
}
}
pub(super) fn extract_head_spans<R: Repo>(
repo: &R,
target: &str,
) -> Result<Vec<(String, u32, u32)>> {
let Some(lang) = Tier1Language::from_path(target) else {
return Ok(Vec::new());
};
let source = match repo.read_blob_at("HEAD", target) {
Ok(Some(b)) => b,
Ok(None) => {
tracing::debug!("function-xray: {target} not tracked at HEAD");
return Ok(Vec::new());
}
Err(e) => {
return Err(CodeLoreError::Analysis(format!(
"function-xray: blob read failed for {target}: {e}"
)));
}
};
if source.len() > crate::constants::DEFAULT_MAX_AST_FILE_BYTES {
return Err(CodeLoreError::Analysis(format!(
"function-xray: {target} exceeds {}-byte AST cap",
crate::constants::DEFAULT_MAX_AST_FILE_BYTES
)));
}
let path = std::path::Path::new(target);
let entities = compute_for_file(path, source, lang)?;
let deduped = dedup_entities(entities);
let spans: Vec<(String, u32, u32)> = deduped
.into_iter()
.filter(|e| e.kind == "function" || e.kind == "method")
.map(|e| (e.name, e.start_line, e.end_line))
.collect();
Ok(spans)
}
pub(super) fn fetch_hunks_for_path(
db: &FactsDb,
target: &str,
) -> Result<Vec<(String, String, u32, u32)>> {
use crate::analyses::query::query_map_collect;
query_map_collect(
db,
"SELECT h.rev, CAST(c.date AS TEXT), h.new_start, h.new_lines
FROM hunks h
JOIN commits c ON c.rev = h.rev
WHERE h.path = ?
ORDER BY c.date",
duckdb::params![target],
"function-xray:hunks",
|r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, u32>(2)?,
r.get::<_, u32>(3)?,
))
},
)
}
type FnMetrics = (u32, Option<i64>, Option<i64>);
fn fetch_head_metrics(db: &FactsDb, target: &str) -> Result<HashMap<String, FnMetrics>> {
use crate::analyses::query::query_map_collect;
let rows: Vec<(String, u32, Option<i64>, Option<i64>)> = query_map_collect(
db,
"SELECT name, COALESCE(sloc, 0), cyclomatic, cognitive
FROM complexity_metrics
WHERE path = ?
GROUP BY name, sloc, cyclomatic, cognitive",
duckdb::params![target],
"function-xray:metrics",
|r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, u32>(1)?,
r.get::<_, Option<i64>>(2)?,
r.get::<_, Option<i64>>(3)?,
))
},
)?;
let mut map: HashMap<String, FnMetrics> = HashMap::new();
for (name, sloc, cy, cog) in rows {
map.entry(name)
.and_modify(|e| {
if sloc > e.0 {
*e = (sloc, cy, cog);
}
})
.or_insert((sloc, cy, cog));
}
Ok(map)
}
pub(super) fn rev_to_function_sets<R: Repo>(
db: &FactsDb,
repo: &R,
target: &str,
) -> Result<HashMap<String, HashSet<String>>> {
let head_spans = extract_head_spans(repo, target)?;
if head_spans.is_empty() {
return Ok(HashMap::new());
}
let hunk_rows = fetch_hunks_for_path(db, target)?;
let mut rev_sets: HashMap<String, HashSet<String>> = HashMap::new();
for (rev, _date, new_start, new_lines) in &hunk_rows {
let (new_start, new_lines) = (*new_start, *new_lines);
for (name, start_line, end_line) in &head_spans {
if hunk_overlaps(*start_line, *end_line, new_start, new_lines) {
rev_sets
.entry(rev.clone())
.or_default()
.insert(name.clone());
}
}
}
Ok(rev_sets)
}
#[cfg(test)]
mod tests {
use super::hunk_overlaps;
struct Case {
label: &'static str,
start: u32,
end: u32,
new_start: u32,
new_lines: u32,
expected: bool,
}
#[test]
#[allow(clippy::too_many_lines)]
fn overlap_predicate() {
let cases = vec![
Case {
label: "hunk fully inside function",
start: 10,
end: 20,
new_start: 12,
new_lines: 3,
expected: true,
},
Case {
label: "hunk starts before and overlaps",
start: 10,
end: 20,
new_start: 8,
new_lines: 5,
expected: true,
},
Case {
label: "hunk starts after and overlaps",
start: 10,
end: 20,
new_start: 18,
new_lines: 5,
expected: true,
},
Case {
label: "hunk completely before function",
start: 10,
end: 20,
new_start: 3,
new_lines: 5,
expected: false,
},
Case {
label: "hunk completely after function",
start: 10,
end: 20,
new_start: 22,
new_lines: 5,
expected: false,
},
Case {
label: "hunk ends exactly at function start",
start: 10,
end: 20,
new_start: 8,
new_lines: 2, expected: false,
},
Case {
label: "hunk starts exactly at function end (inclusive overlap)",
start: 10,
end: 20,
new_start: 20,
new_lines: 1,
expected: true,
},
Case {
label: "pure deletion inside function",
start: 10,
end: 20,
new_start: 15,
new_lines: 0,
expected: true,
},
Case {
label: "pure deletion at function start",
start: 10,
end: 20,
new_start: 10,
new_lines: 0,
expected: true,
},
Case {
label: "pure deletion at function end",
start: 10,
end: 20,
new_start: 20,
new_lines: 0,
expected: true,
},
Case {
label: "pure deletion before function",
start: 10,
end: 20,
new_start: 9,
new_lines: 0,
expected: false,
},
Case {
label: "pure deletion after function",
start: 10,
end: 20,
new_start: 21,
new_lines: 0,
expected: false,
},
];
for c in &cases {
assert_eq!(
hunk_overlaps(c.start, c.end, c.new_start, c.new_lines),
c.expected,
"FAILED: {}",
c.label
);
}
}
}