use std::collections::HashMap;
use crate::graph::types::{Edge, Provenance, RefRole, Symbol, SymbolKind};
use crate::symbol::SymbolId;
use super::super::{enclosing_path_ends_with, namespaces_end_with};
use super::subgraph::PendingRef;
#[derive(Default)]
pub(crate) struct GlobalIndex {
by_name: HashMap<String, Vec<SymbolId>>,
modules_by_name: HashMap<String, Vec<SymbolId>>,
}
impl GlobalIndex {
pub(crate) fn new() -> Self {
Self {
by_name: HashMap::new(),
modules_by_name: HashMap::new(),
}
}
pub(crate) fn from_symbols(symbols: &[Symbol]) -> Self {
let mut idx = Self::new();
idx.insert_symbols(symbols);
idx
}
pub(crate) fn insert_symbols(&mut self, symbols: &[Symbol]) {
for s in symbols {
if s.kind == SymbolKind::Module {
self.modules_by_name
.entry(s.name.clone())
.or_default()
.push(s.id.clone());
}
if let Some(n) = s.id.leaf_name() {
self.by_name
.entry(n.to_string())
.or_default()
.push(s.id.clone());
}
}
}
pub(crate) fn remove_symbols(&mut self, symbols: &[Symbol]) {
for s in symbols {
if s.kind == SymbolKind::Module {
if let Some(bucket) = self.modules_by_name.get_mut(&s.name) {
if let Some(pos) = bucket.iter().position(|id| id == &s.id) {
bucket.swap_remove(pos);
}
if bucket.is_empty() {
self.modules_by_name.remove(&s.name);
}
}
}
if let Some(n) = s.id.leaf_name() {
if let Some(bucket) = self.by_name.get_mut(n) {
if let Some(pos) = bucket.iter().position(|id| id == &s.id) {
bucket.swap_remove(pos);
}
if bucket.is_empty() {
self.by_name.remove(n);
}
}
}
}
}
fn unique_match(&self, name: &str, segs: &[String]) -> Option<&SymbolId> {
self.by_name.get(name).and_then(|cands| {
let mut it = cands
.iter()
.filter(|id| segs.is_empty() || namespaces_end_with(id, segs));
match (it.next(), it.next()) {
(Some(only), None) => Some(only), _ => None, }
})
}
fn unique_qualified_match(&self, name: &str, segs: &[String]) -> Option<&SymbolId> {
self.by_name.get(name).and_then(|cands| {
let mut it = cands
.iter()
.filter(|id| namespaces_end_with(id, segs) || enclosing_path_ends_with(id, segs));
match (it.next(), it.next()) {
(Some(only), None) => Some(only),
_ => None,
}
})
}
fn unique_module_match(&self, name: &str, segs: &[String]) -> Option<&SymbolId> {
self.modules_by_name.get(name).and_then(|cands| {
let mut it = cands
.iter()
.filter(|id| segs.is_empty() || namespaces_end_with(id, segs));
match (it.next(), it.next()) {
(Some(only), None) => Some(only), _ => None, }
})
}
}
pub(crate) fn stitch(pending: &[PendingRef], index: &GlobalIndex) -> Vec<Edge> {
let mut edges = Vec::new();
for p in pending {
let matched = match p.role {
RefRole::ModuleRef => index.unique_module_match(&p.name, &p.segs),
_ if p.qualified => index.unique_qualified_match(&p.name, &p.segs),
_ => index.unique_match(&p.name, &p.segs),
};
if let Some(matched_id) = matched {
if *matched_id == p.from {
continue;
}
edges.push(Edge {
from: p.from.clone(),
to: matched_id.clone(),
role: p.role,
confidence: p.confidence,
provenance: Provenance::ScopeGraph,
occ: p.occ.clone(),
});
}
}
edges
}
#[cfg(test)]
mod tests {
use super::super::subgraph::build_subgraph;
use super::*;
use crate::extract::{Extractor, RustExtractor};
#[test]
fn insert_then_remove_restores_no_match() {
let conf = RustExtractor
.extract("pub struct Config {}", "src/conf.rs")
.unwrap();
let app = RustExtractor
.extract("use conf::Config;\npub fn run() {}", "src/app.rs")
.unwrap();
let conf_sub = build_subgraph(&conf);
let app_sub = build_subgraph(&app);
use crate::graph::types::RefRole;
let mut index = GlobalIndex::new();
index.insert_symbols(&conf_sub.symbols);
let edges = stitch(&app_sub.pending, &index);
assert_eq!(
edges.iter().filter(|e| e.role == RefRole::Import).count(),
1,
"import must resolve while conf::Config is indexed"
);
index.remove_symbols(&conf_sub.symbols);
let edges = stitch(&app_sub.pending, &index);
assert!(
edges.is_empty(),
"after removing conf's symbols, nothing must resolve"
);
}
#[test]
fn module_ref_resolves_to_module_symbol() {
let lib = RustExtractor
.extract("mod util;\npub fn run() {}", "src/lib.rs")
.unwrap();
let util = RustExtractor
.extract("pub fn helper() {}", "src/util.rs")
.unwrap();
let lib_sub = build_subgraph(&lib);
let util_sub = build_subgraph(&util);
let mut index = GlobalIndex::new();
index.insert_symbols(&lib_sub.symbols);
index.insert_symbols(&util_sub.symbols);
let edges = stitch(&lib_sub.pending, &index);
assert_eq!(edges.len(), 1, "mod util; must resolve to exactly one edge");
let edge = &edges[0];
assert_eq!(edge.role, RefRole::ModuleRef);
assert_eq!(edge.provenance, Provenance::ScopeGraph);
let util_module = util_sub
.symbols
.iter()
.find(|s| s.kind == SymbolKind::Module)
.expect("util.rs has a module symbol");
assert_eq!(edge.to, util_module.id);
}
#[test]
fn module_ref_does_not_resolve_to_function() {
let lib = RustExtractor
.extract("mod config;\npub fn run() {}", "src/lib.rs")
.unwrap();
let other = RustExtractor
.extract("pub fn config() {}", "src/other.rs")
.unwrap();
let lib_sub = build_subgraph(&lib);
let other_sub = build_subgraph(&other);
let mut index = GlobalIndex::new();
index.insert_symbols(&lib_sub.symbols);
index.insert_symbols(&other_sub.symbols);
let edges = stitch(&lib_sub.pending, &index);
for e in &edges {
assert_ne!(
e.role,
RefRole::ModuleRef,
"ModuleRef(config) must not resolve to the `config` function"
);
}
}
#[test]
fn pending_ref_to_own_id_yields_no_self_edge() {
use crate::graph::types::{Confidence, Occurrence};
let recurse = RustExtractor
.extract("pub fn recurse() { recurse() }", "src/main.rs")
.unwrap();
let sub = build_subgraph(&recurse);
let own_id = sub
.symbols
.iter()
.find(|s| s.id.leaf_name() == Some("recurse"))
.map(|s| s.id.clone())
.expect("recurse must be defined");
let mut index = GlobalIndex::new();
index.insert_symbols(&sub.symbols);
let pending = vec![PendingRef {
from: own_id.clone(),
name: "recurse".to_string(),
segs: Vec::new(),
role: RefRole::Call,
occ: Occurrence {
file: "src/main.rs".to_string(),
line: 1,
col: 0,
byte: 20,
},
confidence: Confidence::Scoped,
qualified: false,
}];
let edges = stitch(&pending, &index);
assert!(
edges.iter().all(|e| e.from != e.to),
"stitch must not emit a from == to self-edge"
);
}
#[test]
fn module_ref_ambiguous_name_no_edge() {
let lib = RustExtractor
.extract("mod util;\npub fn run() {}", "src/lib.rs")
.unwrap();
let util_a = RustExtractor
.extract("pub fn a() {}", "src/a/util.rs")
.unwrap();
let util_b = RustExtractor
.extract("pub fn b() {}", "src/b/util.rs")
.unwrap();
let lib_sub = build_subgraph(&lib);
let a_sub = build_subgraph(&util_a);
let b_sub = build_subgraph(&util_b);
let mut index = GlobalIndex::new();
index.insert_symbols(&lib_sub.symbols);
index.insert_symbols(&a_sub.symbols);
index.insert_symbols(&b_sub.symbols);
let module_refs = stitch(&lib_sub.pending, &index)
.into_iter()
.filter(|e| e.role == RefRole::ModuleRef)
.count();
assert_eq!(
module_refs, 0,
"two modules named `util` → ModuleRef must resolve to no edge"
);
}
}