fluidattacks-blends-domain 0.17.1

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
//! Counterpart of `blends/syntax/builders/import_statement.py` (the
//! `StackGraph` binding and import-chain wiring is excluded: the migration
//! covers only AST, Syntax and CFG).

use alloc::string::String;
use alloc::vec::Vec;

use crate::syntax::builders::utils::{bound_import_symbol, register_symbol_in_scope};
use crate::syntax::SyntaxNode;
use crate::syntax::{SyntaxGraphArgs, SyntaxGraphError};
use crate::NodeId;

/// Node an element materializes as: a bound name, or a member of a JS/TS
/// import statement.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ImportLabel {
    #[default]
    Import,
    ModuleImport,
}

#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct ImportElement {
    pub expression: Option<String>,
    pub alias: Option<String>,
    pub method_name: Option<String>,
    pub corrected_n_id: Option<NodeId>,
    pub label_type: ImportLabel,
    /// Children dispatched through the reader pipeline instead of built here.
    pub module_nodes: Vec<NodeId>,
}

fn element_node(element: &ImportElement, expression: String, alias: Option<String>) -> SyntaxNode {
    match element.label_type {
        ImportLabel::Import => SyntaxNode::Import {
            expression: Some(expression),
            alias,
            method_name: element.method_name.clone(),
            import_type: None,
        },
        ImportLabel::ModuleImport => SyntaxNode::ModuleImport { expression, alias },
    }
}

/// Emit a single import node. The only place `Import`/`ModuleImport` nodes are
/// created.
pub fn build_import_node(
    args: &mut SyntaxGraphArgs<'_>,
    n_id: NodeId,
    element: &ImportElement,
) -> Result<NodeId, SyntaxGraphError> {
    let expression = element.expression.clone().unwrap_or_default();
    let alias = element.alias.clone().filter(|value| !value.is_empty());
    let bound = bound_import_symbol(&expression, alias.as_deref());

    args.syntax_graph
        .add_node(n_id, element_node(element, expression, alias));

    for node in &element.module_nodes {
        let built = args.generic(*node)?;
        args.syntax_graph.add_ast_edge(n_id, built);
    }

    if !bound.is_empty() {
        register_symbol_in_scope(args.syntax_graph, args.metadata, bound, n_id);
    }

    Ok(n_id)
}

pub fn build_wildcard_import_node(
    args: &mut SyntaxGraphArgs<'_>,
    n_id: NodeId,
) -> Result<NodeId, SyntaxGraphError> {
    build_import_node(
        args,
        n_id,
        &ImportElement {
            expression: Some(String::from("*")),
            ..ImportElement::default()
        },
    )
}

fn build_multiple_import_node(
    args: &mut SyntaxGraphArgs<'_>,
    n_id: NodeId,
    imported_elements: &[ImportElement],
    wildcards: &[NodeId],
) -> Result<NodeId, SyntaxGraphError> {
    args.syntax_graph.add_node(
        n_id,
        SyntaxNode::Import {
            expression: None,
            alias: None,
            method_name: None,
            import_type: Some(String::from("multiple_import")),
        },
    );

    for element in imported_elements {
        if let Some(corrected_n_id) = element.corrected_n_id {
            let built = build_import_node(args, corrected_n_id, element)?;
            args.syntax_graph.add_ast_edge(n_id, built);
        }
    }

    for child_n_id in wildcards {
        let built = build_wildcard_import_node(args, *child_n_id)?;
        args.syntax_graph.add_ast_edge(n_id, built);
    }

    Ok(n_id)
}

#[allow(
    dead_code,
    reason = "wired when a consuming language reader lands, #26623"
)]
pub fn build_import_statement_node(
    args: &mut SyntaxGraphArgs<'_>,
    n_id: NodeId,
    imported_elements: &[ImportElement],
    always_wrap: bool,
    wildcards: &[NodeId],
) -> Result<NodeId, SyntaxGraphError> {
    match imported_elements {
        [element] if !always_wrap && wildcards.is_empty() => build_import_node(args, n_id, element),
        _ => build_multiple_import_node(args, n_id, imported_elements, wildcards),
    }
}

