fluidattacks-blends-domain 0.37.0

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