fluidattacks-blends-domain 0.2.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;

use crate::syntax::builders::utils::{
    get_event_subpath, receiver_base_symbol, register_usage_event, setup_scope_index_binding,
};
use crate::syntax::metadata::java::del_metadata_instance;
use crate::syntax::{SyntaxGraph, SyntaxGraphArgs, SyntaxGraphError, SyntaxNode, UsageRole};
use crate::{Language, NodeId};

pub fn build_assignment_node(
    args: &mut SyntaxGraphArgs<'_>,
    n_id: NodeId,
    variable_id: NodeId,
    value_id: Option<NodeId>,
    operator: Option<String>,
) -> Result<NodeId, SyntaxGraphError> {
    args.syntax_graph.add_node(
        n_id,
        SyntaxNode::Assignment {
            variable_id,
            value_id,
            operator: operator.filter(|value| !value.is_empty()),
        },
    );

    let built_variable = args.generic(variable_id)?;
    args.syntax_graph.add_ast_edge(n_id, built_variable);

    if let Some(value_id) = value_id {
        let built_value = args.generic(value_id)?;
        args.syntax_graph.add_ast_edge(n_id, built_value);
    }

    if let Some(value_id) = value_id {
        if args.language == Language::Java && args.syntax_graph.nodes.contains_key(&NodeId(0)) {
            del_metadata_instance(args, variable_id, value_id)?;
        }
    }

    setup_scope_index_binding(
        args.ast_graph,
        args.syntax_graph,
        args.metadata,
        variable_id,
        n_id,
    );
    index_member_write(args, n_id, variable_id);

    Ok(n_id)
}

fn index_member_write(args: &mut SyntaxGraphArgs<'_>, n_id: NodeId, var_id: NodeId) {
    let (base, dotted) = member_write_names(args.syntax_graph, var_id);
    let Some(base) = base else {
        return;
    };
    let subpath = get_event_subpath(args.metadata);
    register_usage_event(
        args.syntax_graph,
        args.metadata,
        n_id,
        &base,
        UsageRole::MemberWrite,
        None,
        subpath,
    );
    if let Some(dotted) = dotted {
        if dotted != base {
            register_usage_event(
                args.syntax_graph,
                args.metadata,
                n_id,
                &dotted,
                UsageRole::MemberWrite,
                None,
                subpath,
            );
        }
    }
}

fn member_write_names(graph: &SyntaxGraph, var_id: NodeId) -> (Option<String>, Option<String>) {
    match graph.nodes.get(&var_id) {
        Some(SyntaxNode::MemberAccess {
            expression, member, ..
        }) => {
            let dotted =
                if !expression.is_empty() && !member.is_empty() && !expression.contains('(') {
                    Some(format!("{expression}.{member}"))
                } else {
                    None
                };
            (receiver_base_symbol(graph, var_id), dotted)
        }
        Some(SyntaxNode::SymbolLookup { symbol, .. }) => {
            if symbol.contains('.') && !symbol.contains('(') {
                let base = symbol.split('.').next().unwrap_or(symbol.as_str());
                (Some(base.to_owned()), Some(symbol.clone()))
            } else {
                (None, None)
            }
        }
        _ => (None, None),
    }
}

#[cfg(test)]
mod tests {
    use super::member_write_names;
    use crate::syntax::{SyntaxGraph, SyntaxNode};
    use crate::NodeId;
    use alloc::borrow::ToOwned;

    #[test]
    fn member_write_names_reads_member_access_base_and_dotted() {
        let mut graph = SyntaxGraph::new();
        graph.add_node(
            NodeId(1),
            SyntaxNode::SymbolLookup {
                symbol: "user".to_owned(),
                symbol_scope: None,
                value: None,
            },
        );
        graph.add_node(
            NodeId(2),
            SyntaxNode::MemberAccess {
                member: "name".to_owned(),
                expression: "user".to_owned(),
                expression_id: NodeId(1),
                symbol_scope: None,
            },
        );

        assert_eq!(
            member_write_names(&graph, NodeId(2)),
            (Some("user".to_owned()), Some("user.name".to_owned()))
        );
    }

    #[test]
    fn member_write_names_skips_call_expressions() {
        let mut graph = SyntaxGraph::new();
        graph.add_node(
            NodeId(1),
            SyntaxNode::MemberAccess {
                member: "name".to_owned(),
                expression: "get()".to_owned(),
                expression_id: NodeId(9),
                symbol_scope: None,
            },
        );

        assert_eq!(member_write_names(&graph, NodeId(1)).1, None);
    }

    #[test]
    fn member_write_names_splits_dotted_symbol_lookup() {
        let mut graph = SyntaxGraph::new();
        graph.add_node(
            NodeId(1),
            SyntaxNode::SymbolLookup {
                symbol: "obj.field".to_owned(),
                symbol_scope: None,
                value: None,
            },
        );

        assert_eq!(
            member_write_names(&graph, NodeId(1)),
            (Some("obj".to_owned()), Some("obj.field".to_owned()))
        );
    }

    #[test]
    fn member_write_names_ignores_plain_symbol_and_other_nodes() {
        let mut graph = SyntaxGraph::new();
        graph.add_node(
            NodeId(1),
            SyntaxNode::SymbolLookup {
                symbol: "x".to_owned(),
                symbol_scope: None,
                value: None,
            },
        );
        graph.add_node(NodeId(2), SyntaxNode::ArrayInitializer);

        assert_eq!(member_write_names(&graph, NodeId(1)), (None, None));
        assert_eq!(member_write_names(&graph, NodeId(2)), (None, None));
    }
}