use alloc::string::String;
use alloc::vec::Vec;
use crate::syntax::SyntaxNode;
use crate::syntax::{SyntaxGraphArgs, SyntaxGraphError};
use crate::NodeId;
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct ExportStatement {
pub expression: Option<String>,
pub declaration_id: Option<NodeId>,
pub alias: Option<String>,
pub default_id: Option<NodeId>,
pub specifier_ids: Vec<NodeId>,
}
pub fn build_exported_name_node(
args: &mut SyntaxGraphArgs<'_>,
n_id: NodeId,
expression: String,
alias: Option<String>,
) -> NodeId {
args.syntax_graph.add_node(
n_id,
SyntaxNode::Export {
expression: Some(expression),
declaration_id: None,
alias: alias.filter(|value| !value.is_empty()),
default_id: None,
},
);
n_id
}
pub fn build_export_statement_node(
args: &mut SyntaxGraphArgs<'_>,
n_id: NodeId,
statement: ExportStatement,
) -> Result<NodeId, SyntaxGraphError> {
args.syntax_graph.add_node(
n_id,
SyntaxNode::Export {
expression: statement.expression.filter(|value| !value.is_empty()),
declaration_id: statement.declaration_id,
alias: statement.alias.filter(|value| !value.is_empty()),
default_id: statement.default_id,
},
);
if let Some(declaration_id) = statement.declaration_id {
let built = args.generic(declaration_id)?;
args.syntax_graph.add_ast_edge(n_id, built);
}
for spec_id in statement.specifier_ids {
let built = args.generic(spec_id)?;
args.syntax_graph.add_ast_edge(n_id, built);
}
if let Some(default_id) = statement.default_id {
args.syntax_graph.add_node(default_id, SyntaxNode::Default);
args.syntax_graph.add_ast_edge(n_id, default_id);
}
Ok(n_id)
}
#[cfg(test)]
mod tests {
use super::{build_export_statement_node, build_exported_name_node, ExportStatement};
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;
use alloc::vec;
const STATEMENT: NodeId = NodeId(1);
fn no_dispatch(_: &str) -> Option<SyntaxReader> {
None
}
#[expect(
clippy::unnecessary_wraps,
reason = "the signature must match the reader dispatch table"
)]
fn name_reader(
args: &mut SyntaxGraphArgs<'_>,
n_id: NodeId,
) -> Result<Option<NodeId>, SyntaxGraphError> {
Ok(Some(build_exported_name_node(
args,
n_id,
String::from("./module.m"),
Some(String::from("m")),
)))
}
#[expect(
clippy::unnecessary_wraps,
reason = "the signature must match the dispatch lookup"
)]
fn dispatch_names(_: &str) -> Option<SyntaxReader> {
Some(name_reader)
}
fn build(
ast: &AstGraph,
dispatch: fn(&str) -> Option<SyntaxReader>,
statement: ExportStatement,
) -> (Result<NodeId, SyntaxGraphError>, SyntaxGraph) {
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, dispatch);
build_export_statement_node(&mut args, STATEMENT, statement)
};
(result, graph)
}
fn has_ast_edge(graph: &SyntaxGraph, to: NodeId) -> bool {
graph
.edges
.get(&STATEMENT)
.is_some_and(|adjacent| adjacent.contains_key(&to))
}
#[test]
fn the_declaration_specifiers_and_default_keyword_are_all_wired() {
let mut ast = AstGraph::new();
ast.add_node(NodeId(2), AstNode::new(1, 1, "declaration".to_owned()));
ast.add_node(NodeId(3), AstNode::new(1, 1, "export_specifier".to_owned()));
let (result, graph) = build(
&ast,
dispatch_names,
ExportStatement {
expression: Some(String::from("./module")),
declaration_id: Some(NodeId(2)),
default_id: Some(NodeId(4)),
specifier_ids: vec![NodeId(3)],
..ExportStatement::default()
},
);
assert_eq!(result.expect("the builder must succeed"), STATEMENT);
assert_eq!(
graph.nodes.get(&STATEMENT),
Some(&SyntaxNode::Export {
expression: Some(String::from("./module")),
declaration_id: Some(NodeId(2)),
alias: None,
default_id: Some(NodeId(4)),
})
);
assert!(has_ast_edge(&graph, NodeId(2)));
assert_eq!(
graph.nodes.get(&NodeId(3)),
Some(&SyntaxNode::Export {
expression: Some(String::from("./module.m")),
declaration_id: None,
alias: Some(String::from("m")),
default_id: None,
})
);
assert!(has_ast_edge(&graph, NodeId(3)));
assert_eq!(graph.nodes.get(&NodeId(4)), Some(&SyntaxNode::Default));
assert!(has_ast_edge(&graph, NodeId(4)));
assert!(graph.symbol_index.is_empty());
}
#[test]
fn an_exported_name_keeps_its_expression_and_drops_an_empty_alias() {
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_exported_name_node(
&mut args,
NodeId(5),
String::from("handler"),
Some(String::new()),
)
};
assert_eq!(result, NodeId(5));
assert_eq!(
graph.nodes.get(&NodeId(5)),
Some(&SyntaxNode::Export {
expression: Some(String::from("handler")),
declaration_id: None,
alias: None,
default_id: None,
})
);
}
#[test]
fn empty_strings_are_normalized_to_none_and_optional_fields_are_skipped() {
let (result, graph) = build(
&AstGraph::new(),
no_dispatch,
ExportStatement {
expression: Some(String::new()),
alias: Some(String::new()),
..ExportStatement::default()
},
);
assert_eq!(result.expect("the builder must succeed"), STATEMENT);
assert_eq!(
graph.nodes.get(&STATEMENT),
Some(&SyntaxNode::Export {
expression: None,
declaration_id: None,
alias: None,
default_id: None,
})
);
assert_eq!(graph.nodes.len(), 1);
}
#[test]
fn missing_declaration_ast_node_propagates_the_error() {
let (result, _) = build(
&AstGraph::new(),
no_dispatch,
ExportStatement {
declaration_id: Some(NodeId(2)),
..ExportStatement::default()
},
);
assert!(matches!(result, Err(SyntaxGraphError::MissingAstNode)));
}
}