#[cfg(test)]
mod tests {
    use super::{build_import_statement_node, ImportElement, ImportLabel};
    use crate::ast::AstGraph;
    use crate::syntax::{SyntaxGraph, SyntaxGraphArgs, SyntaxMetadata, SyntaxNode, SyntaxReader};
    use crate::{Language, NodeId};
    use alloc::string::String;
    use alloc::vec;

    fn no_dispatch(_: &str) -> Option<SyntaxReader> {
        None
    }

    #[test]
    fn a_single_element_is_added_directly_without_the_wrapper_node() {
        let ast = AstGraph::new();
        let mut graph = SyntaxGraph::new();
        let mut meta = SyntaxMetadata::seeded(NodeId(10));
        let elements = vec![ImportElement {
            expression: Some(String::from("os")),
            corrected_n_id: Some(NodeId(99)),
            ..ImportElement::default()
        }];

        let result = {
            let mut args =
                SyntaxGraphArgs::new(Language::Python, &ast, &mut graph, &mut meta, no_dispatch);
            build_import_statement_node(&mut args, NodeId(1), &elements, false, &[])
        };

        let built_id = result.expect("the builder must succeed");
        assert_eq!(built_id, NodeId(1));
        let Some(SyntaxNode::Import {
            expression,
            import_type,
            ..
        }) = graph.nodes.get(&NodeId(1))
        else {
            panic!("the builder must emit the expected node");
        };
        assert_eq!(expression.as_deref(), Some("os"));
        assert_eq!(*import_type, None);
        assert!(!graph.nodes.contains_key(&NodeId(99)));
    }

    #[test]
    fn multiple_elements_wrap_and_wire_only_the_corrected_ones() {
        let ast = AstGraph::new();
        let mut graph = SyntaxGraph::new();
        let mut meta = SyntaxMetadata::seeded(NodeId(10));
        let elements = vec![
            ImportElement {
                expression: Some(String::from("a")),
                corrected_n_id: Some(NodeId(2)),
                ..ImportElement::default()
            },
            ImportElement {
                expression: Some(String::from("b")),
                alias: Some(String::from("bee")),
                ..ImportElement::default()
            },
        ];

        let result = {
            let mut args =
                SyntaxGraphArgs::new(Language::Python, &ast, &mut graph, &mut meta, no_dispatch);
            build_import_statement_node(&mut args, NodeId(1), &elements, false, &[])
        };

        let built_id = result.expect("the builder must succeed");
        assert_eq!(built_id, NodeId(1));
        let Some(SyntaxNode::Import { import_type, .. }) = graph.nodes.get(&NodeId(1)) else {
            panic!("the builder must emit the expected node");
        };
        assert_eq!(import_type.as_deref(), Some("multiple_import"));

        let Some(SyntaxNode::Import {
            expression: child_expression,
            import_type: child_import_type,
            ..
        }) = graph.nodes.get(&NodeId(2))
        else {
            panic!("the corrected element must be wired as an Import node");
        };
        assert_eq!(child_expression.as_deref(), Some("a"));
        assert_eq!(*child_import_type, None);
        assert!(graph
            .edges
            .get(&NodeId(1))
            .is_some_and(|adjacent| adjacent.contains_key(&NodeId(2))));
        assert_eq!(graph.nodes.len(), 2);
    }

    #[test]
    fn an_empty_element_list_still_produces_the_wrapper_node() {
        let ast = AstGraph::new();
        let mut graph = SyntaxGraph::new();
        let mut meta = SyntaxMetadata::seeded(NodeId(10));

        let result = {
            let mut args =
                SyntaxGraphArgs::new(Language::Python, &ast, &mut graph, &mut meta, no_dispatch);
            build_import_statement_node(&mut args, NodeId(1), &[], false, &[])
        };

        let built_id = result.expect("the builder must succeed");
        assert_eq!(built_id, NodeId(1));
        assert_eq!(graph.nodes.len(), 1);
        assert!(!graph.edges.contains_key(&NodeId(1)));

        // The wrapper is what distinguishes this from the single-element path,
        // where the element's own node is used and import_type stays None.
        let Some(SyntaxNode::Import { import_type, .. }) = graph.nodes.get(&NodeId(1)) else {
            panic!("the builder must emit the wrapper Import node");
        };
        assert_eq!(import_type.as_deref(), Some("multiple_import"));
    }

