fluidattacks-blends-domain 0.44.4

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, StackNode, SymbolPop};
use crate::syntax::{SyntaxGraph, SyntaxKind, SyntaxNode};
use crate::{CodeGraph, NodeId};

impl StackBuilder {
    pub(super) fn add_parameters(&mut self, syntax: &SyntaxGraph) {
        let functions = syntax
            .nodes_by_type
            .get(&SyntaxKind::MethodDeclaration)
            .map_or(&[][..], |ids| ids.as_slice());
        for function in functions {
            let Some(SyntaxNode::MethodDeclaration {
                parameters_id: Some(parameters),
                ..
            }) = syntax.nodes.get(function)
            else {
                continue;
            };
            let mut bindings = Vec::new();
            collect_bindings(syntax, *parameters, &mut bindings);
            bindings.retain(|(id, _)| !self.graph.nodes.contains_key(id));
            for (id, name) in bindings {
                self.graph.add_node(
                    id,
                    StackNode::PopSymbol(SymbolPop {
                        symbol: name.into(),
                        precedence: 1,
                        definition: Some(Definition::default()),
                        import: None,
                    }),
                );
                self.graph.add_edge(*function, id, 1);
            }
        }
    }
}

fn collect_bindings<'s>(syntax: &'s SyntaxGraph, node: NodeId, out: &mut Vec<(NodeId, &'s str)>) {
    let children = match syntax.nodes.get(&node) {
        Some(SyntaxNode::SymbolLookup { symbol, .. }) => {
            out.push((node, symbol.as_str()));
            return;
        }
        Some(SyntaxNode::Parameter {
            variable: Some(variable),
            ..
        }) => {
            let name = variable.trim_start_matches(['*', '.']);
            if !name.is_empty() {
                out.push((node, name));
            }
            return;
        }
        Some(SyntaxNode::Pair { value_id, .. } | SyntaxNode::RestPattern { value_id }) => {
            Vec::from([*value_id])
        }
        Some(SyntaxNode::MissingNode { node_type }) if node_type == "assignment_pattern" => {
            syntax.ast_children(node).into_iter().take(1).collect()
        }
        Some(
            SyntaxNode::ParameterList
            | SyntaxNode::Object { .. }
            | SyntaxNode::ArrayInitializer { .. }
            | SyntaxNode::MissingNode { .. },
        ) => syntax.ast_children(node),
        _ => Vec::new(),
    };
    for child in children {
        collect_bindings(syntax, child, out);
    }
}

#[cfg(test)]
mod tests {
    use crate::stack::{build_stack_graph, StackGraph, StackNode};
    use crate::syntax::{SyntaxGraph, SyntaxNode};
    use crate::{Language, NodeId};
    use alloc::string::String;
    use alloc::vec::Vec;

    const FILE: NodeId = NodeId(1);
    const FUNCTION: NodeId = NodeId(2);
    const PARAMETERS: NodeId = NodeId(3);

    fn lookup(symbol: &str) -> SyntaxNode {
        SyntaxNode::SymbolLookup {
            symbol: String::from(symbol),
            symbol_scope: Some(FUNCTION),
            value: None,
        }
    }

    fn parameter(variable: &str) -> SyntaxNode {
        SyntaxNode::Parameter {
            variable: Some(String::from(variable)),
            variable_type: None,
            value_id: None,
            parameter_mode: None,
        }
    }

    fn missing(node_type: &str) -> SyntaxNode {
        SyntaxNode::MissingNode {
            node_type: String::from(node_type),
        }
    }

    fn rest(value: u64) -> SyntaxNode {
        SyntaxNode::RestPattern {
            value_id: NodeId(value),
        }
    }

    fn pair(key: u64, value: u64) -> SyntaxNode {
        SyntaxNode::Pair {
            key_id: NodeId(key),
            value_id: NodeId(value),
        }
    }

