use std::collections::{HashMap, HashSet};
use crate::graph;
use super::dispatch::{implemented_interfaces, inbound_walk_kinds};
use super::impact_why::{self, Why, WhyTrack};
use super::index::{def_files, GraphIndex};
use super::infra::is_infra_file;
use super::member::{self, MemberCandidate, MemberSeedResolution};
use super::rank::{personalized_page_rank, DEFAULT_DAMPING, DEFAULT_ITERATIONS};
use super::refs_tables::{cap_rows, edge_loc, row_tier};
use super::seq::{SeqMap, SeqSet};
use super::symbol::{resolve_symbol, Resolution};
pub const DEFAULT_HOPS: u32 = 2;
pub const DEFAULT_IFACE_MAX_FANIN: usize = 8;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct KindLines {
pub direct: usize,
pub direct_amb: usize,
pub ctor_di: usize,
pub heuristic: usize,
pub iface: usize,
}
fn note_line(slot: &mut usize, line: usize) {
if line > 0 && (*slot == 0 || line < *slot) {
*slot = line;
}
}
#[derive(Debug, Clone, Default)]
pub(super) struct Hit {
pub(super) via_count: u32,
pub(super) ambiguous_count: u32,
pub(super) heuristic_count: u32,
pub(super) ext_count: u32,
pub(super) symbols: SeqSet<String>,
pub(super) iface_via: SeqSet<String>,
pub(super) lines: KindLines,
pub(super) why_track: WhyTrack,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VisitedEntry {
pub hop: u32,
pub via_count: u32,
pub ambiguous_count: u32,
pub heuristic_count: u32,
pub ext_count: u32,
pub symbols: Vec<String>,
pub iface_via: Vec<String>,
pub lines: KindLines,
pub infra: bool,
pub why: Why,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BrakedIface {
pub iface: String,
pub fanin: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BrakedFile {
pub file: String,
pub indegree: usize,
}
pub struct ImpactWalkResult {
pub visited: SeqMap<VisitedEntry>,
pub fwd_adj: HashMap<String, HashSet<String>>,
pub seed_files: SeqSet<String>,
pub braked: Vec<BrakedIface>,
pub braked_files: Vec<BrakedFile>,
}
fn add_adj(fwd_adj: &mut HashMap<String, HashSet<String>>, from: &str, to: &str) {
fwd_adj
.entry(from.to_string())
.or_default()
.insert(to.to_string());
}
#[allow(
clippy::too_many_lines,
clippy::cognitive_complexity,
reason = "one breadth-first walk with both widening brakes inline; the brakes only work if they see the same frontier the walk itself sees"
)]
pub fn impact_walk(
index: &GraphIndex,
seed_ids: &[String],
hops: u32,
iface: bool,
iface_max_fanin: usize,
hub_max_indegree: usize,
) -> ImpactWalkResult {
let mut visited: SeqMap<VisitedEntry> = SeqMap::new();
let mut braked: HashMap<String, usize> = HashMap::new();
let mut braked_files: HashMap<String, usize> = HashMap::new();
let mut seed_files: SeqSet<String> = SeqSet::new();
for id in seed_ids {
for f in def_files(index, id) {
seed_files.insert(f);
}
}
let mut fwd_adj: HashMap<String, HashSet<String>> = HashMap::new();
let mut frontier: SeqSet<String> = SeqSet::new();
for id in seed_ids {
frontier.insert(id.clone());
}
let mut seen_defs: HashSet<String> = seed_ids.iter().cloned().collect();
let mut hop = 1u32;
while hop <= hops && frontier.len() > 0 {
let mut hits: SeqMap<Hit> = SeqMap::new();
for def_id in frontier.iter() {
if let Some(inb) = index.inbound.get(def_id) {
for kind_edges in inbound_walk_kinds(inb) {
for &ei in kind_edges {
let (loc_file, loc_line) = edge_loc(&index.graph.edges[ei]);
let from_file = loc_file.to_string();
{
let h = hits.get_or_insert_default(&from_file);
h.via_count += 1;
h.symbols.insert(def_id.clone());
h.note_direct(loc_line, &index.graph.edges[ei]);
}
for sf in def_files(index, def_id) {
add_adj(&mut fwd_adj, &sf, &from_file);
}
}
}
}
if let Some(amb_idxs) = index.ambiguous_by_candidate.get(def_id) {
for &ei in amb_idxs {
let (loc_file, loc_line) = edge_loc(&index.graph.edges[ei]);
let from_file = loc_file.to_string();
{
let h = hits.get_or_insert_default(&from_file);
h.ambiguous_count += 1;
h.note_direct_amb(loc_line, &index.graph.edges[ei]);
}
for sf in def_files(index, def_id) {
add_adj(&mut fwd_adj, &sf, &from_file);
}
}
}
if iface {
let mut seen_sites: HashSet<(String, usize)> = HashSet::new();
if let Some(ctor_idxs) = index.ctor_di_by_to.get(def_id) {
for &ei in ctor_idxs {
let graph::Edge::CtorDi {
from_file,
from_line,
iface: iface_name,
..
} = &index.graph.edges[ei]
else {
continue;
};
if brake_fanin(index, iface_max_fanin, iface_name, &mut braked) {
continue;
}
let from_file = from_file.clone();
{
let h = hits.get_or_insert_default(&from_file);
h.via_count += 1;
h.symbols.insert(def_id.clone());
h.iface_via.insert(format!("{iface_name} (ctor-di)"));
note_line(&mut h.lines.ctor_di, *from_line);
}
seen_sites.insert((from_file.clone(), *from_line));
for sf in def_files(index, def_id) {
add_adj(&mut fwd_adj, &sf, &from_file);
}
}
}
for iface_id in implemented_interfaces(index, def_id) {
let Some(iface_inb) = index.inbound.get(&iface_id) else {
continue;
};
let iface_name = index
.def(&iface_id)
.map(|d| d.name.clone())
.unwrap_or_else(|| iface_id.clone());
if brake_fanin(index, iface_max_fanin, &iface_name, &mut braked) {
continue;
}
for kind_edges in [&iface_inb.uses_type, &iface_inb.uses_member] {
for &ei in kind_edges {
let (loc_file, loc_line) = edge_loc(&index.graph.edges[ei]);
let site = (loc_file.to_string(), loc_line);
if seen_sites.contains(&site) {
continue;
}
let from_file = site.0.clone();
seen_sites.insert(site);
{
let h = hits.get_or_insert_default(&from_file);
h.via_count += 1;
h.symbols.insert(def_id.clone());
h.iface_via.insert(iface_name.clone());
h.note_iface(loc_line, &index.graph.edges[ei]);
}
for sf in def_files(index, def_id) {
add_adj(&mut fwd_adj, &sf, &from_file);
}
}
}
}
}
if let Some(hinb) = index.heuristic_inbound.get(def_id) {
for kind_edges in [&hinb.inherits, &hinb.uses_type, &hinb.uses_member] {
for &ei in kind_edges {
let (loc_file, loc_line) = edge_loc(&index.graph.edges[ei]);
let from_file = loc_file.to_string();
let h = hits.get_or_insert_default(&from_file);
h.heuristic_count += 1;
if index.graph.edges[ei].tier() == Some(graph::HeuristicTier::Ext) {
h.ext_count += 1;
}
h.symbols.insert(def_id.clone());
h.note_heuristic(loc_line, &index.graph.edges[ei]);
}
}
}
}
let mut next_frontier: SeqSet<String> = SeqSet::new();
for (file, h) in hits.iter() {
let indegree = index.hub_indegree.get(file).copied().unwrap_or(0);
let hub = is_infra_file(file) || (hub_max_indegree > 0 && indegree >= hub_max_indegree);
if !visited.contains_key(file) && !seed_files.contains(file) {
visited.insert(file.clone(), impact_why::build_visited_entry(hop, hub, h));
}
if h.via_count == 0 && h.ambiguous_count == 0 {
continue;
}
if hub {
if hop < hops {
braked_files.insert(file.clone(), indegree);
}
continue;
}
if let Some(def_idxs) = index.by_file.get(file) {
for &di in def_idxs {
let def_id = &index.graph.defs[di].id;
if !seen_defs.contains(def_id) {
seen_defs.insert(def_id.clone());
next_frontier.insert(def_id.clone());
}
}
}
}
frontier = next_frontier;
hop += 1;
}
let mut braked: Vec<BrakedIface> = braked
.into_iter()
.map(|(iface, fanin)| BrakedIface { iface, fanin })
.collect();
braked.sort_by(|a, b| b.fanin.cmp(&a.fanin).then_with(|| a.iface.cmp(&b.iface)));
let mut braked_files: Vec<BrakedFile> = braked_files
.into_iter()
.map(|(file, indegree)| BrakedFile { file, indegree })
.collect();
braked_files.sort_by(|a, b| {
b.indegree
.cmp(&a.indegree)
.then_with(|| a.file.cmp(&b.file))
});
ImpactWalkResult {
visited,
fwd_adj,
seed_files,
braked,
braked_files,
}
}
fn brake_fanin(
index: &GraphIndex,
iface_max_fanin: usize,
name: &str,
braked: &mut HashMap<String, usize>,
) -> bool {
if iface_max_fanin == 0 {
return false;
}
let fanin = index.ctor_di_fanin.get(name).copied().unwrap_or(0);
if fanin <= iface_max_fanin {
return false;
}
braked.insert(name.to_string(), fanin);
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeedKind {
File,
Symbol,
}
pub fn looks_like_file_path(arg: &str) -> bool {
if arg.contains('/') {
return true;
}
match arg.rfind('.') {
Some(pos) => {
let ext = &arg[pos + 1..];
!ext.is_empty() && ext.chars().all(|c| c.is_ascii_alphanumeric())
}
None => false,
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SeedResolution {
Resolved {
kind: SeedKind,
ids: Vec<String>,
},
Ambiguous {
kind: SeedKind,
ids: Vec<String>,
},
NotFound {
kind: SeedKind,
},
MemberAmbiguous(Vec<MemberCandidate>),
}
fn member_fallback(index: &GraphIndex, arg: &str, not_found_kind: SeedKind) -> SeedResolution {
match member::resolve_member_seed(index, arg) {
MemberSeedResolution::Resolved(id) => SeedResolution::Resolved {
kind: SeedKind::Symbol,
ids: vec![id],
},
MemberSeedResolution::Ambiguous(candidates) => SeedResolution::MemberAmbiguous(candidates),
MemberSeedResolution::NotFound => SeedResolution::NotFound {
kind: not_found_kind,
},
}
}
pub fn resolve_impact_seed(index: &GraphIndex, arg: &str) -> SeedResolution {
if looks_like_file_path(arg) {
return match index.by_file.get(arg) {
Some(ids) if !ids.is_empty() => SeedResolution::Resolved {
kind: SeedKind::File,
ids: ids
.iter()
.map(|&i| index.graph.defs[i].id.clone())
.collect(),
},
_ => member_fallback(index, arg, SeedKind::File),
};
}
match resolve_symbol(index, arg) {
Resolution::Resolved(id) => SeedResolution::Resolved {
kind: SeedKind::Symbol,
ids: vec![id],
},
Resolution::Ambiguous(ids) => SeedResolution::Ambiguous {
kind: SeedKind::Symbol,
ids,
},
Resolution::NotFound => member_fallback(index, arg, SeedKind::Symbol),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImpactRow {
pub file: String,
pub hop: u32,
pub via_count: u32,
pub ambiguous_count: u32,
pub top_symbols: Vec<String>,
pub top_symbols_more: usize,
pub score: f64,
pub heuristic_count: u32,
pub heuristic: bool,
pub tier: Option<graph::HeuristicTier>,
pub iface_via: Vec<String>,
pub from_lines: Vec<(&'static str, usize)>,
pub infra: bool,
pub why: Why,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImpactModel {
pub kind: SeedKind,
pub seed_files: Vec<String>,
pub hops: u32,
pub total_affected: usize,
pub rows: Vec<ImpactRow>,
pub dropped: usize,
pub manifest_gap: usize,
pub heuristic_affected: usize,
pub tests_affected: usize,
pub braked: Vec<BrakedIface>,
pub braked_files: Vec<BrakedFile>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ImpactResult {
Resolved(ImpactModel),
Ambiguous {
kind: SeedKind,
ids: Vec<String>,
},
NotFound {
kind: SeedKind,
},
MemberAmbiguous(Vec<MemberCandidate>),
}
fn from_lines_of(lines: &KindLines) -> Vec<(&'static str, usize)> {
let mut out: Vec<(&'static str, usize)> = Vec::new();
let direct = if lines.direct != 0 {
lines.direct
} else {
lines.direct_amb
};
if direct != 0 {
out.push(("direct", direct));
}
if lines.ctor_di != 0 {
out.push(("ctor-di", lines.ctor_di));
}
if lines.heuristic != 0 {
out.push(("heuristic", lines.heuristic));
}
if lines.iface != 0 {
out.push(("iface", lines.iface));
}
out
}
pub fn build_impact_model(
index: &GraphIndex,
arg: &str,
hops: u32,
cap: usize,
iface: bool,
iface_max_fanin: usize,
hub_max_indegree: usize,
) -> ImpactResult {
let (kind, seed_ids) = match resolve_impact_seed(index, arg) {
SeedResolution::Resolved { kind, ids } => (kind, ids),
SeedResolution::Ambiguous { kind, ids } => return ImpactResult::Ambiguous { kind, ids },
SeedResolution::NotFound { kind } => return ImpactResult::NotFound { kind },
SeedResolution::MemberAmbiguous(candidates) => {
return ImpactResult::MemberAmbiguous(candidates)
}
};
let walk = impact_walk(
index,
&seed_ids,
hops,
iface,
iface_max_fanin,
hub_max_indegree,
);
let mut node_set: SeqSet<String> = SeqSet::new();
for f in walk.seed_files.iter() {
node_set.insert(f.clone());
}
for f in walk.visited.keys() {
node_set.insert(f.clone());
}
let nodes = node_set.into_vec();
let seeds: Vec<String> = walk.seed_files.iter().cloned().collect();
let rank = personalized_page_rank(
&nodes,
&walk.fwd_adj,
&seeds,
DEFAULT_DAMPING,
DEFAULT_ITERATIONS,
);
let mut rows: Vec<ImpactRow> = walk
.visited
.iter()
.map(|(file, h)| {
let names: Vec<String> = h
.symbols
.iter()
.map(|id| {
index
.def(id)
.map(|d| d.name.clone())
.unwrap_or_else(|| id.clone())
})
.collect();
let top_symbols: Vec<String> = names.iter().take(3).cloned().collect();
let top_symbols_more = names.len().saturating_sub(3);
let heuristic = h.via_count == 0 && h.ambiguous_count == 0;
ImpactRow {
file: file.clone(),
hop: h.hop,
via_count: h.via_count,
ambiguous_count: h.ambiguous_count,
top_symbols,
top_symbols_more,
score: *rank.get(file).unwrap_or(&0.0),
heuristic_count: if heuristic { h.heuristic_count } else { 0 },
heuristic,
tier: row_tier(heuristic, h.ext_count > 0),
iface_via: h.iface_via.clone(),
from_lines: from_lines_of(&h.lines),
infra: h.infra,
why: h.why,
}
})
.collect();
rows.sort_by(|a, b| {
a.heuristic
.cmp(&b.heuristic)
.then_with(|| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
})
.then_with(|| a.hop.cmp(&b.hop))
.then_with(|| a.file.cmp(&b.file))
});
let heuristic_affected = rows.iter().filter(|r| r.heuristic).count();
let total_affected = rows.len() - heuristic_affected;
let tests_affected = rows
.iter()
.filter(|r| !r.heuristic && index.is_test_file(&r.file))
.count();
let (shown, dropped) = cap_rows(rows, cap);
ImpactResult::Resolved(ImpactModel {
kind,
seed_files: walk.seed_files.into_vec(),
hops,
total_affected,
rows: shown,
dropped,
manifest_gap: index.flagged_files.len(),
heuristic_affected,
tests_affected,
braked: walk.braked,
braked_files: walk.braked_files,
})
}