fluidattacks-blends-domain 0.41.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
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};

/// Builds the stack graph of one file from its finished syntax graph.
///
/// `path`, relative to where imports resolve from and `/`-separated, names the
/// module reached from the root. New nodes get ids above every id in `syntax`.
#[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)]
        );
    }
}