use crate::stack::build::StackBuilder;
use crate::stack::{Definition, ScopeType, StackNode, StackScope, SymbolPop};
use crate::syntax::{SyntaxGraph, SyntaxKind, SyntaxNode};
use crate::{CodeGraph, NodeId};
impl StackBuilder {
pub(super) fn add_function_scopes(&mut self, syntax: &SyntaxGraph) {
let functions = syntax
.nodes_by_type
.get(&SyntaxKind::MethodDeclaration)
.map_or(&[][..], |ids| ids.as_slice());
for function in functions {
self.graph.add_node(
*function,
StackNode::Scope(StackScope {
scope_type: Some(ScopeType::Function),
}),
);
}
for function in functions {
self.link_function(syntax, *function);
}
}
fn link_function(&mut self, syntax: &SyntaxGraph, function: NodeId) {
let Some(parent) = syntax
.scope_parent
.get(&function)
.copied()
.filter(|parent| matches!(self.graph.nodes.get(parent), Some(StackNode::Scope(_))))
else {
return;
};
self.graph.add_edge(function, parent, 0);
let Some(SyntaxNode::MethodDeclaration {
name: Some(name), ..
}) = syntax.nodes.get(&function)
else {
return;
};
if name.is_empty() || !is_statement(syntax, function) {
return;
}
let definition = self.add(StackNode::PopSymbol(SymbolPop {
symbol: name.clone(),
precedence: 1,
definition: Some(Definition::default()),
import: None,
}));
self.graph.add_edge(parent, definition, 1);
self.graph.add_edge(definition, function, 0);
}
}
fn is_statement(syntax: &SyntaxGraph, function: NodeId) -> bool {
syntax.ast_parents(function).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);
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()));
}
}