use std::collections::HashMap;
use super::super::edge::EdgeKind;
use super::super::file::FileId;
use super::super::node::NodeId;
use super::super::string::StringId;
use super::pass3_intra::{PendingEdge, UnresolvedRef};
#[derive(Debug, Clone, Default)]
pub struct ExportMap {
exports: HashMap<String, Vec<(FileId, NodeId)>>,
}
impl ExportMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, qualified_name: String, file_id: FileId, node_id: NodeId) {
self.exports
.entry(qualified_name)
.or_default()
.push((file_id, node_id));
}
#[must_use]
pub fn lookup(&self, qualified_name: &str) -> Option<(FileId, NodeId)> {
self.exports
.get(qualified_name)
.and_then(|entries| entries.first().copied())
}
#[must_use]
pub fn lookup_cross_file(
&self,
qualified_name: &str,
exclude_file: FileId,
) -> Option<(FileId, NodeId)> {
self.exports.get(qualified_name).and_then(|entries| {
entries
.iter()
.find(|(fid, _)| *fid != exclude_file)
.copied()
})
}
#[must_use]
pub fn contains(&self, qualified_name: &str) -> bool {
self.exports.contains_key(qualified_name)
}
#[must_use]
pub fn len(&self) -> usize {
self.exports.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.exports.is_empty()
}
pub fn remove_file(&mut self, file_id: FileId) {
for entries in self.exports.values_mut() {
entries.retain(|(fid, _)| *fid != file_id);
}
self.exports.retain(|_, entries| !entries.is_empty());
}
pub fn iter(&self) -> impl Iterator<Item = (&String, (FileId, NodeId))> + '_ {
self.exports
.iter()
.filter_map(|(name, entries)| entries.first().map(|e| (name, *e)))
}
}
#[derive(Debug, Clone, Default)]
pub struct Pass4Stats {
pub unresolved_processed: usize,
pub cross_file_calls: usize,
pub cross_file_references: usize,
pub imports_created: usize,
pub still_unresolved: usize,
}
#[derive(Debug, Clone)]
pub struct ImportDecl {
pub imported_name: String,
pub alias_name: Option<String>,
pub alias_id: Option<StringId>,
pub is_wildcard: bool,
pub line: usize,
}
#[must_use]
pub fn pass4_cross_file(
unresolved: &[UnresolvedRef],
imports: &[ImportDecl],
export_map: &ExportMap,
file_node: Option<NodeId>,
source_file: FileId,
) -> (Pass4Stats, Vec<PendingEdge>) {
let mut stats = Pass4Stats::default();
let mut edges = Vec::new();
let alias_map = build_alias_map(imports);
process_unresolved_refs(
unresolved,
export_map,
&alias_map,
source_file,
&mut stats,
&mut edges,
);
process_import_edges(
imports,
export_map,
file_node,
source_file,
&mut stats,
&mut edges,
);
(stats, edges)
}
fn build_alias_map(imports: &[ImportDecl]) -> HashMap<&str, &str> {
let mut alias_map: HashMap<&str, &str> = HashMap::new();
for import in imports {
if let Some(alias_name) = &import.alias_name {
alias_map.insert(alias_name.as_str(), import.imported_name.as_str());
}
}
alias_map
}
fn process_unresolved_refs(
unresolved: &[UnresolvedRef],
export_map: &ExportMap,
alias_map: &HashMap<&str, &str>,
source_file: FileId,
stats: &mut Pass4Stats,
edges: &mut Vec<PendingEdge>,
) {
for unref in unresolved {
stats.unresolved_processed += 1;
if let Some(edge) = resolve_unresolved_ref(unref, export_map, alias_map, source_file, stats)
{
edges.push(edge);
}
}
}
fn process_import_edges(
imports: &[ImportDecl],
export_map: &ExportMap,
file_node: Option<NodeId>,
source_file: FileId,
stats: &mut Pass4Stats,
edges: &mut Vec<PendingEdge>,
) {
let Some(file_node_id) = file_node else {
return;
};
for import in imports {
if let Some((_target_file, target_node)) = export_map.lookup(&import.imported_name) {
stats.imports_created += 1;
edges.push(PendingEdge {
source: file_node_id,
target: target_node,
kind: EdgeKind::Imports {
alias: import.alias_id,
is_wildcard: import.is_wildcard,
},
file: source_file,
spans: vec![], });
}
}
}
fn resolve_unresolved_ref(
unref: &UnresolvedRef,
export_map: &ExportMap,
alias_map: &HashMap<&str, &str>,
source_file: FileId,
stats: &mut Pass4Stats,
) -> Option<PendingEdge> {
let target_name = alias_map
.get(unref.target_name.as_str())
.copied()
.unwrap_or(unref.target_name.as_str());
let Some((_target_file, target_node)) = export_map.lookup_cross_file(target_name, source_file)
else {
stats.still_unresolved += 1;
return None;
};
update_stats_for_edge_kind(stats, &unref.kind);
let spans = if unref.has_location {
vec![crate::graph::node::Span {
start: crate::graph::node::Position {
line: unref.line,
column: unref.column,
},
end: crate::graph::node::Position {
line: unref.line,
column: unref.column,
},
}]
} else {
vec![]
};
Some(PendingEdge {
source: unref.source_node,
target: target_node,
kind: unref.kind.clone(),
file: source_file,
spans,
})
}
fn update_stats_for_edge_kind(stats: &mut Pass4Stats, kind: &EdgeKind) {
match kind {
EdgeKind::Calls { .. } => {
stats.cross_file_calls += 1;
}
_ => {
stats.cross_file_references += 1;
}
}
}
pub fn build_export_map<'a>(
file_symbols: impl Iterator<Item = (FileId, &'a HashMap<String, NodeId>)>,
) -> ExportMap {
let mut export_map = ExportMap::new();
for (file_id, symbol_to_node) in file_symbols {
for (qualified_name, &node_id) in symbol_to_node {
export_map.register(qualified_name.clone(), file_id, node_id);
}
}
export_map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_export_map_register_and_lookup() {
let mut export_map = ExportMap::new();
let file_id = FileId::new(0);
let node_id = NodeId::new(1, 1);
export_map.register("module::MyClass".to_string(), file_id, node_id);
assert!(export_map.contains("module::MyClass"));
assert!(!export_map.contains("other::Class"));
let result = export_map.lookup("module::MyClass");
assert_eq!(result, Some((file_id, node_id)));
}
#[test]
fn test_export_map_remove_file() {
let mut export_map = ExportMap::new();
let file_a = FileId::new(0);
let file_b = FileId::new(1);
export_map.register("a::Foo".to_string(), file_a, NodeId::new(0, 1));
export_map.register("a::Bar".to_string(), file_a, NodeId::new(1, 1));
export_map.register("b::Baz".to_string(), file_b, NodeId::new(2, 1));
assert_eq!(export_map.len(), 3);
export_map.remove_file(file_a);
assert_eq!(export_map.len(), 1);
assert!(!export_map.contains("a::Foo"));
assert!(!export_map.contains("a::Bar"));
assert!(export_map.contains("b::Baz"));
}
#[test]
fn test_pass4_resolves_cross_file_call() {
let mut export_map = ExportMap::new();
let file_a = FileId::new(0);
let file_b = FileId::new(1);
let source_node = NodeId::new(0, 1);
let target_node = NodeId::new(1, 1);
export_map.register("module_b::helper".to_string(), file_b, target_node);
let unresolved = vec![UnresolvedRef {
source_node,
target_name: "module_b::helper".to_string(),
kind: EdgeKind::Calls {
argument_count: 255,
is_async: false,
},
source_file: file_a,
line: 0,
column: 0,
has_location: false,
}];
let (stats, edges) = pass4_cross_file(&unresolved, &[], &export_map, None, file_a);
assert_eq!(stats.unresolved_processed, 1);
assert_eq!(stats.cross_file_calls, 1);
assert_eq!(stats.still_unresolved, 0);
assert_eq!(edges.len(), 1);
let edge = &edges[0];
assert_eq!(edge.source, source_node);
assert_eq!(edge.target, target_node);
assert!(matches!(
edge.kind,
EdgeKind::Calls {
argument_count: 255,
is_async: false
}
));
}
#[test]
fn test_pass4_preserves_call_metadata() {
let mut export_map = ExportMap::new();
let file_a = FileId::new(0);
let file_b = FileId::new(1);
let source_node = NodeId::new(0, 1);
let target_node = NodeId::new(1, 1);
export_map.register("module_b::async_helper".to_string(), file_b, target_node);
let unresolved = vec![UnresolvedRef {
source_node,
target_name: "module_b::async_helper".to_string(),
kind: EdgeKind::Calls {
argument_count: 3,
is_async: true,
},
source_file: file_a,
line: 10,
column: 5,
has_location: true,
}];
let (stats, edges) = pass4_cross_file(&unresolved, &[], &export_map, None, file_a);
assert_eq!(stats.unresolved_processed, 1);
assert_eq!(stats.cross_file_calls, 1);
assert_eq!(edges.len(), 1);
let edge = &edges[0];
assert!(matches!(
edge.kind,
EdgeKind::Calls {
argument_count: 3,
is_async: true
}
));
}
#[test]
fn test_pass4_creates_imports_edge() {
let mut export_map = ExportMap::new();
let file_a = FileId::new(0);
let file_b = FileId::new(1);
let file_node = NodeId::new(10, 1);
let target_node = NodeId::new(1, 1);
export_map.register("module_b::Widget".to_string(), file_b, target_node);
let imports = vec![ImportDecl {
imported_name: "module_b::Widget".to_string(),
alias_name: None,
alias_id: None,
is_wildcard: false,
line: 1,
}];
let (stats, edges) = pass4_cross_file(&[], &imports, &export_map, Some(file_node), file_a);
assert_eq!(stats.imports_created, 1);
assert_eq!(edges.len(), 1);
let edge = &edges[0];
assert_eq!(edge.source, file_node);
assert_eq!(edge.target, target_node);
assert!(matches!(
edge.kind,
EdgeKind::Imports {
alias: None,
is_wildcard: false
}
));
}
#[test]
fn test_pass4_import_edge_preserves_metadata() {
let mut export_map = ExportMap::new();
let file_a = FileId::new(0);
let file_b = FileId::new(1);
let file_node = NodeId::new(10, 1);
let target_node = NodeId::new(1, 1);
export_map.register("module_b::Widget".to_string(), file_b, target_node);
let alias_id = StringId::new(7);
let imports = vec![ImportDecl {
imported_name: "module_b::Widget".to_string(),
alias_name: Some("W".to_string()),
alias_id: Some(alias_id),
is_wildcard: true,
line: 1,
}];
let (stats, edges) = pass4_cross_file(&[], &imports, &export_map, Some(file_node), file_a);
assert_eq!(stats.imports_created, 1);
assert_eq!(edges.len(), 1);
assert!(matches!(
edges[0].kind,
EdgeKind::Imports {
alias: Some(id),
is_wildcard: true,
} if id == alias_id
));
}
#[test]
fn test_pass4_resolves_alias() {
let mut export_map = ExportMap::new();
let file_a = FileId::new(0);
let file_b = FileId::new(1);
let source_node = NodeId::new(0, 1);
let target_node = NodeId::new(1, 1);
export_map.register(
"very::long::module::path::Helper".to_string(),
file_b,
target_node,
);
let imports = vec![ImportDecl {
imported_name: "very::long::module::path::Helper".to_string(),
alias_name: Some("Helper".to_string()),
alias_id: Some(StringId::new(42)),
is_wildcard: false,
line: 1,
}];
let unresolved = vec![UnresolvedRef {
source_node,
target_name: "Helper".to_string(),
kind: EdgeKind::References,
source_file: file_a,
line: 5,
column: 10,
has_location: true,
}];
let (stats, edges) = pass4_cross_file(&unresolved, &imports, &export_map, None, file_a);
assert_eq!(stats.cross_file_references, 1);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].target, target_node);
}
#[test]
fn test_pass4_unresolved_remains_unresolved() {
let export_map = ExportMap::new(); let file_a = FileId::new(0);
let source_node = NodeId::new(0, 1);
let unresolved = vec![UnresolvedRef {
source_node,
target_name: "unknown::symbol".to_string(),
kind: EdgeKind::Calls {
argument_count: 2,
is_async: false,
},
source_file: file_a,
line: 0,
column: 0,
has_location: false,
}];
let (stats, edges) = pass4_cross_file(&unresolved, &[], &export_map, None, file_a);
assert_eq!(stats.unresolved_processed, 1);
assert_eq!(stats.still_unresolved, 1);
assert_eq!(stats.cross_file_calls, 0);
assert!(edges.is_empty());
}
#[test]
fn test_build_export_map() {
let file_a = FileId::new(0);
let file_b = FileId::new(1);
let mut symbols_a = HashMap::new();
symbols_a.insert("a::Foo".to_string(), NodeId::new(0, 1));
symbols_a.insert("a::Bar".to_string(), NodeId::new(1, 1));
let mut symbols_b = HashMap::new();
symbols_b.insert("b::Baz".to_string(), NodeId::new(2, 1));
let files = vec![(file_a, &symbols_a), (file_b, &symbols_b)];
let export_map = build_export_map(files.into_iter());
assert_eq!(export_map.len(), 3);
assert!(export_map.contains("a::Foo"));
assert!(export_map.contains("a::Bar"));
assert!(export_map.contains("b::Baz"));
}
}