use std::collections::{BTreeSet, HashMap};
use crate::graph;
use super::index::{def_files, GraphIndex};
use super::test_path;
use super::ts_edges;
pub const NAME_HINT_REFERENCE_CAP: usize = 5;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NameHintRow {
pub file: String,
pub references: Vec<String>,
pub reference_total: usize,
}
const AFFIXES: [&str; 3] = ["tests", "test", "spec"];
fn normalize(s: &str) -> String {
s.chars()
.filter(|c| !matches!(c, '-' | '_' | '.'))
.flat_map(char::to_lowercase)
.collect()
}
fn stem(file: &str) -> Option<&str> {
let segment = file.rsplit('/').next()?;
let dot = segment.rfind('.')?;
(dot > 0).then(|| &segment[..dot])
}
pub fn stem_matches(name: &str, file: &str) -> bool {
let Some(stem) = stem(file) else {
return false;
};
let normalized = normalize(stem);
let wanted = normalize(name);
AFFIXES
.iter()
.any(|affix| normalized.strip_suffix(affix) == Some(wanted.as_str()))
}
fn edge_targets(e: &graph::Edge) -> Vec<&str> {
match e {
graph::Edge::Inherits { to, .. }
| graph::Edge::UsesType { to, .. }
| graph::Edge::UsesMember { to, .. }
| graph::Edge::Implements { to, .. }
| graph::Edge::Overrides { to, .. } => vec![to.as_str()],
other => ts_edges::admitted_targets(other).unwrap_or_default(),
}
}
fn is_production_target(index: &GraphIndex, to: &str) -> bool {
index
.by_id
.get(to)
.is_some_and(|&i| !index.is_test_file(&index.graph.defs[i].file))
}
fn candidate_files<'a>(index: &'a GraphIndex<'_>) -> BTreeSet<&'a str> {
index
.by_file
.keys()
.chain(index.test_defs_by_file.keys())
.chain(index.outbound_by_file.keys())
.chain(index.heuristic_outbound_by_file.keys())
.map(String::as_str)
.collect()
}
fn name_hint_row(index: &GraphIndex, file: &str) -> NameHintRow {
let mut counts: HashMap<&str, usize> = HashMap::new();
if let Some(out) = index.outbound_by_file.get(file) {
let precise = out
.inherits
.iter()
.chain(&out.uses_type)
.chain(&out.uses_member)
.chain(&out.implements)
.chain(&out.overrides)
.chain(&out.ts.call)
.chain(&out.ts.jsx_use)
.chain(&out.ts.dispatch)
.chain(&out.ts.import);
for &i in precise {
for to in edge_targets(&index.graph.edges[i]) {
if is_production_target(index, to) {
*counts.entry(to).or_default() += 1;
}
}
}
}
let reference_total = counts.len();
let mut ranked: Vec<(&str, usize)> = counts.into_iter().collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
let references = ranked
.into_iter()
.take(NAME_HINT_REFERENCE_CAP)
.map(|(id, _)| id.to_string())
.collect();
NameHintRow {
file: file.to_string(),
references,
reference_total,
}
}
pub fn name_hint_rows(index: &GraphIndex, id: &str) -> Vec<NameHintRow> {
let Some(&sym) = index.by_id.get(id) else {
return Vec::new();
};
let name = index.graph.defs[sym].name.clone();
let own = def_files(index, id);
candidate_files(index)
.into_iter()
.filter(|file| !own.iter().any(|f| f == file))
.filter(|file| stem_matches(&name, file))
.filter(|file| test_path::test_via(index, file).is_some())
.map(|file| name_hint_row(index, file))
.collect()
}