    #[test]
    fn items_and_wildcards_become_ast_children_of_the_import() {
        let ast = AstGraph::new();
        let mut graph = SyntaxGraph::new();
        let mut meta = SyntaxMetadata::seeded(NodeId(10));
        let elements = vec![ImportElement {
            expression: Some(String::from("os")),
            alias: Some(String::from("operating_system")),
            corrected_n_id: Some(NodeId(2)),
            ..ImportElement::default()
        }];

        let result = {
            let mut args =
                SyntaxGraphArgs::new(Language::Python, &ast, &mut graph, &mut meta, no_dispatch);
            build_import_statement_node(&mut args, NodeId(1), &elements, false, &[NodeId(3)])
        };

        let built_id = result.expect("the builder must succeed");
        assert_eq!(built_id, NodeId(1));

        let Some(SyntaxNode::Import { import_type, .. }) = graph.nodes.get(&NodeId(1)) else {
            panic!("the builder must emit the expected node");
        };
        assert_eq!(import_type.as_deref(), Some("multiple_import"));

        assert!(graph.edges.get(&NodeId(1)).is_some_and(|adjacent| adjacent
            .contains_key(&NodeId(2))
            && adjacent.contains_key(&NodeId(3))));
        let Some(SyntaxNode::Import {
            expression, alias, ..
        }) = graph.nodes.get(&NodeId(2))
        else {
            panic!("the item must be wired as an Import node");
        };
        assert_eq!(expression.as_deref(), Some("os"));
        assert_eq!(alias.as_deref(), Some("operating_system"));
        let Some(SyntaxNode::Import {
            expression: wildcard_expression,
            ..
        }) = graph.nodes.get(&NodeId(3))
        else {
            panic!("the wildcard must be wired as an Import node");
        };
        assert_eq!(wildcard_expression.as_deref(), Some("*"));
    }

    #[test]
    fn always_wrap_keeps_the_wrapper_node_for_a_lone_element() {
        let ast = AstGraph::new();
        let mut graph = SyntaxGraph::new();
        let mut meta = SyntaxMetadata::seeded(NodeId(10));
        let elements = vec![ImportElement {
            expression: Some(String::from("os")),
            corrected_n_id: Some(NodeId(2)),
            ..ImportElement::default()
        }];

        let result = {
            let mut args =
                SyntaxGraphArgs::new(Language::Python, &ast, &mut graph, &mut meta, no_dispatch);
            build_import_statement_node(&mut args, NodeId(1), &elements, true, &[])
        };

        let built_id = result.expect("the builder must succeed");
        assert_eq!(built_id, NodeId(1));

        let Some(SyntaxNode::Import { import_type, .. }) = graph.nodes.get(&NodeId(1)) else {
            panic!("the builder must emit the wrapper Import node");
        };
        assert_eq!(import_type.as_deref(), Some("multiple_import"));

        let Some(SyntaxNode::Import { expression, .. }) = graph.nodes.get(&NodeId(2)) else {
            panic!("the element must be wired as its own Import node");
        };
        assert_eq!(expression.as_deref(), Some("os"));
    }

    #[test]
    fn a_module_import_element_emits_a_module_import_node() {
        let ast = AstGraph::new();
        let mut graph = SyntaxGraph::new();
        let mut meta = SyntaxMetadata::seeded(NodeId(10));
        let elements = vec![ImportElement {
            expression: Some(String::from("./module.thing")),
            alias: Some(String::from("thing")),
            label_type: ImportLabel::ModuleImport,
            ..ImportElement::default()
        }];

        let result = {
            let mut args = SyntaxGraphArgs::new(
                Language::JavaScript,
                &ast,
                &mut graph,
                &mut meta,
                no_dispatch,
            );
            build_import_statement_node(&mut args, NodeId(1), &elements, false, &[])
        };

        let built_id = result.expect("the builder must succeed");
        assert_eq!(built_id, NodeId(1));

        let Some(SyntaxNode::ModuleImport { expression, alias }) = graph.nodes.get(&NodeId(1))
        else {
            panic!("the builder must emit a ModuleImport node");
        };
        assert_eq!(expression, "./module.thing");
        assert_eq!(alias.as_deref(), Some("thing"));
    }
}