arbor_graph/
symbol_table.rs1use crate::graph::NodeId;
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5#[derive(Debug, Default, Clone)]
10pub struct SymbolTable {
11 by_fqn: HashMap<String, NodeId>,
13
14 exports_by_file: HashMap<PathBuf, Vec<String>>,
17}
18
19impl SymbolTable {
20 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn insert(&mut self, fqn: String, id: NodeId, file: PathBuf) {
31 self.by_fqn.insert(fqn.clone(), id);
32 self.exports_by_file.entry(file).or_default().push(fqn);
33 }
34
35 pub fn resolve(&self, fqn: &str) -> Option<NodeId> {
37 self.by_fqn.get(fqn).copied()
38 }
39
40 pub fn get_file_exports(&self, file: &PathBuf) -> Option<&Vec<String>> {
42 self.exports_by_file.get(file)
43 }
44
45 pub fn clear(&mut self) {
47 self.by_fqn.clear();
48 self.exports_by_file.clear();
49 }
50
51 pub fn resolve_with_context(
62 &self,
63 name: &str,
64 context_file: &std::path::Path,
65 ) -> Option<NodeId> {
66 if let Some(id) = self.by_fqn.get(name) {
68 return Some(*id);
69 }
70
71 let context_dir = context_file.parent();
73 let mut candidates: Vec<(&String, NodeId, bool)> = Vec::new();
74
75 for (fqn, &id) in &self.by_fqn {
76 if fqn.ends_with(name) {
78 let prefix_len = fqn.len() - name.len();
80 if prefix_len == 0
81 || fqn.chars().nth(prefix_len - 1) == Some('.')
82 || fqn.chars().nth(prefix_len - 1) == Some(':')
83 {
84 let same_dir = self
86 .exports_by_file
87 .iter()
88 .find(|(_, exports)| exports.contains(fqn))
89 .map(|(file, _)| file.parent() == context_dir)
90 .unwrap_or(false);
91
92 candidates.push((fqn, id, same_dir));
93 }
94 }
95 }
96
97 match candidates.len() {
98 0 => None,
99 1 => Some(candidates[0].1),
100 _ => {
101 let same_dir_candidates: Vec<_> =
103 candidates.iter().filter(|(_, _, same)| *same).collect();
104 if same_dir_candidates.len() == 1 {
105 Some(same_dir_candidates[0].1)
106 } else {
107 None
109 }
110 }
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_insert_resolve() {
121 let mut table = SymbolTable::new();
122 let path = PathBuf::from("main.rs");
123 let id = NodeId::new(1);
124
125 table.insert("main::foo".to_string(), id, path.clone());
126
127 assert_eq!(table.resolve("main::foo"), Some(id));
128 assert_eq!(table.resolve("main::bar"), None);
129
130 let exports = table.get_file_exports(&path).unwrap();
131 assert_eq!(exports.len(), 1);
132 assert_eq!(exports[0], "main::foo");
133 }
134
135 #[test]
136 fn test_resolve_with_context_exact_match() {
137 let mut table = SymbolTable::new();
138 let path = PathBuf::from("src/utils.rs");
139 let id = NodeId::new(1);
140
141 table.insert("pkg.utils.helper".to_string(), id, path.clone());
142
143 let result =
145 table.resolve_with_context("pkg.utils.helper", &PathBuf::from("other/file.rs"));
146 assert_eq!(result, Some(id));
147 }
148
149 #[test]
150 fn test_resolve_with_context_suffix_match() {
151 let mut table = SymbolTable::new();
152 let path = PathBuf::from("src/utils.rs");
153 let id = NodeId::new(1);
154
155 table.insert("pkg.utils.helper".to_string(), id, path.clone());
156
157 let result = table.resolve_with_context("helper", &PathBuf::from("other/file.rs"));
159 assert_eq!(result, Some(id));
160 }
161
162 #[test]
163 fn test_resolve_with_context_ambiguous_returns_none() {
164 let mut table = SymbolTable::new();
165 let id1 = NodeId::new(1);
166 let id2 = NodeId::new(2);
167
168 table.insert(
170 "pkg.a.helper".to_string(),
171 id1,
172 PathBuf::from("src/a/mod.rs"),
173 );
174 table.insert(
175 "pkg.b.helper".to_string(),
176 id2,
177 PathBuf::from("src/b/mod.rs"),
178 );
179
180 let result = table.resolve_with_context("helper", &PathBuf::from("src/c/caller.rs"));
182 assert_eq!(result, None);
183 }
184
185 #[test]
186 fn test_resolve_with_context_locality_preference() {
187 let mut table = SymbolTable::new();
188 let id1 = NodeId::new(1);
189 let id2 = NodeId::new(2);
190
191 table.insert(
193 "pkg.a.helper".to_string(),
194 id1,
195 PathBuf::from("src/a/mod.rs"),
196 );
197 table.insert(
198 "pkg.b.helper".to_string(),
199 id2,
200 PathBuf::from("src/b/mod.rs"),
201 );
202
203 let result = table.resolve_with_context("helper", &PathBuf::from("src/a/caller.rs"));
205 assert_eq!(result, Some(id1));
206
207 let result = table.resolve_with_context("helper", &PathBuf::from("src/b/caller.rs"));
209 assert_eq!(result, Some(id2));
210 }
211}