use alloc::string::String;
use crate::ast::AstGraph;
use crate::query::{adj_ast, label_text};
use crate::syntax::SyntaxGraph;
use crate::syntax::SyntaxMetadata;
use crate::syntax::SyntaxNode;
use crate::syntax::{UsageEvent, UsageRole};
use crate::CodeGraph;
use crate::NodeId;
#[must_use]
pub fn bound_import_symbol(expression: &str, alias: Option<&str>) -> String {
alias.filter(|value| !value.is_empty()).map_or_else(
|| String::from(expression.rsplit('.').next().unwrap_or(expression)),
String::from,
)
}
#[must_use]
pub fn get_event_subpath(metadata: &SyntaxMetadata) -> Option<NodeId> {
metadata.control_flow_stack.last().copied()
}
pub fn record_subpath(graph: &mut SyntaxGraph, metadata: &SyntaxMetadata, def_nid: NodeId) {
graph
.subpath_index
.insert(def_nid, get_event_subpath(metadata));
}
pub fn register_symbol_in_scope(
graph: &mut SyntaxGraph,
metadata: &SyntaxMetadata,
symbol: String,
def_nid: NodeId,
) {
if let Some(scope) = metadata.scope_stack.last().copied() {
graph
.symbol_index
.entry(symbol)
.or_default()
.entry(scope)
.or_default()
.push(def_nid);
record_subpath(graph, metadata, def_nid);
}
}
#[must_use]
pub fn bound_identifier_symbol(ast_graph: &AstGraph, var_id: NodeId) -> Option<String> {
if ast_graph.label_type(var_id) != Some("identifier") {
return None;
}
label_text(ast_graph, var_id)
.filter(|text| !text.is_empty())
.map(String::from)
}
pub fn register_multi_binding(
graph: &mut SyntaxGraph,
metadata: &SyntaxMetadata,
var_id: NodeId,
scope: NodeId,
n_id: NodeId,
) {
for child in adj_ast(graph, var_id, Some(1), &["SymbolLookup"]) {
let Some(SyntaxNode::SymbolLookup { symbol, .. }) = graph.nodes.get(&child) else {
continue;
};
if symbol.is_empty() || symbol == "_" {
continue;
}
let symbol = symbol.clone();
graph
.symbol_index
.entry(symbol)
.or_default()
.entry(scope)
.or_default()
.push(n_id);
record_subpath(graph, metadata, n_id);
}
}
pub fn setup_scope_index_binding(
ast_graph: &AstGraph,
graph: &mut SyntaxGraph,
metadata: &SyntaxMetadata,
var_id: NodeId,
n_id: NodeId,
) {
let Some(scope) = metadata.scope_stack.last().copied() else {
return;
};
if let Some(symbol) = bound_identifier_symbol(ast_graph, var_id) {
register_symbol_in_scope(graph, metadata, symbol, n_id);
} else if matches!(
graph.label_type(var_id),
Some("ArgumentList" | "ArrayInitializer")
) {
register_multi_binding(graph, metadata, var_id, scope, n_id);
}
}
pub fn enter_scope_stack(graph: &mut SyntaxGraph, metadata: &mut SyntaxMetadata, n_id: NodeId) {
if let Some(parent) = metadata.scope_stack.last().copied() {
graph.scope_parent.insert(n_id, parent);
}
metadata.scope_stack.push(n_id);
}
pub fn exit_scope_stack_if_current(metadata: &mut SyntaxMetadata, n_id: NodeId) {
if metadata.scope_stack.last() == Some(&n_id) {
metadata.scope_stack.pop();
}
}
#[must_use]
pub fn find_definition_scope(
graph: &SyntaxGraph,
scope_stack: &[NodeId],
symbol: &str,
) -> Option<NodeId> {
let symbol_scopes = graph.symbol_index.get(symbol);
scope_stack.iter().rev().copied().find(|scope| {
symbol_scopes
.and_then(|scopes| scopes.get(scope))
.is_some_and(|defs| !defs.is_empty())
})
}
#[must_use]
pub fn receiver_descent_target(graph: &SyntaxGraph, n_id: NodeId) -> Option<NodeId> {
match graph.nodes.get(&n_id)? {
SyntaxNode::MemberAccess { expression_id, .. }
| SyntaxNode::ElementAccess { expression_id, .. } => Some(*expression_id),
SyntaxNode::ParenthesizedExpression => adj_ast(graph, n_id, Some(1), &[]).first().copied(),
SyntaxNode::MethodInvocation {
object_id,
expression_id,
..
} => (*object_id).or_else(|| {
(*expression_id).filter(|callee_id| {
matches!(
graph.nodes.get(callee_id),
Some(SyntaxNode::MemberAccess { .. })
)
})
}),
_ => None,
}
}
#[must_use]
pub fn receiver_base_symbol(graph: &SyntaxGraph, n_id: NodeId) -> Option<String> {
if let Some(SyntaxNode::SymbolLookup { symbol, .. }) = graph.nodes.get(&n_id) {
return (!symbol.is_empty()).then(|| symbol.clone());
}
if let Some(target) = receiver_descent_target(graph, n_id) {
return receiver_base_symbol(graph, target);
}
if let Some(SyntaxNode::MemberAccess { expression, .. }) = graph.nodes.get(&n_id) {
if !expression.is_empty() && !expression.contains('(') {
let base = expression.split('.').next().unwrap_or(expression.as_str());
return Some(String::from(base));
}
}
None
}
#[allow(
clippy::too_many_arguments,
reason = "mirrors the Python register_usage_event inputs"
)]
pub fn register_usage_event(
graph: &mut SyntaxGraph,
metadata: &SyntaxMetadata,
n_id: NodeId,
symbol: &str,
role: UsageRole,
arg_index: Option<i64>,
subpath: Option<NodeId>,
) {
let scope = find_definition_scope(graph, &metadata.scope_stack, symbol)
.or_else(|| metadata.scope_stack.last().copied());
let Some(scope) = scope else {
return;
};
graph.register_usage(
symbol,
scope,
UsageEvent {
node_id: n_id,
role,
arg_index,
subpath,
},
);
}
#[cfg(test)]
mod tests {
use super::{
bound_identifier_symbol, enter_scope_stack, exit_scope_stack_if_current,
find_definition_scope, get_event_subpath, receiver_base_symbol, receiver_descent_target,
record_subpath, register_multi_binding, register_usage_event, setup_scope_index_binding,
};
use crate::ast::{AstGraph, AstNode};
use crate::syntax::SyntaxGraph;
use crate::syntax::SyntaxMetadata;
use crate::syntax::SyntaxNode;
use crate::syntax::{UsageEvent, UsageRole};
use crate::NodeId;
use alloc::borrow::ToOwned;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn event_subpath_is_the_control_flow_stack_top_or_none() {
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
assert_eq!(get_event_subpath(&metadata), None);
metadata.control_flow_stack.push(NodeId(5));
metadata.control_flow_stack.push(NodeId(8));
assert_eq!(get_event_subpath(&metadata), Some(NodeId(8)));
}
#[test]
fn recorded_subpath_is_the_active_control_flow_top_or_none() {
let mut graph = SyntaxGraph::new();
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
record_subpath(&mut graph, &metadata, NodeId(4));
metadata.control_flow_stack.push(NodeId(9));
record_subpath(&mut graph, &metadata, NodeId(5));
assert_eq!(graph.subpath_index.get(&NodeId(4)), Some(&None));
assert_eq!(graph.subpath_index.get(&NodeId(5)), Some(&Some(NodeId(9))));
}
#[test]
fn entering_a_scope_records_the_parent_and_pushes_the_stack() {
let mut graph = SyntaxGraph::new();
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
enter_scope_stack(&mut graph, &mut metadata, NodeId(5));
assert_eq!(metadata.scope_stack, vec![NodeId(1), NodeId(5)]);
assert_eq!(graph.scope_parent.get(&NodeId(5)), Some(&NodeId(1)));
}
#[test]
fn entering_a_scope_on_an_empty_stack_records_no_parent() {
let mut graph = SyntaxGraph::new();
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
metadata.scope_stack = Vec::new();
enter_scope_stack(&mut graph, &mut metadata, NodeId(5));
assert_eq!(metadata.scope_stack, vec![NodeId(5)]);
assert!(graph.scope_parent.is_empty());
}
#[test]
fn exiting_pops_only_when_the_node_is_the_current_scope() {
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
metadata.scope_stack = vec![NodeId(1), NodeId(5)];
exit_scope_stack_if_current(&mut metadata, NodeId(7));
assert_eq!(metadata.scope_stack, vec![NodeId(1), NodeId(5)]);
exit_scope_stack_if_current(&mut metadata, NodeId(5));
assert_eq!(metadata.scope_stack, vec![NodeId(1)]);
}
#[test]
fn definition_scope_is_none_without_a_matching_scope() {
let graph = SyntaxGraph::new();
assert_eq!(find_definition_scope(&graph, &[], "x"), None);
assert_eq!(find_definition_scope(&graph, &[NodeId(1)], "x"), None);
}
#[test]
fn definition_scope_returns_the_nearest_scope_holding_the_symbol() {
let mut graph = SyntaxGraph::new();
let mut scopes: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
scopes.insert(NodeId(1), vec![NodeId(10)]);
scopes.insert(NodeId(2), vec![NodeId(20)]);
graph.symbol_index.insert("x".to_owned(), scopes);
assert_eq!(
find_definition_scope(&graph, &[NodeId(1), NodeId(2), NodeId(3)], "x"),
Some(NodeId(2))
);
assert_eq!(
find_definition_scope(&graph, &[NodeId(1)], "x"),
Some(NodeId(1))
);
}
#[test]
fn definition_scope_skips_scopes_with_empty_definition_lists() {
let mut graph = SyntaxGraph::new();
let mut scopes: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
scopes.insert(NodeId(2), Vec::new());
scopes.insert(NodeId(1), vec![NodeId(10)]);
graph.symbol_index.insert("x".to_owned(), scopes);
assert_eq!(
find_definition_scope(&graph, &[NodeId(1), NodeId(2)], "x"),
Some(NodeId(1))
);
}
#[test]
fn usage_event_registers_at_the_definition_scope_when_known() {
let mut graph = SyntaxGraph::new();
let mut scopes: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
scopes.insert(NodeId(1), vec![NodeId(10)]);
graph.symbol_index.insert("x".to_owned(), scopes);
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
metadata.scope_stack = vec![NodeId(1), NodeId(2)];
register_usage_event(
&mut graph,
&metadata,
NodeId(7),
"x",
UsageRole::Receiver,
None,
None,
);
assert_eq!(
graph
.usage_index
.get("x")
.and_then(|by_scope| by_scope.get(&NodeId(1))),
Some(&vec![UsageEvent {
node_id: NodeId(7),
role: UsageRole::Receiver,
arg_index: None,
subpath: None,
}])
);
}
#[test]
fn usage_event_falls_back_to_the_current_scope_when_undefined() {
let mut graph = SyntaxGraph::new();
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
metadata.scope_stack = vec![NodeId(1), NodeId(2)];
register_usage_event(
&mut graph,
&metadata,
NodeId(7),
"y",
UsageRole::Argument,
Some(0),
Some(NodeId(9)),
);
assert_eq!(
graph
.usage_index
.get("y")
.and_then(|scopes| scopes.get(&NodeId(2))),
Some(&vec![UsageEvent {
node_id: NodeId(7),
role: UsageRole::Argument,
arg_index: Some(0),
subpath: Some(NodeId(9)),
}])
);
}
#[test]
fn usage_event_is_dropped_when_no_scope_resolves() {
let mut graph = SyntaxGraph::new();
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
metadata.scope_stack = Vec::new();
register_usage_event(
&mut graph,
&metadata,
NodeId(7),
"z",
UsageRole::MemberWrite,
None,
None,
);
assert!(graph.usage_index.is_empty());
}
#[test]
fn descent_target_follows_each_receiver_shape() {
let mut graph = SyntaxGraph::new();
graph.add_node(
NodeId(1),
SyntaxNode::MemberAccess {
member: "m".to_owned(),
expression: "a".to_owned(),
expression_id: NodeId(2),
symbol_scope: None,
},
);
graph.add_node(
NodeId(3),
SyntaxNode::ElementAccess {
expression_id: NodeId(4),
arguments_id: None,
},
);
graph.add_node(NodeId(5), SyntaxNode::ParenthesizedExpression);
graph.add_ast_edge(NodeId(5), NodeId(6));
graph.add_node(
NodeId(7),
SyntaxNode::MethodInvocation {
expression: "f".to_owned(),
object: None,
symbol_scope: None,
expression_id: None,
arguments_id: None,
object_id: Some(NodeId(8)),
block_id: None,
receiver_type_fqn: None,
},
);
assert_eq!(receiver_descent_target(&graph, NodeId(1)), Some(NodeId(2)));
assert_eq!(receiver_descent_target(&graph, NodeId(3)), Some(NodeId(4)));
assert_eq!(receiver_descent_target(&graph, NodeId(5)), Some(NodeId(6)));
assert_eq!(receiver_descent_target(&graph, NodeId(7)), Some(NodeId(8)));
assert_eq!(receiver_descent_target(&graph, NodeId(99)), None);
}
#[test]
fn base_symbol_descends_to_the_root_identifier() {
let mut graph = SyntaxGraph::new();
graph.add_node(
NodeId(1),
SyntaxNode::SymbolLookup {
symbol: "obj".to_owned(),
symbol_scope: None,
value: None,
},
);
graph.add_node(
NodeId(2),
SyntaxNode::MemberAccess {
member: "method".to_owned(),
expression: "obj".to_owned(),
expression_id: NodeId(1),
symbol_scope: None,
},
);
graph.add_node(
NodeId(3),
SyntaxNode::SymbolLookup {
symbol: String::new(),
symbol_scope: None,
value: None,
},
);
assert_eq!(
receiver_base_symbol(&graph, NodeId(1)),
Some("obj".to_owned())
);
assert_eq!(
receiver_base_symbol(&graph, NodeId(2)),
Some("obj".to_owned())
);
assert_eq!(receiver_base_symbol(&graph, NodeId(3)), None);
}
#[test]
fn bound_identifier_symbol_reads_only_non_empty_identifier_text() {
let mut ast = AstGraph::new();
let mut ident = AstNode::new(1, 0, "identifier".to_owned());
ident.text = Some("value".to_owned());
ast.add_node(NodeId(1), ident);
let mut empty = AstNode::new(2, 0, "identifier".to_owned());
empty.text = Some(String::new());
ast.add_node(NodeId(2), empty);
ast.add_node(NodeId(3), AstNode::new(3, 0, "number".to_owned()));
assert_eq!(
bound_identifier_symbol(&ast, NodeId(1)),
Some("value".to_owned())
);
assert_eq!(bound_identifier_symbol(&ast, NodeId(2)), None);
assert_eq!(bound_identifier_symbol(&ast, NodeId(3)), None);
assert_eq!(bound_identifier_symbol(&ast, NodeId(9)), None);
}
#[test]
fn multi_binding_registers_each_named_symbol_lookup_child_against_the_statement() {
let mut graph = SyntaxGraph::new();
let metadata = SyntaxMetadata::seeded(NodeId(1));
graph.add_node(NodeId(10), SyntaxNode::ArrayInitializer);
for (child, symbol) in [
(NodeId(11), "a"),
(NodeId(12), "_"),
(NodeId(13), ""),
(NodeId(14), "b"),
] {
graph.add_node(
child,
SyntaxNode::SymbolLookup {
symbol: symbol.to_owned(),
symbol_scope: None,
value: None,
},
);
graph.add_ast_edge(NodeId(10), child);
}
register_multi_binding(&mut graph, &metadata, NodeId(10), NodeId(1), NodeId(10));
assert_eq!(
graph.symbol_index.get("a").and_then(|s| s.get(&NodeId(1))),
Some(&vec![NodeId(10)])
);
assert_eq!(
graph.symbol_index.get("b").and_then(|s| s.get(&NodeId(1))),
Some(&vec![NodeId(10)])
);
assert!(!graph.symbol_index.contains_key("_"));
assert!(!graph.symbol_index.contains_key(""));
assert_eq!(graph.subpath_index.get(&NodeId(10)), Some(&None));
}
#[test]
fn scope_index_binding_binds_a_bound_identifier() {
let mut ast = AstGraph::new();
let mut ident = AstNode::new(1, 0, "identifier".to_owned());
ident.text = Some("x".to_owned());
ast.add_node(NodeId(5), ident);
let mut graph = SyntaxGraph::new();
let metadata = SyntaxMetadata::seeded(NodeId(1));
setup_scope_index_binding(&ast, &mut graph, &metadata, NodeId(5), NodeId(7));
assert_eq!(
graph.symbol_index.get("x").and_then(|s| s.get(&NodeId(1))),
Some(&vec![NodeId(7)])
);
}
#[test]
fn scope_index_binding_falls_back_to_multi_binding_for_patterns() {
let ast = AstGraph::new();
let mut graph = SyntaxGraph::new();
let metadata = SyntaxMetadata::seeded(NodeId(1));
graph.add_node(NodeId(5), SyntaxNode::ArgumentList);
graph.add_node(
NodeId(6),
SyntaxNode::SymbolLookup {
symbol: "p".to_owned(),
symbol_scope: None,
value: None,
},
);
graph.add_ast_edge(NodeId(5), NodeId(6));
setup_scope_index_binding(&ast, &mut graph, &metadata, NodeId(5), NodeId(7));
assert_eq!(
graph.symbol_index.get("p").and_then(|s| s.get(&NodeId(1))),
Some(&vec![NodeId(7)])
);
}
#[test]
fn scope_index_binding_is_a_noop_without_a_scope() {
let mut ast = AstGraph::new();
let mut ident = AstNode::new(1, 0, "identifier".to_owned());
ident.text = Some("x".to_owned());
ast.add_node(NodeId(5), ident);
let mut graph = SyntaxGraph::new();
let mut metadata = SyntaxMetadata::seeded(NodeId(1));
metadata.scope_stack = Vec::new();
setup_scope_index_binding(&ast, &mut graph, &metadata, NodeId(5), NodeId(7));
assert!(graph.symbol_index.is_empty());
}
}