use alloc::vec::Vec;
use crate::stack::build::StackBuilder;
use crate::stack::{Definition, ScopeType, StackNode, StackScope, SymbolPop};
use crate::syntax::{SyntaxGraph, SyntaxKind, SyntaxNode};
use crate::{CodeGraph, NodeId};
const SCOPES: [(SyntaxKind, ScopeType); 3] = [
(SyntaxKind::MethodDeclaration, ScopeType::Function),
(SyntaxKind::Class, ScopeType::Class),
(SyntaxKind::Declaration, ScopeType::Declaration),
];
impl StackBuilder {
pub(super) fn add_scopes(&mut self, syntax: &SyntaxGraph) {
let scopes: Vec<NodeId> = SCOPES
.iter()
.flat_map(|(kind, scope_type)| {
syntax
.nodes_by_type
.get(kind)
.into_iter()
.flatten()
.map(move |id| (*id, *scope_type))
})
.map(|(id, scope_type)| {
self.graph.add_node(
id,
StackNode::Scope(StackScope {
scope_type: Some(scope_type),
}),
);
id
})
.collect();
for scope in scopes {
self.link_scope(syntax, scope);
}
}
fn link_scope(&mut self, syntax: &SyntaxGraph, scope: NodeId) {
let Some(parent) = syntax
.scope_parent
.get(&scope)
.copied()
.filter(|parent| matches!(self.graph.nodes.get(parent), Some(StackNode::Scope(_))))
else {
return;
};
self.graph.add_edge(scope, parent, 0);
if self.is_class(parent) || !is_statement(syntax, scope) {
return;
}
let Some(name) = syntax
.nodes
.get(&scope)
.and_then(SyntaxNode::name)
.filter(|name| !name.is_empty())
else {
return;
};
let definition = self.add(StackNode::PopSymbol(SymbolPop {
symbol: name.into(),
precedence: 1,
definition: Some(Definition::default()),
import: None,
}));
self.graph.add_edge(parent, definition, 1);
self.graph.add_edge(definition, scope, 0);
}
fn is_class(&self, scope: NodeId) -> bool {
matches!(
self.graph.nodes.get(&scope),
Some(StackNode::Scope(StackScope {
scope_type: Some(ScopeType::Class),
}))
)
}
}
fn is_statement(syntax: &SyntaxGraph, scope: NodeId) -> bool {
syntax.ast_parents(scope).iter().any(|parent| {
matches!(
syntax.nodes.get(parent),
Some(SyntaxNode::File | SyntaxNode::ExecutionBlock | SyntaxNode::Export { .. })
)
})
}
#[cfg(test)]
mod tests {
use crate::stack::{build_stack_graph, ScopeType, StackGraph, StackNode, StackScope};
use crate::syntax::{SyntaxGraph, SyntaxNode};
use crate::{Language, NodeId};
use alloc::string::String;
use alloc::vec::Vec;
const FILE: NodeId = NodeId(1);
const HOLDER: NodeId = NodeId(2);
const OUTER: NodeId = NodeId(3);
const INNER: NodeId = NodeId(4);
const CLASS: NodeId = NodeId(5);
const BODY: NodeId = NodeId(6);
fn file_with(holder: SyntaxNode) -> SyntaxGraph {
let mut syntax = SyntaxGraph::new();
syntax.add_node(FILE, SyntaxNode::File);
syntax.add_node(HOLDER, holder);
syntax.add_ast_edge(FILE, HOLDER);
syntax
}
fn declare(
syntax: &mut SyntaxGraph,
function: NodeId,
name: Option<&str>,
at: (NodeId, NodeId),
) {
syntax.add_node(
function,
SyntaxNode::MethodDeclaration {
name: name.map(String::from),
access_modifiers: None,
block_id: None,
modifiers_id: None,
parameters_id: None,
},
);
syntax.add_ast_edge(at.0, function);
syntax.scope_parent.insert(function, at.1);
}
fn statement(name: &str) -> SyntaxGraph {
let mut syntax = file_with(SyntaxNode::ExecutionBlock);
declare(&mut syntax, OUTER, Some(name), (FILE, FILE));
syntax
}
fn stack(syntax: &SyntaxGraph) -> StackGraph {
build_stack_graph(syntax, Language::Python, "mod.py")
}
fn definitions_in(graph: &StackGraph, scope: NodeId) -> Vec<(&str, i64, Vec<NodeId>)> {
graph
.successors(scope)
.into_iter()
.filter_map(|(id, precedence)| {
let node = graph.nodes.get(&id).filter(|node| node.is_definition())?;
let targets = graph
.successors(id)
.into_iter()
.map(|(sink, _)| sink)
.collect();
Some((node.symbol()?, precedence, targets))
})
.collect()
}
#[test]
fn a_function_becomes_a_function_scope() {
let graph = stack(&statement("f"));
assert_eq!(
graph.nodes.get(&OUTER),
Some(&StackNode::Scope(StackScope {
scope_type: Some(ScopeType::Function),
}))
);
}
#[test]
fn a_function_scope_reaches_its_enclosing_scope() {
let graph = stack(&statement("f"));
assert_eq!(graph.successors(OUTER), [(FILE, 0)]);
}
#[test]
fn a_function_statement_defines_its_name_in_its_enclosing_scope_over_the_parent_edge() {
let graph = stack(&statement("f"));
assert_eq!(definitions_in(&graph, FILE), [("f", 1, Vec::from([OUTER]))]);
}
#[test]
fn a_nested_function_defines_its_name_in_the_outer_function() {
let mut syntax = statement("f");
syntax.add_ast_edge(OUTER, HOLDER);
declare(&mut syntax, INNER, Some("g"), (HOLDER, OUTER));
assert_eq!(
definitions_in(&stack(&syntax), OUTER),
[("g", 1, Vec::from([INNER]))]
);
}
#[test]
fn an_exported_function_defines_its_name_in_its_enclosing_scope() {
let mut syntax = file_with(SyntaxNode::Export {
expression: None,
declaration_id: Some(OUTER),
alias: None,
default_id: None,
});
declare(&mut syntax, OUTER, Some("f"), (HOLDER, FILE));
assert_eq!(
definitions_in(&stack(&syntax), FILE),
[("f", 1, Vec::from([OUTER]))]
);
}
#[test]
fn a_function_expression_does_not_define_its_name_in_the_enclosing_scope() {
let mut syntax = file_with(SyntaxNode::ArgumentList);
declare(&mut syntax, OUTER, Some("callback"), (HOLDER, FILE));
assert_eq!(definitions_in(&stack(&syntax), FILE), []);
}
#[test]
fn an_anonymous_function_defines_no_name() {
let mut syntax = file_with(SyntaxNode::ExecutionBlock);
declare(&mut syntax, OUTER, None, (FILE, FILE));
assert!(stack(&syntax)
.nodes
.values()
.all(|node| !node.is_definition()));
}
#[test]
fn a_function_whose_enclosing_scope_is_not_yet_a_stack_scope_stays_unlinked() {
let mut syntax = file_with(SyntaxNode::ExecutionBlock);
declare(&mut syntax, OUTER, Some("f"), (HOLDER, HOLDER));
let graph = stack(&syntax);
assert_eq!(graph.successors(OUTER), []);
assert!(graph.nodes.values().all(|node| !node.is_definition()));
}
fn class_named(name: &str) -> SyntaxNode {
SyntaxNode::Class {
name: String::from(name),
block_id: None,
modifiers_id: None,
inherited_class: None,
access_modifiers: None,
}
}
fn class_statement() -> SyntaxGraph {
let mut syntax = file_with(SyntaxNode::ExecutionBlock);
syntax.add_node(CLASS, class_named("C"));
syntax.add_ast_edge(FILE, CLASS);
syntax.scope_parent.insert(CLASS, FILE);
syntax.add_node(BODY, SyntaxNode::ExecutionBlock);
syntax.add_ast_edge(CLASS, BODY);
syntax
}
fn class_with_method() -> SyntaxGraph {
let mut syntax = class_statement();
declare(&mut syntax, OUTER, Some("m"), (BODY, CLASS));
syntax
}
#[test]
fn a_class_becomes_a_class_scope() {
let graph = stack(&class_statement());
assert_eq!(
graph.nodes.get(&CLASS),
Some(&StackNode::Scope(StackScope {
scope_type: Some(ScopeType::Class),
}))
);
}
#[test]
fn a_class_statement_defines_its_name_in_its_enclosing_scope_over_the_parent_edge() {
assert_eq!(
definitions_in(&stack(&class_statement()), FILE),
[("C", 1, Vec::from([CLASS]))]
);
}
#[test]
fn a_class_expression_does_not_define_its_name_in_the_enclosing_scope() {
let mut syntax = file_with(SyntaxNode::ArgumentList);
syntax.add_node(CLASS, class_named("C"));
syntax.add_ast_edge(HOLDER, CLASS);
syntax.scope_parent.insert(CLASS, FILE);
assert_eq!(definitions_in(&stack(&syntax), FILE), []);
}
#[test]
fn a_declaration_becomes_a_declaration_scope() {
let mut syntax = file_with(SyntaxNode::ExecutionBlock);
syntax.add_node(
CLASS,
SyntaxNode::Declaration {
name: String::from("Color"),
block_id: None,
access_modifiers: None,
},
);
syntax.add_ast_edge(FILE, CLASS);
syntax.scope_parent.insert(CLASS, FILE);
let graph = build_stack_graph(&syntax, Language::TypeScript, "mod.ts");
assert_eq!(
graph.nodes.get(&CLASS),
Some(&StackNode::Scope(StackScope {
scope_type: Some(ScopeType::Declaration),
}))
);
}
#[test]
fn a_method_reaches_its_class_scope() {
assert_eq!(stack(&class_with_method()).successors(OUTER), [(CLASS, 0)]);
}
#[test]
fn a_method_defines_no_name_in_its_class() {
assert_eq!(definitions_in(&stack(&class_with_method()), CLASS), []);
}
#[test]
fn a_class_inside_a_function_reaches_the_function_scope() {
let mut syntax = statement("f");
syntax.add_node(CLASS, class_named("C"));
syntax.add_ast_edge(OUTER, BODY);
syntax.add_node(BODY, SyntaxNode::ExecutionBlock);
syntax.add_ast_edge(BODY, CLASS);
syntax.scope_parent.insert(CLASS, OUTER);
assert_eq!(stack(&syntax).successors(CLASS), [(OUTER, 0)]);
}
}