mod functions;
mod imports;
mod lookups;
mod module_chain;
use alloc::string::String;
use crate::stack::build::module_chain::module_segments;
use crate::stack::{ScopeType, StackGraph, StackNode, StackScope, SymbolPop};
use crate::syntax::{SyntaxGraph, SyntaxKind};
use crate::{CodeGraph, Language, NodeId};
#[must_use]
pub fn build_stack_graph(syntax: &SyntaxGraph, language: Language, path: &str) -> StackGraph {
let mut builder = StackBuilder::after(syntax);
let root = builder.add(StackNode::Root);
let file = syntax
.nodes_by_type
.get(&SyntaxKind::File)
.into_iter()
.flatten()
.copied()
.find(|file| syntax.ast_parents(*file).is_empty());
if let Some(file) = file {
builder.graph.add_node(
file,
StackNode::Scope(StackScope {
scope_type: Some(ScopeType::File),
}),
);
builder.add_module_chain(root, file, &module_segments(language, path));
}
builder.add_function_scopes(syntax);
builder.add_lookups(syntax, language);
builder.add_imports(syntax, language, root, path);
builder.graph
}
struct StackBuilder {
graph: StackGraph,
next_id: u64,
}
impl StackBuilder {
fn after(syntax: &SyntaxGraph) -> Self {
let last_id = syntax.nodes.last_key_value().map_or(0, |(id, _)| id.0);
Self {
graph: StackGraph::new(),
next_id: last_id.saturating_add(1),
}
}
fn add(&mut self, node: StackNode) -> NodeId {
let id = NodeId(self.next_id);
self.next_id = self.next_id.saturating_add(1);
self.graph.add_node(id, node);
id
}
fn add_module_chain(&mut self, root: NodeId, file: NodeId, segments: &[&str]) {
if segments.is_empty() {
return;
}
let symbols = segments
.iter()
.enumerate()
.flat_map(|(index, segment)| (index > 0).then_some(".").into_iter().chain([*segment]));
let last = symbols.fold(root, |previous, symbol| {
let pop = self.add(StackNode::PopSymbol(SymbolPop {
symbol: String::from(symbol),
precedence: 0,
definition: None,
import: None,
}));
self.graph.add_edge(previous, pop, 0);
pop
});
self.graph.add_edge(last, file, 0);
}
}
#[cfg(test)]
mod tests {
use super::build_stack_graph;
use crate::stack::{ScopeType, StackGraph, StackNode, StackScope};
use crate::syntax::{SyntaxGraph, SyntaxNode};
use crate::{Language, NodeId};
use alloc::vec::Vec;
const FILE: NodeId = NodeId(1);
fn file_syntax() -> SyntaxGraph {
let mut syntax = SyntaxGraph::new();
syntax.add_node(FILE, SyntaxNode::File);
syntax.add_node(NodeId(7), SyntaxNode::ExecutionBlock);
syntax
}
const ROOT: NodeId = NodeId(8);
fn path_from_root(graph: &StackGraph) -> (Vec<&str>, Option<NodeId>) {
let mut symbols = Vec::new();
let mut current = ROOT;
while let [(next, _)] = graph.successors(current).as_slice() {
match graph.nodes.get(next) {
Some(StackNode::PopSymbol(pop)) => symbols.push(pop.symbol.as_str()),
_ => return (symbols, Some(*next)),
}
current = *next;
}
(symbols, None)
}
#[test]
fn the_file_node_becomes_a_file_scope() {
let graph = build_stack_graph(&file_syntax(), Language::Python, "pkg/mod.py");
assert_eq!(
graph.nodes.get(&FILE),
Some(&StackNode::Scope(StackScope {
scope_type: Some(ScopeType::File),
}))
);
}
#[test]
fn the_root_reaches_the_file_scope_through_its_module_path() {
let graph = build_stack_graph(&file_syntax(), Language::Python, "pkg/sub/mod.py");
assert_eq!(
path_from_root(&graph),
(Vec::from(["pkg", ".", "sub", ".", "mod"]), Some(FILE))
);
}
#[test]
fn a_file_without_a_module_name_is_not_reachable_from_the_root() {
let graph = build_stack_graph(&file_syntax(), Language::Python, "__init__.py");
assert_eq!(path_from_root(&graph), (Vec::new(), None));
}
#[test]
fn a_nested_file_node_is_not_a_file_scope() {
let mut syntax = file_syntax();
syntax.add_node(NodeId(2), SyntaxNode::File);
syntax.add_ast_edge(FILE, NodeId(2));
let graph = build_stack_graph(&syntax, Language::Python, "pkg/mod.py");
assert_eq!(graph.nodes.get(&NodeId(2)), None);
assert_eq!(
path_from_root(&graph),
(Vec::from(["pkg", ".", "mod"]), Some(FILE))
);
}
#[test]
fn module_path_nodes_pop_symbols_without_defining_them() {
let graph = build_stack_graph(&file_syntax(), Language::JavaScript, "lib/x.js");
assert!(graph.nodes.values().all(|node| !node.is_definition()));
}
#[test]
fn the_root_and_the_module_path_get_ids_after_every_syntax_id() {
let graph = build_stack_graph(&file_syntax(), Language::Python, "pkg/mod.py");
assert_eq!(graph.nodes.get(&ROOT), Some(&StackNode::Root));
assert_eq!(
graph.nodes.keys().copied().collect::<Vec<_>>(),
[FILE, ROOT, NodeId(9), NodeId(10), NodeId(11)]
);
}
}