pub mod alias;
pub mod plane;
pub mod query;
pub mod shadow;
pub use alias::{AliasEntry, AliasEntryId, AliasTable};
pub use plane::{BindingPlane, BindingResolution};
pub use shadow::{ShadowEntry, ShadowEntryId, ShadowTable};
pub use witness::WitnessRendering;
pub mod scope;
pub mod witness;
pub use query::BindingQuery;
pub use witness::{
RejectionReason, ResolutionStep, TieBreakReason, UnresolvedReason, VisibilityReason,
};
use serde::{Deserialize, Serialize};
use super::concurrent::GraphSnapshot;
use super::edge::kind::{EdgeKind, ExportKind};
use super::node::id::NodeId;
use super::node::kind::NodeKind;
use super::resolution::{NormalizedSymbolQuery, SymbolCandidateBucket, SymbolResolutionOutcome};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SymbolClassification {
Declaration,
Reference,
Import,
Ambiguous,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedBinding {
pub node_id: NodeId,
pub classification: SymbolClassification,
pub bucket: SymbolCandidateBucket,
pub kind: NodeKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BindingResult {
pub query: Option<NormalizedSymbolQuery>,
pub bindings: Vec<ResolvedBinding>,
pub outcome: SymbolResolutionOutcome,
}
pub(crate) fn classify_node(
snapshot: &GraphSnapshot,
node_id: NodeId,
kind: NodeKind,
) -> SymbolClassification {
if kind == NodeKind::Import {
return SymbolClassification::Import;
}
if kind == NodeKind::Export {
let incoming = snapshot.edges().edges_to(node_id);
let has_reexport = incoming.iter().any(|e| {
matches!(
&e.kind,
EdgeKind::Exports {
kind: ExportKind::Reexport | ExportKind::Namespace,
..
}
)
});
let outgoing = snapshot.edges().edges_from(node_id);
let has_reexport_outgoing = outgoing.iter().any(|e| {
matches!(
&e.kind,
EdgeKind::Exports {
kind: ExportKind::Reexport | ExportKind::Namespace,
..
}
)
});
return if has_reexport || has_reexport_outgoing {
SymbolClassification::Ambiguous
} else {
SymbolClassification::Import
};
}
if kind == NodeKind::CallSite {
return SymbolClassification::Reference;
}
let incoming = snapshot.edges().edges_to(node_id);
let has_structural = incoming
.iter()
.any(|e| matches!(&e.kind, EdgeKind::Defines | EdgeKind::Contains));
if has_structural {
return SymbolClassification::Declaration;
}
if !incoming.is_empty() {
return SymbolClassification::Reference;
}
let outgoing = snapshot.edges().edges_from(node_id);
let has_outgoing_structural = outgoing
.iter()
.any(|e| matches!(&e.kind, EdgeKind::Defines | EdgeKind::Contains));
if has_outgoing_structural {
return SymbolClassification::Declaration;
}
SymbolClassification::Unknown
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::node::Language;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::file::FileId;
use crate::graph::unified::storage::arena::NodeEntry;
struct TestGraph {
graph: CodeGraph,
file_id: Option<FileId>,
}
impl TestGraph {
fn new() -> Self {
Self {
graph: CodeGraph::new(),
file_id: None,
}
}
fn ensure_file_id(&mut self) -> FileId {
if let Some(fid) = self.file_id {
return fid;
}
let file_path = std::path::PathBuf::from("/bind-tests/test.rs");
let fid = self
.graph
.files_mut()
.register_with_language(&file_path, Some(Language::Rust))
.unwrap();
self.file_id = Some(fid);
fid
}
fn add_node(&mut self, name: &str, kind: NodeKind) -> NodeId {
let file_id = self.ensure_file_id();
let name_id = self.graph.strings_mut().intern(name).unwrap();
let qn_id = self
.graph
.strings_mut()
.intern(&format!("test::{name}"))
.unwrap();
let entry = NodeEntry::new(kind, name_id, file_id)
.with_qualified_name(qn_id)
.with_location(1, 0, 10, 0);
let node_id = self.graph.nodes_mut().alloc(entry).unwrap();
self.graph
.indices_mut()
.add(node_id, kind, name_id, Some(qn_id), file_id);
node_id
}
fn add_edge(&mut self, source: NodeId, target: NodeId, kind: EdgeKind) {
let file_id = self.ensure_file_id();
self.graph
.edges_mut()
.add_edge(source, target, kind, file_id);
}
fn snapshot(&self) -> GraphSnapshot {
self.graph.snapshot()
}
}
#[test]
fn declaration_classification() {
let mut tg = TestGraph::new();
let module_node = tg.add_node("my_module", NodeKind::Module);
let func_node = tg.add_node("my_func", NodeKind::Function);
tg.add_edge(module_node, func_node, EdgeKind::Defines);
let snapshot = tg.snapshot();
let result = BindingQuery::new("my_func").resolve(&snapshot);
assert!(!result.bindings.is_empty(), "expected at least one binding");
let binding = result
.bindings
.iter()
.find(|b| b.node_id == func_node)
.expect("expected binding for func_node");
assert_eq!(binding.classification, SymbolClassification::Declaration);
assert_eq!(binding.kind, NodeKind::Function);
}
#[test]
fn reference_classification_callsite() {
let mut tg = TestGraph::new();
let _call_node = tg.add_node("some_call", NodeKind::CallSite);
let snapshot = tg.snapshot();
let result = BindingQuery::new("some_call").resolve(&snapshot);
assert!(!result.bindings.is_empty());
assert_eq!(
result.bindings[0].classification,
SymbolClassification::Reference
);
}
#[test]
fn import_classification() {
let mut tg = TestGraph::new();
let _import_node = tg.add_node("imported_sym", NodeKind::Import);
let snapshot = tg.snapshot();
let result = BindingQuery::new("imported_sym").resolve(&snapshot);
assert!(!result.bindings.is_empty());
assert_eq!(
result.bindings[0].classification,
SymbolClassification::Import
);
}
#[test]
fn export_direct_classification() {
let mut tg = TestGraph::new();
let _export_node = tg.add_node("exported_sym", NodeKind::Export);
let snapshot = tg.snapshot();
let result = BindingQuery::new("exported_sym").resolve(&snapshot);
assert!(!result.bindings.is_empty());
assert_eq!(
result.bindings[0].classification,
SymbolClassification::Import
);
}
#[test]
fn export_reexport_ambiguous() {
let mut tg = TestGraph::new();
let source = tg.add_node("source_mod", NodeKind::Module);
let export_node = tg.add_node("reexported", NodeKind::Export);
tg.add_edge(
source,
export_node,
EdgeKind::Exports {
kind: ExportKind::Reexport,
alias: None,
},
);
let snapshot = tg.snapshot();
let result = BindingQuery::new("reexported").resolve(&snapshot);
assert!(!result.bindings.is_empty());
let binding = result
.bindings
.iter()
.find(|b| b.node_id == export_node)
.expect("expected binding for export_node");
assert_eq!(binding.classification, SymbolClassification::Ambiguous);
}
#[test]
fn not_found_result() {
let tg = TestGraph::new();
let snapshot = tg.snapshot();
let result = BindingQuery::new("nonexistent_symbol_xyz").resolve(&snapshot);
assert!(result.bindings.is_empty());
assert_eq!(result.outcome, SymbolResolutionOutcome::NotFound);
}
#[test]
fn declaration_via_contains_edge() {
let mut tg = TestGraph::new();
let class_node = tg.add_node("MyClass", NodeKind::Class);
let method_node = tg.add_node("my_method", NodeKind::Method);
tg.add_edge(class_node, method_node, EdgeKind::Contains);
let snapshot = tg.snapshot();
let result = BindingQuery::new("my_method").resolve(&snapshot);
assert!(!result.bindings.is_empty());
let binding = result
.bindings
.iter()
.find(|b| b.node_id == method_node)
.expect("expected binding for method_node");
assert_eq!(binding.classification, SymbolClassification::Declaration);
}
#[test]
fn root_declaration_with_outgoing_defines() {
let mut tg = TestGraph::new();
let module_node = tg.add_node("root_mod", NodeKind::Module);
let child = tg.add_node("child_func", NodeKind::Function);
tg.add_edge(module_node, child, EdgeKind::Defines);
let snapshot = tg.snapshot();
let result = BindingQuery::new("root_mod").resolve(&snapshot);
assert!(!result.bindings.is_empty());
let binding = result
.bindings
.iter()
.find(|b| b.node_id == module_node)
.expect("expected binding for module_node");
assert_eq!(binding.classification, SymbolClassification::Declaration);
}
}