use std::collections::{HashMap, HashSet};
use std::path::Path;
use crate::graph;
use crate::manifest;
use super::dispatch;
use super::seq::push_ordered_unique;
#[derive(Debug, Clone, Default)]
pub struct InboundEntry {
pub inherits: Vec<usize>,
pub uses_type: Vec<usize>,
pub uses_member: Vec<usize>,
pub implements: Vec<usize>,
pub overrides: Vec<usize>,
}
pub type HeuristicEntry = InboundEntry;
#[derive(Debug, Clone, Default)]
pub struct OutboundEntry {
pub inherits: Vec<usize>,
pub uses_type: Vec<usize>,
pub uses_member: Vec<usize>,
pub implements: Vec<usize>,
pub overrides: Vec<usize>,
pub imports: Vec<usize>,
}
pub struct GraphIndex<'g> {
pub graph: &'g graph::Graph,
pub root: std::path::PathBuf,
pub by_id: HashMap<String, usize>,
pub by_file: HashMap<String, Vec<usize>>,
pub by_simple_name: HashMap<String, Vec<usize>>,
pub by_lower_name: HashMap<String, Vec<usize>>,
pub inbound: HashMap<String, InboundEntry>,
pub outbound_by_file: HashMap<String, OutboundEntry>,
pub ambiguous_by_candidate: HashMap<String, Vec<usize>>,
pub ambiguous_by_file: HashMap<String, Vec<usize>>,
pub heuristic_inbound: HashMap<String, HeuristicEntry>,
pub heuristic_outbound_by_file: HashMap<String, HeuristicEntry>,
pub test_defs_by_file: HashMap<String, Vec<usize>>,
pub flagged_files: HashSet<String>,
pub manifest_present: bool,
pub ctor_di_by_to: HashMap<String, Vec<usize>>,
pub ctor_di_fanin: HashMap<String, usize>,
pub project: Option<crate::project::ProjectModel>,
pub hub_indegree: HashMap<String, usize>,
}
impl<'g> GraphIndex<'g> {
pub fn def(&self, id: &str) -> Option<&graph::Def> {
self.by_id.get(id).map(|&i| &self.graph.defs[i])
}
pub fn is_test_file(&self, file: &str) -> bool {
if self.test_defs_by_file.contains_key(file) {
return true;
}
let Some(project) = &self.project else {
return false;
};
project
.unit_of_file(file)
.is_some_and(|u| project.units[u].test)
}
}
fn note_file(flagged: &mut HashSet<String>, manifest_paths: Option<&HashSet<String>>, file: &str) {
if let Some(paths) = manifest_paths {
if !file.is_empty() && !paths.contains(file) {
flagged.insert(file.to_string());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexOptions {
pub include_guesses: bool,
pub include_dispatch: bool,
}
impl Default for IndexOptions {
fn default() -> Self {
IndexOptions {
include_guesses: true,
include_dispatch: true,
}
}
}
pub fn load_graph_index<'g>(graph: &'g graph::Graph, root: &Path) -> GraphIndex<'g> {
load_graph_index_with(graph, root, IndexOptions::default())
}
#[allow(
clippy::too_many_lines,
reason = "one ordered pass building every index table over the same manifest and graph, so the tables stay mutually consistent"
)]
pub fn load_graph_index_with<'g>(
graph: &'g graph::Graph,
root: &Path,
opts: IndexOptions,
) -> GraphIndex<'g> {
let manifest_value = match manifest::read_manifest(root) {
Ok(v) => v,
Err(_) => None, };
let manifest_present = manifest_value.is_some();
let manifest_paths: Option<HashSet<String>> = manifest_value.as_ref().and_then(|m| {
m.get("entries")
.and_then(|e| e.as_object())
.map(|entries| entries.iter().map(|(k, _)| k.clone()).collect())
});
let manifest_paths_ref = manifest_paths.as_ref();
let mut flagged_files: HashSet<String> = HashSet::new();
let mut by_id: HashMap<String, usize> = HashMap::with_capacity(graph.defs.len());
let mut by_file: HashMap<String, Vec<usize>> = HashMap::new();
let mut by_simple_name: HashMap<String, Vec<usize>> = HashMap::new();
let mut by_lower_name: HashMap<String, Vec<usize>> = HashMap::new();
let mut test_defs_by_file: HashMap<String, Vec<usize>> = HashMap::new();
for (i, d) in graph.defs.iter().enumerate() {
by_id.insert(d.id.clone(), i);
note_file(&mut flagged_files, manifest_paths_ref, &d.file);
push_ordered_unique(&mut by_file, &d.file, i);
for also in &d.also_in {
note_file(&mut flagged_files, manifest_paths_ref, &also.file);
push_ordered_unique(&mut by_file, &also.file, i);
}
if !d.test_methods.is_empty() {
let mut files: Vec<&str> = vec![d.file.as_str()];
for also in &d.also_in {
if !files.contains(&also.file.as_str()) {
files.push(also.file.as_str());
}
}
for file in files {
test_defs_by_file
.entry(file.to_string())
.or_default()
.push(i);
}
}
by_simple_name.entry(d.name.clone()).or_default().push(i);
by_lower_name
.entry(d.name.to_lowercase())
.or_default()
.push(i);
}
let mut inbound: HashMap<String, InboundEntry> = HashMap::new();
let mut outbound_by_file: HashMap<String, OutboundEntry> = HashMap::new();
let mut ambiguous_by_candidate: HashMap<String, Vec<usize>> = HashMap::new();
let mut ambiguous_by_file: HashMap<String, Vec<usize>> = HashMap::new();
let mut heuristic_inbound: HashMap<String, HeuristicEntry> = HashMap::new();
let mut heuristic_outbound_by_file: HashMap<String, HeuristicEntry> = HashMap::new();
let mut ctor_di_by_to: HashMap<String, Vec<usize>> = HashMap::new();
let mut ctor_di_sites_by_iface: HashMap<String, HashSet<(String, usize)>> = HashMap::new();
let mut hub_referrers_by_file: HashMap<String, HashSet<String>> = HashMap::new();
let admits =
|e: &graph::Edge| opts.include_guesses || e.tier() == Some(graph::HeuristicTier::Ext);
for (i, e) in graph.edges.iter().enumerate() {
match e {
graph::Edge::Imports { from_file, .. } => {
note_file(&mut flagged_files, manifest_paths_ref, from_file);
outbound_by_file
.entry(from_file.clone())
.or_default()
.imports
.push(i);
}
graph::Edge::Ambiguous {
from_file,
candidates,
..
} => {
note_file(&mut flagged_files, manifest_paths_ref, from_file);
ambiguous_by_file
.entry(from_file.clone())
.or_default()
.push(i);
for c in candidates {
ambiguous_by_candidate
.entry(c.id.clone())
.or_default()
.push(i);
}
}
graph::Edge::Inherits {
from_file,
to,
to_file,
heuristic,
..
} => {
note_file(&mut flagged_files, manifest_paths_ref, from_file);
note_file(&mut flagged_files, manifest_paths_ref, to_file);
if !to_file.is_empty() && !from_file.is_empty() && to_file != from_file {
hub_referrers_by_file
.entry(to_file.clone())
.or_default()
.insert(from_file.clone());
}
if *heuristic {
if admits(e) {
heuristic_outbound_by_file
.entry(from_file.clone())
.or_default()
.inherits
.push(i);
heuristic_inbound
.entry(to.clone())
.or_default()
.inherits
.push(i);
}
} else {
outbound_by_file
.entry(from_file.clone())
.or_default()
.inherits
.push(i);
inbound.entry(to.clone()).or_default().inherits.push(i);
}
}
graph::Edge::UsesType {
from_file,
to,
to_file,
heuristic,
..
} => {
note_file(&mut flagged_files, manifest_paths_ref, from_file);
note_file(&mut flagged_files, manifest_paths_ref, to_file);
if !to_file.is_empty() && !from_file.is_empty() && to_file != from_file {
hub_referrers_by_file
.entry(to_file.clone())
.or_default()
.insert(from_file.clone());
}
if *heuristic {
if admits(e) {
heuristic_outbound_by_file
.entry(from_file.clone())
.or_default()
.uses_type
.push(i);
heuristic_inbound
.entry(to.clone())
.or_default()
.uses_type
.push(i);
}
} else {
outbound_by_file
.entry(from_file.clone())
.or_default()
.uses_type
.push(i);
inbound.entry(to.clone()).or_default().uses_type.push(i);
}
}
graph::Edge::UsesMember {
from_file,
to,
to_file,
heuristic,
..
} => {
note_file(&mut flagged_files, manifest_paths_ref, from_file);
note_file(&mut flagged_files, manifest_paths_ref, to_file);
if !to_file.is_empty() && !from_file.is_empty() && to_file != from_file {
hub_referrers_by_file
.entry(to_file.clone())
.or_default()
.insert(from_file.clone());
}
if *heuristic {
if admits(e) {
heuristic_outbound_by_file
.entry(from_file.clone())
.or_default()
.uses_member
.push(i);
heuristic_inbound
.entry(to.clone())
.or_default()
.uses_member
.push(i);
}
} else {
outbound_by_file
.entry(from_file.clone())
.or_default()
.uses_member
.push(i);
inbound.entry(to.clone()).or_default().uses_member.push(i);
}
}
graph::Edge::Implements {
from_file,
to,
to_file,
..
}
| graph::Edge::Overrides {
from_file,
to,
to_file,
..
} => {
note_file(&mut flagged_files, manifest_paths_ref, from_file);
note_file(&mut flagged_files, manifest_paths_ref, to_file);
if !to_file.is_empty() && !from_file.is_empty() && to_file != from_file {
hub_referrers_by_file
.entry(to_file.clone())
.or_default()
.insert(from_file.clone());
}
if opts.include_dispatch {
dispatch::record_dispatch_edge(
e,
i,
from_file,
to,
&mut outbound_by_file,
&mut inbound,
);
}
}
graph::Edge::CtorDi {
from_file,
from_line,
iface,
to,
..
} => {
ctor_di_sites_by_iface
.entry(iface.clone())
.or_default()
.insert((from_file.clone(), *from_line));
if let Some(to) = to {
ctor_di_by_to.entry(to.clone()).or_default().push(i);
}
}
graph::Edge::Import { .. }
| graph::Edge::Call { .. }
| graph::Edge::JsxUse { .. }
| graph::Edge::Dispatch { .. } => {}
}
}
GraphIndex {
graph,
root: root.to_path_buf(),
by_id,
by_file,
by_simple_name,
by_lower_name,
inbound,
outbound_by_file,
ambiguous_by_candidate,
ambiguous_by_file,
heuristic_inbound,
heuristic_outbound_by_file,
test_defs_by_file,
flagged_files,
manifest_present,
ctor_di_by_to,
project: if graph.units.is_empty() {
None
} else {
Some(crate::project::ProjectModel::from_units(
crate::project::units_from_graph(&graph.units),
))
},
ctor_di_fanin: ctor_di_sites_by_iface
.into_iter()
.map(|(name, sites)| (name, sites.len()))
.collect(),
hub_indegree: hub_referrers_by_file
.into_iter()
.map(|(file, refs)| (file, refs.len()))
.collect(),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DefSite {
pub file: String,
pub line: usize,
}
pub fn def_files(index: &GraphIndex, def_id: &str) -> Vec<String> {
let mut out = Vec::new();
let mut seen = HashSet::new();
if let Some(&i) = index.by_id.get(def_id) {
let d = &index.graph.defs[i];
if seen.insert(d.file.clone()) {
out.push(d.file.clone());
}
for also in &d.also_in {
if seen.insert(also.file.clone()) {
out.push(also.file.clone());
}
}
}
out
}
pub fn def_sites(index: &GraphIndex, def_id: &str) -> Vec<DefSite> {
let mut out = Vec::new();
if let Some(&i) = index.by_id.get(def_id) {
let d = &index.graph.defs[i];
out.push(DefSite {
file: d.file.clone(),
line: d.line,
});
for also in &d.also_in {
out.push(DefSite {
file: also.file.clone(),
line: also.line,
});
}
}
out
}
#[derive(Debug, Clone, Default)]
pub struct SymbolRefs {
pub inbound_inherits: Vec<usize>,
pub inbound_uses_type: Vec<usize>,
pub inbound_uses_member: Vec<usize>,
pub inbound_implements: Vec<usize>,
pub inbound_overrides: Vec<usize>,
pub outbound_inherits: Vec<usize>,
pub outbound_uses_type: Vec<usize>,
pub outbound_uses_member: Vec<usize>,
pub outbound_implements: Vec<usize>,
pub outbound_overrides: Vec<usize>,
pub outbound_imports: Vec<usize>,
pub heuristic_inbound_inherits: Vec<usize>,
pub heuristic_inbound_uses_type: Vec<usize>,
pub heuristic_inbound_uses_member: Vec<usize>,
pub heuristic_outbound_inherits: Vec<usize>,
pub heuristic_outbound_uses_type: Vec<usize>,
pub heuristic_outbound_uses_member: Vec<usize>,
pub ambiguous_inbound: Vec<usize>,
pub ambiguous_outbound: Vec<usize>,
}
pub fn symbol_refs(index: &GraphIndex, def_id: &str) -> SymbolRefs {
let empty = InboundEntry::default();
let base = index.inbound.get(def_id).unwrap_or(&empty);
let inbound_inherits = base.inherits.clone();
let inbound_uses_type = base.uses_type.clone();
let mut inbound_uses_member = base.uses_member.clone();
let inbound_implements = base.implements.clone();
let inbound_overrides = base.overrides.clone();
if let Some(&i) = index.by_id.get(def_id) {
if index.graph.defs[i].kind == "enum" {
let prefix = format!("{def_id}.");
for d in &index.graph.defs {
if d.kind != "enum-member" || !d.id.starts_with(&prefix) {
continue;
}
if let Some(m) = index.inbound.get(&d.id) {
inbound_uses_member.extend(m.uses_member.iter().copied());
}
}
}
}
let mut outbound_inherits = Vec::new();
let mut outbound_uses_type = Vec::new();
let mut outbound_uses_member = Vec::new();
let mut outbound_implements = Vec::new();
let mut outbound_overrides = Vec::new();
let mut outbound_imports = Vec::new();
for file in def_files(index, def_id) {
if let Some(o) = index.outbound_by_file.get(&file) {
outbound_inherits.extend(o.inherits.iter().copied());
outbound_uses_type.extend(o.uses_type.iter().copied());
outbound_uses_member.extend(o.uses_member.iter().copied());
outbound_implements.extend(o.implements.iter().copied());
outbound_overrides.extend(o.overrides.iter().copied());
outbound_imports.extend(o.imports.iter().copied());
}
}
let heuristic_base = index.heuristic_inbound.get(def_id).unwrap_or(&empty);
let heuristic_inbound_inherits = heuristic_base.inherits.clone();
let heuristic_inbound_uses_type = heuristic_base.uses_type.clone();
let mut heuristic_inbound_uses_member = heuristic_base.uses_member.clone();
if let Some(&i) = index.by_id.get(def_id) {
if index.graph.defs[i].kind == "enum" {
let prefix = format!("{def_id}.");
for d in &index.graph.defs {
if d.kind != "enum-member" || !d.id.starts_with(&prefix) {
continue;
}
if let Some(m) = index.heuristic_inbound.get(&d.id) {
heuristic_inbound_uses_member.extend(m.uses_member.iter().copied());
}
}
}
}
let mut heuristic_outbound_inherits = Vec::new();
let mut heuristic_outbound_uses_type = Vec::new();
let mut heuristic_outbound_uses_member = Vec::new();
for file in def_files(index, def_id) {
if let Some(o) = index.heuristic_outbound_by_file.get(&file) {
heuristic_outbound_inherits.extend(o.inherits.iter().copied());
heuristic_outbound_uses_type.extend(o.uses_type.iter().copied());
heuristic_outbound_uses_member.extend(o.uses_member.iter().copied());
}
}
let ambiguous_inbound = index
.ambiguous_by_candidate
.get(def_id)
.cloned()
.unwrap_or_default();
let ambiguous_outbound: Vec<usize> = def_files(index, def_id)
.iter()
.flat_map(|f| index.ambiguous_by_file.get(f).cloned().unwrap_or_default())
.collect();
SymbolRefs {
inbound_inherits,
inbound_uses_type,
inbound_uses_member,
inbound_implements,
inbound_overrides,
outbound_inherits,
outbound_uses_type,
outbound_uses_member,
outbound_implements,
outbound_overrides,
outbound_imports,
heuristic_inbound_inherits,
heuristic_inbound_uses_type,
heuristic_inbound_uses_member,
heuristic_outbound_inherits,
heuristic_outbound_uses_type,
heuristic_outbound_uses_member,
ambiguous_inbound,
ambiguous_outbound,
}
}