fluidattacks-blends-domain 0.44.2

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