fluidattacks-blends-domain 0.41.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
use crate::stack::build::StackBuilder;
use crate::stack::{StackNode, SymbolPush};
use crate::syntax::{SyntaxGraph, SyntaxKind, SyntaxNode};
use crate::{CodeGraph, Language, NodeId};

impl StackBuilder {
    pub(super) fn add_lookups(&mut self, syntax: &SyntaxGraph, language: Language) {
        let lookups = syntax
            .nodes_by_type
            .get(&SyntaxKind::SymbolLookup)
            .map_or(&[][..], |ids| ids.as_slice());
        for lookup in lookups {
            self.add_lookup(syntax, language, *lookup);
        }
    }

    fn add_lookup(&mut self, syntax: &SyntaxGraph, language: Language, lookup: NodeId) {
        let Some(SyntaxNode::SymbolLookup {
            symbol,
            symbol_scope: Some(scope),
            ..
        }) = syntax.nodes.get(&lookup)
        else {
            return;
        };
        if is_bound_by(syntax, language, lookup) {
            return;
        }
        self.graph.add_node(
            lookup,
            StackNode::PushSymbol(SymbolPush {
                symbol: symbol.clone(),
                precedence: 0,
                is_reference: true,
            }),
        );
        if matches!(self.graph.nodes.get(scope), Some(StackNode::Scope(_))) {
            self.graph.add_edge(lookup, *scope, 0);
        }
    }
}

fn is_bound_by(syntax: &SyntaxGraph, language: Language, lookup: NodeId) -> bool {
    let mut child = lookup;
    while let Some(parent) = syntax.ast_parents(child).first().copied() {
        if let Some(binds) = binds(syntax.nodes.get(&parent), child, language) {
            return binds;
        }
        child = parent;
    }
    false
}

fn binds(parent: Option<&SyntaxNode>, child: NodeId, language: Language) -> Option<bool> {
    let python = language == Language::Python;
    match parent {
        Some(
            SyntaxNode::ArrayInitializer { .. }
            | SyntaxNode::Object { .. }
            | SyntaxNode::Pair { .. }
            | SyntaxNode::RestPattern { .. },
        ) => None,
        Some(SyntaxNode::ParameterList) => Some(true),
        Some(SyntaxNode::VariableDeclaration { variable_id, .. }) => {
            Some(*variable_id == Some(child))
        }
        Some(SyntaxNode::CatchClause {
            catch_declaration, ..
        }) => Some(*catch_declaration == Some(child)),
        Some(SyntaxNode::ForEachStatement { variable_id, .. }) => Some(*variable_id == child),
        Some(SyntaxNode::ForStatement { initializer_id, .. }) => {
            Some(python && *initializer_id == Some(child))
        }
        Some(SyntaxNode::Assignment {
            variable_id,
            operator: None,
            ..
        }) => Some(python && *variable_id == child),
        _ => Some(false),
    }
}

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

    const FILE: NodeId = NodeId(1);
    const HOLDER: NodeId = NodeId(2);
    const LOOKUP: NodeId = NodeId(3);

    fn lookup_in(scope: NodeId) -> StackGraph {
        let mut syntax = SyntaxGraph::new();
        syntax.add_node(FILE, SyntaxNode::File);
        syntax.add_node(HOLDER, SyntaxNode::ArgumentList);
        syntax.add_ast_edge(FILE, HOLDER);
        let symbol = String::from("x");
        let symbol_scope = Some(scope);
        syntax.add_node(
            LOOKUP,
            SyntaxNode::SymbolLookup {
                symbol,
                symbol_scope,
                value: None,
            },
        );
        syntax.add_ast_edge(HOLDER, LOOKUP);
        build_stack_graph(&syntax, Language::Python, "mod.py")
    }

    #[test]
    fn a_lookup_pushes_its_symbol_without_attaching_scopes() {
        let push = SymbolPush {
            symbol: String::from("x"),
            precedence: 0,
            is_reference: true,
        };

        assert_eq!(
            lookup_in(FILE).nodes.get(&LOOKUP),
            Some(&StackNode::PushSymbol(push))
        );
    }

    #[test]
    fn a_lookup_searches_from_its_scope() {
        assert_eq!(lookup_in(FILE).successors(LOOKUP), [(FILE, 0)]);
    }

    #[test]
    fn a_lookup_whose_scope_is_not_yet_a_stack_scope_stays_unlinked() {
        assert_eq!(lookup_in(HOLDER).successors(LOOKUP), []);
    }
}