Skip to main content

fluidattacks_blends_domain/stack/
build.rs

1mod functions;
2mod imports;
3mod lookups;
4mod module_chain;
5
6use alloc::string::String;
7
8use crate::stack::build::module_chain::module_segments;
9use crate::stack::{ScopeType, StackGraph, StackNode, StackScope, SymbolPop};
10use crate::syntax::{SyntaxGraph, SyntaxKind};
11use crate::{CodeGraph, Language, NodeId};
12
13/// Builds the stack graph of one file from its finished syntax graph.
14///
15/// `path`, relative to where imports resolve from and `/`-separated, names the
16/// module reached from the root. New nodes get ids above every id in `syntax`.
17#[must_use]
18pub fn build_stack_graph(syntax: &SyntaxGraph, language: Language, path: &str) -> StackGraph {
19    let mut builder = StackBuilder::after(syntax);
20    let root = builder.add(StackNode::Root);
21    let file = syntax
22        .nodes_by_type
23        .get(&SyntaxKind::File)
24        .into_iter()
25        .flatten()
26        .copied()
27        .find(|file| syntax.ast_parents(*file).is_empty());
28    if let Some(file) = file {
29        builder.graph.add_node(
30            file,
31            StackNode::Scope(StackScope {
32                scope_type: Some(ScopeType::File),
33            }),
34        );
35        builder.add_module_chain(root, file, &module_segments(language, path));
36    }
37    builder.add_function_scopes(syntax);
38    builder.add_lookups(syntax, language);
39    builder.add_imports(syntax, language, root, path);
40    builder.graph
41}
42
43struct StackBuilder {
44    graph: StackGraph,
45    next_id: u64,
46}
47
48impl StackBuilder {
49    fn after(syntax: &SyntaxGraph) -> Self {
50        let last_id = syntax.nodes.last_key_value().map_or(0, |(id, _)| id.0);
51        Self {
52            graph: StackGraph::new(),
53            next_id: last_id.saturating_add(1),
54        }
55    }
56
57    fn add(&mut self, node: StackNode) -> NodeId {
58        let id = NodeId(self.next_id);
59        self.next_id = self.next_id.saturating_add(1);
60        self.graph.add_node(id, node);
61        id
62    }
63
64    fn add_module_chain(&mut self, root: NodeId, file: NodeId, segments: &[&str]) {
65        if segments.is_empty() {
66            return;
67        }
68        let symbols = segments
69            .iter()
70            .enumerate()
71            .flat_map(|(index, segment)| (index > 0).then_some(".").into_iter().chain([*segment]));
72        let last = symbols.fold(root, |previous, symbol| {
73            let pop = self.add(StackNode::PopSymbol(SymbolPop {
74                symbol: String::from(symbol),
75                precedence: 0,
76                definition: None,
77                import: None,
78            }));
79            self.graph.add_edge(previous, pop, 0);
80            pop
81        });
82        self.graph.add_edge(last, file, 0);
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::build_stack_graph;
89    use crate::stack::{ScopeType, StackGraph, StackNode, StackScope};
90    use crate::syntax::{SyntaxGraph, SyntaxNode};
91    use crate::{Language, NodeId};
92    use alloc::vec::Vec;
93
94    const FILE: NodeId = NodeId(1);
95
96    fn file_syntax() -> SyntaxGraph {
97        let mut syntax = SyntaxGraph::new();
98        syntax.add_node(FILE, SyntaxNode::File);
99        syntax.add_node(NodeId(7), SyntaxNode::ExecutionBlock);
100        syntax
101    }
102
103    const ROOT: NodeId = NodeId(8);
104
105    fn path_from_root(graph: &StackGraph) -> (Vec<&str>, Option<NodeId>) {
106        let mut symbols = Vec::new();
107        let mut current = ROOT;
108        while let [(next, _)] = graph.successors(current).as_slice() {
109            match graph.nodes.get(next) {
110                Some(StackNode::PopSymbol(pop)) => symbols.push(pop.symbol.as_str()),
111                _ => return (symbols, Some(*next)),
112            }
113            current = *next;
114        }
115        (symbols, None)
116    }
117
118    #[test]
119    fn the_file_node_becomes_a_file_scope() {
120        let graph = build_stack_graph(&file_syntax(), Language::Python, "pkg/mod.py");
121
122        assert_eq!(
123            graph.nodes.get(&FILE),
124            Some(&StackNode::Scope(StackScope {
125                scope_type: Some(ScopeType::File),
126            }))
127        );
128    }
129
130    #[test]
131    fn the_root_reaches_the_file_scope_through_its_module_path() {
132        let graph = build_stack_graph(&file_syntax(), Language::Python, "pkg/sub/mod.py");
133
134        assert_eq!(
135            path_from_root(&graph),
136            (Vec::from(["pkg", ".", "sub", ".", "mod"]), Some(FILE))
137        );
138    }
139
140    #[test]
141    fn a_file_without_a_module_name_is_not_reachable_from_the_root() {
142        let graph = build_stack_graph(&file_syntax(), Language::Python, "__init__.py");
143
144        assert_eq!(path_from_root(&graph), (Vec::new(), None));
145    }
146
147    #[test]
148    fn a_nested_file_node_is_not_a_file_scope() {
149        let mut syntax = file_syntax();
150        syntax.add_node(NodeId(2), SyntaxNode::File);
151        syntax.add_ast_edge(FILE, NodeId(2));
152        let graph = build_stack_graph(&syntax, Language::Python, "pkg/mod.py");
153
154        assert_eq!(graph.nodes.get(&NodeId(2)), None);
155        assert_eq!(
156            path_from_root(&graph),
157            (Vec::from(["pkg", ".", "mod"]), Some(FILE))
158        );
159    }
160
161    #[test]
162    fn module_path_nodes_pop_symbols_without_defining_them() {
163        let graph = build_stack_graph(&file_syntax(), Language::JavaScript, "lib/x.js");
164
165        assert!(graph.nodes.values().all(|node| !node.is_definition()));
166    }
167
168    #[test]
169    fn the_root_and_the_module_path_get_ids_after_every_syntax_id() {
170        let graph = build_stack_graph(&file_syntax(), Language::Python, "pkg/mod.py");
171
172        assert_eq!(graph.nodes.get(&ROOT), Some(&StackNode::Root));
173        assert_eq!(
174            graph.nodes.keys().copied().collect::<Vec<_>>(),
175            [FILE, ROOT, NodeId(9), NodeId(10), NodeId(11)]
176        );
177    }
178}