fluidattacks-blends-domain 0.16.1

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

use alloc::string::String;

use crate::syntax::builders::import_statement::{build_import_node, ImportElement, ImportLabel};
use crate::syntax::SyntaxNode;
use crate::syntax::{SyntaxGraphArgs, SyntaxGraphError};
use crate::NodeId;

#[allow(
    dead_code,
    reason = "wired when a consuming language reader lands, #26623"
)]
#[allow(
    clippy::too_many_arguments,
    reason = "mirrors the Python build_export_statement_node inputs"
)]
pub fn build_export_statement_node(
    args: &mut SyntaxGraphArgs<'_>,
    n_id: NodeId,
    expression: Option<String>,
    exported_block: Option<NodeId>,
    reexport_specifiers: &[(NodeId, String, Option<String>)],
    default_export: Option<NodeId>,
) -> Result<NodeId, SyntaxGraphError> {
    args.syntax_graph.add_node(
        n_id,
        SyntaxNode::Export {
            expression: expression.filter(|value| !value.is_empty()),
            declaration_id: exported_block,
        },
    );

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

    for (spec_n_id, spec_expression, spec_alias) in reexport_specifiers {
        build_import_node(
            args,
            *spec_n_id,
            &ImportElement {
                expression: Some(spec_expression.clone()),
                alias: spec_alias.clone(),
                label_type: ImportLabel::ModuleImport,
                ..ImportElement::default()
            },
        )?;
    }

    if let Some(default_keyword_nid) = default_export {
        args.syntax_graph
            .add_node(default_keyword_nid, SyntaxNode::Default);
    }

    Ok(n_id)
}

#[cfg(test)]
mod tests {
    use super::build_export_statement_node;
    use crate::ast::{AstGraph, AstNode};
    use crate::syntax::{
        SyntaxGraph, SyntaxGraphArgs, SyntaxGraphError, SyntaxMetadata, SyntaxNode, SyntaxReader,
    };
    use crate::{Language, NodeId};
    use alloc::borrow::ToOwned;
    use alloc::string::String;

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

    #[test]
    fn the_exported_block_reexports_and_default_keyword_are_all_wired() {
        let mut ast = AstGraph::new();
        ast.add_node(NodeId(2), AstNode::new(1, 1, "declaration".to_owned()));
        let mut graph = SyntaxGraph::new();
        let mut meta = SyntaxMetadata::seeded(NodeId(10));

        let result = {
            let mut args = SyntaxGraphArgs::new(
                Language::JavaScript,
                &ast,
                &mut graph,
                &mut meta,
                no_dispatch,
            );
            build_export_statement_node(
                &mut args,
                NodeId(1),
                Some(String::from("default")),
                Some(NodeId(2)),
                &[(NodeId(3), String::from("./module"), Some(String::from("m")))],
                Some(NodeId(4)),
            )
        };

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

        let Some(SyntaxNode::Export {
            expression,
            declaration_id,
        }) = graph.nodes.get(&NodeId(1))
        else {
            panic!("the builder must emit the expected node");
        };
        assert_eq!(expression.as_deref(), Some("default"));
        assert_eq!(*declaration_id, Some(NodeId(2)));

        assert!(graph
            .edges
            .get(&NodeId(1))
            .is_some_and(|adjacent| adjacent.contains_key(&NodeId(2))));
        let Some(SyntaxNode::ModuleImport {
            expression: reexport_expression,
            alias: reexport_alias,
        }) = graph.nodes.get(&NodeId(3))
        else {
            panic!("the reexport specifier must be wired as a ModuleImport node");
        };
        assert_eq!(reexport_expression, "./module");
        assert_eq!(reexport_alias.as_deref(), Some("m"));
        assert!(matches!(
            graph.nodes.get(&NodeId(4)),
            Some(SyntaxNode::Default)
        ));
    }

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

        let result = {
            let mut args = SyntaxGraphArgs::new(
                Language::JavaScript,
                &ast,
                &mut graph,
                &mut meta,
                no_dispatch,
            );
            build_export_statement_node(&mut args, NodeId(1), Some(String::new()), None, &[], None)
        };

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

        let Some(SyntaxNode::Export {
            expression,
            declaration_id,
        }) = graph.nodes.get(&NodeId(1))
        else {
            panic!("the builder must emit the expected node");
        };
        assert_eq!(*expression, None);
        assert_eq!(*declaration_id, None);
        assert!(!graph.nodes.contains_key(&NodeId(4)));
    }

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

        let result = {
            let mut args = SyntaxGraphArgs::new(
                Language::JavaScript,
                &ast,
                &mut graph,
                &mut meta,
                no_dispatch,
            );
            build_export_statement_node(&mut args, NodeId(1), None, Some(NodeId(2)), &[], None)
        };

        assert!(matches!(result, Err(SyntaxGraphError::MissingAstNode)));
    }
}