    fn function_with(nodes: &[(NodeId, NodeId, SyntaxNode)]) -> SyntaxGraph {
        let mut syntax = SyntaxGraph::new();
        syntax.add_node(FILE, SyntaxNode::File);
        syntax.add_node(
            FUNCTION,
            SyntaxNode::MethodDeclaration {
                name: None,
                access_modifiers: None,
                block_id: None,
                modifiers_id: None,
                parameters_id: Some(PARAMETERS),
            },
        );
        syntax.add_ast_edge(FILE, FUNCTION);
        syntax.scope_parent.insert(FUNCTION, FILE);
        for (parent, id, node) in nodes {
            syntax.add_node(*id, node.clone());
            syntax.add_ast_edge(*parent, *id);
        }
        syntax
    }

    fn defined(graph: &StackGraph) -> Vec<(NodeId, &str)> {
        graph
            .successors(FUNCTION)
            .into_iter()
            .filter_map(|(id, _)| {
                let node = graph.nodes.get(&id).filter(|node| node.is_definition())?;
                Some((id, node.symbol()?))
            })
            .collect()
    }

    #[test]
    fn plain_typed_and_rest_parameters_are_defined_in_their_function() {
        let syntax = function_with(&[
            (FUNCTION, PARAMETERS, SyntaxNode::ParameterList),
            (PARAMETERS, NodeId(4), lookup("a")),
            (PARAMETERS, NodeId(5), parameter("b")),
            (PARAMETERS, NodeId(6), rest(7)),
            (NodeId(6), NodeId(7), lookup("args")),
            (PARAMETERS, NodeId(8), parameter("...rest")),
        ]);
        let graph = build_stack_graph(&syntax, Language::Python, "mod.py");

        assert_eq!(
            defined(&graph),
            [
                (NodeId(4), "a"),
                (NodeId(5), "b"),
                (NodeId(7), "args"),
                (NodeId(8), "rest")
            ]
        );
    }

    #[test]
    fn a_default_value_is_not_a_parameter_and_stays_a_reference() {
        let syntax = function_with(&[
            (FUNCTION, PARAMETERS, SyntaxNode::ParameterList),
            (PARAMETERS, NodeId(4), missing("assignment_pattern")),
            (NodeId(4), NodeId(5), lookup("b")),
            (NodeId(4), NodeId(6), lookup("fallback")),
        ]);
        let graph = build_stack_graph(&syntax, Language::JavaScript, "mod.js");

        assert_eq!(defined(&graph), [(NodeId(5), "b")]);
        let fallback = graph.nodes.get(&NodeId(6));
        assert!(fallback.is_some_and(StackNode::is_reference));
    }

    #[test]
    fn destructured_parameters_define_their_bound_names_and_not_their_keys() {
        let syntax = function_with(&[
            (FUNCTION, PARAMETERS, SyntaxNode::ParameterList),
            (
                PARAMETERS,
                NodeId(4),
                SyntaxNode::Object {
                    name: None,
                    tf_reference: None,
                },
            ),
            (NodeId(4), NodeId(5), lookup("k")),
            (NodeId(4), NodeId(6), pair(7, 8)),
            (NodeId(6), NodeId(7), lookup("m")),
            (NodeId(6), NodeId(8), lookup("n")),
            (PARAMETERS, NodeId(9), missing("array_pattern")),
            (NodeId(9), NodeId(10), lookup("u")),
        ]);
        let graph = build_stack_graph(&syntax, Language::JavaScript, "mod.js");

        assert_eq!(
            defined(&graph),
            [(NodeId(5), "k"), (NodeId(8), "n"), (NodeId(10), "u")]
        );
    }

    #[test]
    fn a_lone_arrow_parameter_is_defined_in_its_function() {
        let syntax = function_with(&[(FUNCTION, PARAMETERS, parameter("q"))]);

        assert_eq!(
            defined(&build_stack_graph(&syntax, Language::JavaScript, "mod.js")),
            [(PARAMETERS, "q")]
        );
    }
}