1use super::concurrent::GraphSnapshot;
8use super::edge::kind::{EdgeKind, ExportKind};
9use super::node::id::NodeId;
10use super::node::kind::NodeKind;
11use super::resolution::{
12 FileScope, NormalizedSymbolQuery, ResolutionMode, SymbolCandidateBucket,
13 SymbolCandidateOutcome, SymbolQuery, SymbolResolutionOutcome,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SymbolClassification {
19 Declaration,
22 Reference,
24 Import,
27 Ambiguous,
30 Unknown,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ResolvedBinding {
37 pub node_id: NodeId,
39 pub classification: SymbolClassification,
41 pub bucket: SymbolCandidateBucket,
43 pub kind: NodeKind,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct BindingResult {
50 pub query: Option<NormalizedSymbolQuery>,
52 pub bindings: Vec<ResolvedBinding>,
54 pub outcome: SymbolResolutionOutcome,
56}
57
58pub struct BindingQuery<'a> {
69 symbol: &'a str,
70 file_scope: FileScope<'a>,
71 mode: ResolutionMode,
72}
73
74impl<'a> BindingQuery<'a> {
75 #[must_use]
79 pub fn new(symbol: &'a str) -> Self {
80 Self {
81 symbol,
82 file_scope: FileScope::Any,
83 mode: ResolutionMode::AllowSuffixCandidates,
84 }
85 }
86
87 #[must_use]
89 pub fn file_scope(mut self, scope: FileScope<'a>) -> Self {
90 self.file_scope = scope;
91 self
92 }
93
94 #[must_use]
96 pub fn mode(mut self, mode: ResolutionMode) -> Self {
97 self.mode = mode;
98 self
99 }
100
101 #[must_use]
106 pub fn resolve(self, snapshot: &GraphSnapshot) -> BindingResult {
107 let witness = snapshot.find_symbol_candidates_with_witness(&SymbolQuery {
108 symbol: self.symbol,
109 file_scope: self.file_scope,
110 mode: self.mode,
111 });
112
113 let outcome = match &witness.outcome {
115 SymbolCandidateOutcome::Candidates(candidates) => match candidates.as_slice() {
116 [] => SymbolResolutionOutcome::NotFound,
117 [node_id] => SymbolResolutionOutcome::Resolved(*node_id),
118 _ => SymbolResolutionOutcome::Ambiguous(candidates.clone()),
119 },
120 SymbolCandidateOutcome::NotFound => SymbolResolutionOutcome::NotFound,
121 SymbolCandidateOutcome::FileNotIndexed => SymbolResolutionOutcome::FileNotIndexed,
122 };
123
124 let bindings: Vec<ResolvedBinding> = witness
126 .candidates
127 .iter()
128 .filter_map(|candidate| {
129 let entry = snapshot.get_node(candidate.node_id)?;
130 let classification = classify_node(snapshot, candidate.node_id, entry.kind);
131 Some(ResolvedBinding {
132 node_id: candidate.node_id,
133 classification,
134 bucket: candidate.bucket,
135 kind: entry.kind,
136 })
137 })
138 .collect();
139
140 BindingResult {
141 query: witness.normalized_query,
142 bindings,
143 outcome,
144 }
145 }
146}
147
148fn classify_node(
158 snapshot: &GraphSnapshot,
159 node_id: NodeId,
160 kind: NodeKind,
161) -> SymbolClassification {
162 if kind == NodeKind::Import {
164 return SymbolClassification::Import;
165 }
166
167 if kind == NodeKind::Export {
168 let incoming = snapshot.edges().edges_to(node_id);
170 let has_reexport = incoming.iter().any(|e| {
171 matches!(
172 &e.kind,
173 EdgeKind::Exports {
174 kind: ExportKind::Reexport | ExportKind::Namespace,
175 ..
176 }
177 )
178 });
179 let outgoing = snapshot.edges().edges_from(node_id);
181 let has_reexport_outgoing = outgoing.iter().any(|e| {
182 matches!(
183 &e.kind,
184 EdgeKind::Exports {
185 kind: ExportKind::Reexport | ExportKind::Namespace,
186 ..
187 }
188 )
189 });
190
191 return if has_reexport || has_reexport_outgoing {
192 SymbolClassification::Ambiguous
193 } else {
194 SymbolClassification::Import
195 };
196 }
197
198 if kind == NodeKind::CallSite {
199 return SymbolClassification::Reference;
200 }
201
202 let incoming = snapshot.edges().edges_to(node_id);
204 let has_structural = incoming
205 .iter()
206 .any(|e| matches!(&e.kind, EdgeKind::Defines | EdgeKind::Contains));
207
208 if has_structural {
209 return SymbolClassification::Declaration;
210 }
211
212 if !incoming.is_empty() {
214 return SymbolClassification::Reference;
215 }
216
217 let outgoing = snapshot.edges().edges_from(node_id);
220 let has_outgoing_structural = outgoing
221 .iter()
222 .any(|e| matches!(&e.kind, EdgeKind::Defines | EdgeKind::Contains));
223
224 if has_outgoing_structural {
225 return SymbolClassification::Declaration;
226 }
227
228 SymbolClassification::Unknown
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::graph::node::Language;
235 use crate::graph::unified::concurrent::CodeGraph;
236 use crate::graph::unified::file::FileId;
237 use crate::graph::unified::storage::arena::NodeEntry;
238
239 struct TestGraph {
240 graph: CodeGraph,
241 file_id: Option<FileId>,
242 }
243
244 impl TestGraph {
245 fn new() -> Self {
246 Self {
247 graph: CodeGraph::new(),
248 file_id: None,
249 }
250 }
251
252 fn ensure_file_id(&mut self) -> FileId {
253 if let Some(fid) = self.file_id {
254 return fid;
255 }
256 let file_path = std::path::PathBuf::from("/bind-tests/test.rs");
257 let fid = self
258 .graph
259 .files_mut()
260 .register_with_language(&file_path, Some(Language::Rust))
261 .unwrap();
262 self.file_id = Some(fid);
263 fid
264 }
265
266 fn add_node(&mut self, name: &str, kind: NodeKind) -> NodeId {
267 let file_id = self.ensure_file_id();
268 let name_id = self.graph.strings_mut().intern(name).unwrap();
269 let qn_id = self
270 .graph
271 .strings_mut()
272 .intern(&format!("test::{name}"))
273 .unwrap();
274
275 let entry = NodeEntry::new(kind, name_id, file_id)
276 .with_qualified_name(qn_id)
277 .with_location(1, 0, 10, 0);
278
279 let node_id = self.graph.nodes_mut().alloc(entry).unwrap();
280 self.graph
281 .indices_mut()
282 .add(node_id, kind, name_id, Some(qn_id), file_id);
283 node_id
284 }
285
286 fn add_edge(&mut self, source: NodeId, target: NodeId, kind: EdgeKind) {
287 let file_id = self.ensure_file_id();
288 self.graph
289 .edges_mut()
290 .add_edge(source, target, kind, file_id);
291 }
292
293 fn snapshot(&self) -> GraphSnapshot {
294 self.graph.snapshot()
295 }
296 }
297
298 #[test]
299 fn declaration_classification() {
300 let mut tg = TestGraph::new();
301 let module_node = tg.add_node("my_module", NodeKind::Module);
302 let func_node = tg.add_node("my_func", NodeKind::Function);
303 tg.add_edge(module_node, func_node, EdgeKind::Defines);
304
305 let snapshot = tg.snapshot();
306 let result = BindingQuery::new("my_func").resolve(&snapshot);
307
308 assert!(!result.bindings.is_empty(), "expected at least one binding");
309 let binding = result
310 .bindings
311 .iter()
312 .find(|b| b.node_id == func_node)
313 .expect("expected binding for func_node");
314 assert_eq!(binding.classification, SymbolClassification::Declaration);
315 assert_eq!(binding.kind, NodeKind::Function);
316 }
317
318 #[test]
319 fn reference_classification_callsite() {
320 let mut tg = TestGraph::new();
321 let _call_node = tg.add_node("some_call", NodeKind::CallSite);
322
323 let snapshot = tg.snapshot();
324 let result = BindingQuery::new("some_call").resolve(&snapshot);
325
326 assert!(!result.bindings.is_empty());
327 assert_eq!(
328 result.bindings[0].classification,
329 SymbolClassification::Reference
330 );
331 }
332
333 #[test]
334 fn import_classification() {
335 let mut tg = TestGraph::new();
336 let _import_node = tg.add_node("imported_sym", NodeKind::Import);
337
338 let snapshot = tg.snapshot();
339 let result = BindingQuery::new("imported_sym").resolve(&snapshot);
340
341 assert!(!result.bindings.is_empty());
342 assert_eq!(
343 result.bindings[0].classification,
344 SymbolClassification::Import
345 );
346 }
347
348 #[test]
349 fn export_direct_classification() {
350 let mut tg = TestGraph::new();
351 let _export_node = tg.add_node("exported_sym", NodeKind::Export);
352
353 let snapshot = tg.snapshot();
354 let result = BindingQuery::new("exported_sym").resolve(&snapshot);
355
356 assert!(!result.bindings.is_empty());
357 assert_eq!(
359 result.bindings[0].classification,
360 SymbolClassification::Import
361 );
362 }
363
364 #[test]
365 fn export_reexport_ambiguous() {
366 let mut tg = TestGraph::new();
367 let source = tg.add_node("source_mod", NodeKind::Module);
368 let export_node = tg.add_node("reexported", NodeKind::Export);
369 tg.add_edge(
370 source,
371 export_node,
372 EdgeKind::Exports {
373 kind: ExportKind::Reexport,
374 alias: None,
375 },
376 );
377
378 let snapshot = tg.snapshot();
379 let result = BindingQuery::new("reexported").resolve(&snapshot);
380
381 assert!(!result.bindings.is_empty());
382 let binding = result
383 .bindings
384 .iter()
385 .find(|b| b.node_id == export_node)
386 .expect("expected binding for export_node");
387 assert_eq!(binding.classification, SymbolClassification::Ambiguous);
388 }
389
390 #[test]
391 fn builder_defaults() {
392 let query = BindingQuery::new("test_sym");
393 assert_eq!(query.symbol, "test_sym");
394 assert_eq!(query.file_scope, FileScope::Any);
395 assert_eq!(query.mode, ResolutionMode::AllowSuffixCandidates);
396 }
397
398 #[test]
399 fn not_found_result() {
400 let tg = TestGraph::new();
401 let snapshot = tg.snapshot();
402 let result = BindingQuery::new("nonexistent_symbol_xyz").resolve(&snapshot);
403
404 assert!(result.bindings.is_empty());
405 assert_eq!(result.outcome, SymbolResolutionOutcome::NotFound);
406 }
407
408 #[test]
409 fn declaration_via_contains_edge() {
410 let mut tg = TestGraph::new();
411 let class_node = tg.add_node("MyClass", NodeKind::Class);
412 let method_node = tg.add_node("my_method", NodeKind::Method);
413 tg.add_edge(class_node, method_node, EdgeKind::Contains);
414
415 let snapshot = tg.snapshot();
416 let result = BindingQuery::new("my_method").resolve(&snapshot);
417
418 assert!(!result.bindings.is_empty());
419 let binding = result
420 .bindings
421 .iter()
422 .find(|b| b.node_id == method_node)
423 .expect("expected binding for method_node");
424 assert_eq!(binding.classification, SymbolClassification::Declaration);
425 }
426
427 #[test]
428 fn root_declaration_with_outgoing_defines() {
429 let mut tg = TestGraph::new();
430 let module_node = tg.add_node("root_mod", NodeKind::Module);
431 let child = tg.add_node("child_func", NodeKind::Function);
432 tg.add_edge(module_node, child, EdgeKind::Defines);
433
434 let snapshot = tg.snapshot();
435 let result = BindingQuery::new("root_mod").resolve(&snapshot);
436
437 assert!(!result.bindings.is_empty());
438 let binding = result
439 .bindings
440 .iter()
441 .find(|b| b.node_id == module_node)
442 .expect("expected binding for module_node");
443 assert_eq!(binding.classification, SymbolClassification::Declaration);
445 }
446}