diff --git a/crates/oxc_angular_compiler/src/ir/enums.rs b/crates/oxc_angular_compiler/src/ir/enums.rs index ca3f80db3..84aeae0fa 100644 --- a/crates/oxc_angular_compiler/src/ir/enums.rs +++ b/crates/oxc_angular_compiler/src/ir/enums.rs @@ -260,6 +260,10 @@ pub enum ExpressionKind { Void, /// Template literal with resolved expressions (used after name resolution). ResolvedTemplateLiteral, + /// Tagged template literal with resolved tag and template expressions. + TaggedTemplateLiteral, + /// Spread element (...expr) inside a call or other expression list. + SpreadElement, /// Arrow function expression. ArrowFunction, /// Parenthesized expression. diff --git a/crates/oxc_angular_compiler/src/ir/expression.rs b/crates/oxc_angular_compiler/src/ir/expression.rs index 27bcb02b4..e20b82741 100644 --- a/crates/oxc_angular_compiler/src/ir/expression.rs +++ b/crates/oxc_angular_compiler/src/ir/expression.rs @@ -191,6 +191,12 @@ pub enum IrExpression<'a> { /// Used when a template literal like `\`bwi ${menuItem.icon}\`` has expressions /// that need to be resolved to variables in scope. ResolvedTemplateLiteral(Box<'a, ResolvedTemplateLiteralExpr<'a>>), + /// Tagged template literal (tag`...`) with IR tag and template expressions. + /// Used to preserve pipes and arrow functions inside tagged templates during ingest. + TaggedTemplateLiteral(Box<'a, IrTaggedTemplateLiteralExpr<'a>>), + /// Spread element (...expr) with an IR inner expression. + /// Used to preserve pipes and arrow functions inside spread positions during ingest. + SpreadElement(Box<'a, IrSpreadElementExpr<'a>>), /// Arrow function expression. /// Created by the generateArrowFunctions phase to wrap user-defined arrow functions. ArrowFunction(Box<'a, ArrowFunctionExpr<'a>>), @@ -267,6 +273,10 @@ impl<'a> IrExpression<'a> { IrExpression::Void(_) => ExpressionKind::Void, // ResolvedTemplateLiteral is a template literal with resolved expressions IrExpression::ResolvedTemplateLiteral(_) => ExpressionKind::ResolvedTemplateLiteral, + // TaggedTemplateLiteral is a tagged template literal with IR sub-expressions + IrExpression::TaggedTemplateLiteral(_) => ExpressionKind::TaggedTemplateLiteral, + // SpreadElement is a spread element with an IR inner expression + IrExpression::SpreadElement(_) => ExpressionKind::SpreadElement, // ArrowFunction is an arrow function expression IrExpression::ArrowFunction(_) => ExpressionKind::ArrowFunction, IrExpression::Parenthesized(_) => ExpressionKind::Parenthesized, @@ -780,6 +790,35 @@ impl<'a> IrExpression<'a> { &allocator, )) } + IrExpression::TaggedTemplateLiteral(e) => { + let mut elements = Vec::with_capacity_in(e.elements.len(), &allocator); + for elem in e.elements.iter() { + elements.push(IrTemplateLiteralElement { + text: elem.text.clone(), + source_span: elem.source_span, + }); + } + let mut expressions = Vec::with_capacity_in(e.expressions.len(), &allocator); + for expr in e.expressions.iter() { + expressions.push(expr.clone_in(allocator)); + } + IrExpression::TaggedTemplateLiteral(Box::new_in( + IrTaggedTemplateLiteralExpr { + tag: Box::new_in(e.tag.clone_in(allocator), &allocator), + elements, + expressions, + source_span: e.source_span, + }, + &allocator, + )) + } + IrExpression::SpreadElement(e) => IrExpression::SpreadElement(Box::new_in( + IrSpreadElementExpr { + expr: Box::new_in(e.expr.clone_in(allocator), &allocator), + source_span: e.source_span, + }, + &allocator, + )), IrExpression::ArrowFunction(e) => { let mut params = Vec::with_capacity_in(e.params.len(), &allocator); for param in e.params.iter() { @@ -1073,6 +1112,34 @@ pub struct IrTemplateLiteralElement<'a> { pub source_span: Option, } +/// Tagged template literal (tag`...`) with IR sub-expressions. +/// +/// Used during ingest to preserve pipes and arrow functions inside the tag and +/// the template's interpolated expressions. +#[derive(Debug)] +pub struct IrTaggedTemplateLiteralExpr<'a> { + /// The tag function expression. + pub tag: Box<'a, IrExpression<'a>>, + /// Template literal text elements (the static parts between expressions). + pub elements: Vec<'a, IrTemplateLiteralElement<'a>>, + /// The dynamic parts inside ${...}. + pub expressions: Vec<'a, IrExpression<'a>>, + /// Source span. + pub source_span: Option, +} + +/// Spread element (...expr) with an IR inner expression. +/// +/// Used during ingest to preserve pipes and arrow functions inside spread +/// positions (e.g. call arguments). +#[derive(Debug)] +pub struct IrSpreadElementExpr<'a> { + /// The expression being spread. + pub expr: Box<'a, IrExpression<'a>>, + /// Source span. + pub source_span: Option, +} + /// Derived literal array for pure function bodies. /// This is used when a literal array contains non-constant entries that need /// to be replaced with PureFunctionParameter references. @@ -1714,11 +1781,29 @@ pub fn transform_expressions_in_expression<'a, F>( transform_expressions_in_expression(expr, transform, flags); } } + IrExpression::TaggedTemplateLiteral(e) => { + transform_expressions_in_expression(&mut e.tag, transform, flags); + for expr in e.expressions.iter_mut() { + transform_expressions_in_expression(expr, transform, flags); + } + } + IrExpression::SpreadElement(e) => { + transform_expressions_in_expression(&mut e.expr, transform, flags); + } IrExpression::ArrowFunction(e) => { - // Transform body with InChildOperation and InArrowFunctionOperation flags set - let child_flags = flags - .union(VisitorContextFlag::IN_CHILD_OPERATION) - .union(VisitorContextFlag::IN_ARROW_FUNCTION_OPERATION); + // Mirror ir.ArrowFunctionExpr.transformInternalExpressions: ops are + // transformed as child operations inside an arrow function. The + // InArrowFunctionOperation flag only applies to top-level (collected) + // arrows; arrows preserved in place inside child operations (listener + // handlers, nested arrows) get only InChildOperation, matching the + // o.ArrowFunctionExpr branch of transformExpressionsInExpression. + let mut child_flags = flags.union(VisitorContextFlag::IN_CHILD_OPERATION); + if !flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) { + child_flags = child_flags.union(VisitorContextFlag::IN_ARROW_FUNCTION_OPERATION); + } + for op in e.ops.iter_mut() { + transform_expressions_in_update_op(op, transform, child_flags); + } transform_expressions_in_expression(&mut e.body, transform, child_flags); } IrExpression::Parenthesized(e) => { @@ -1901,11 +1986,25 @@ pub fn visit_expressions_in_expression<'a, F>( visit_expressions_in_expression(expr, visitor, flags); } } + IrExpression::TaggedTemplateLiteral(e) => { + visit_expressions_in_expression(&e.tag, visitor, flags); + for expr in e.expressions.iter() { + visit_expressions_in_expression(expr, visitor, flags); + } + } + IrExpression::SpreadElement(e) => { + visit_expressions_in_expression(&e.expr, visitor, flags); + } IrExpression::ArrowFunction(e) => { - // Visit body with InChildOperation and InArrowFunctionOperation flags set - let child_flags = flags - .union(VisitorContextFlag::IN_CHILD_OPERATION) - .union(VisitorContextFlag::IN_ARROW_FUNCTION_OPERATION); + // See transform_expressions_in_expression for the flag rationale: + // InArrowFunctionOperation applies only to top-level (collected) arrows. + let mut child_flags = flags.union(VisitorContextFlag::IN_CHILD_OPERATION); + if !flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) { + child_flags = child_flags.union(VisitorContextFlag::IN_ARROW_FUNCTION_OPERATION); + } + for op in e.ops.iter() { + visit_expressions_in_update_op(op, visitor, child_flags); + } visit_expressions_in_expression(&e.body, visitor, child_flags); } IrExpression::Parenthesized(e) => { @@ -3003,6 +3102,15 @@ pub fn vars_used_by_ir_expression(expr: &IrExpression<'_>) -> u32 { rtl.expressions.iter().map(vars_used_by_ir_expression).sum() } + // TaggedTemplateLiteral: vars used by tag and template expressions + IrExpression::TaggedTemplateLiteral(ttl) => { + vars_used_by_ir_expression(&ttl.tag) + + ttl.expressions.iter().map(vars_used_by_ir_expression).sum::() + } + + // SpreadElement: vars used by the inner expression + IrExpression::SpreadElement(spread) => vars_used_by_ir_expression(&spread.expr), + // All other expressions don't directly consume variable slots IrExpression::LexicalRead(_) | IrExpression::Reference(_) diff --git a/crates/oxc_angular_compiler/src/pipeline/compilation.rs b/crates/oxc_angular_compiler/src/pipeline/compilation.rs index 3717b6e14..8d5074c0b 100644 --- a/crates/oxc_angular_compiler/src/pipeline/compilation.rs +++ b/crates/oxc_angular_compiler/src/pipeline/compilation.rs @@ -806,6 +806,11 @@ pub struct HostBindingCompilationUnit<'a> { pub vars: Option, /// Generated function name. pub fn_name: Option>, + /// Arrow functions found in this unit. + /// + /// Populated by the generateArrowFunctions phase. Only non-listener, + /// top-level arrows are collected, matching upstream `unit.functions`. + pub functions: Vec<'a, *mut crate::ir::expression::ArrowFunctionExpr<'a>>, } impl<'a> HostBindingCompilationUnit<'a> { @@ -820,6 +825,7 @@ impl<'a> HostBindingCompilationUnit<'a> { attributes: None, vars: None, fn_name: None, + functions: Vec::new_in(&allocator), } } } diff --git a/crates/oxc_angular_compiler/src/pipeline/emit.rs b/crates/oxc_angular_compiler/src/pipeline/emit.rs index 2907b3438..516b28e44 100644 --- a/crates/oxc_angular_compiler/src/pipeline/emit.rs +++ b/crates/oxc_angular_compiler/src/pipeline/emit.rs @@ -1320,6 +1320,50 @@ fn convert_pure_function_body<'a>( &allocator, )) } + // TaggedTemplateLiteral: convert tag and template expressions + IrExpression::TaggedTemplateLiteral(ttl) => { + let tag = convert_pure_function_body(allocator, &ttl.tag, params); + let mut elements = OxcVec::new_in(&allocator); + let mut expressions = OxcVec::new_in(&allocator); + for elem in ttl.elements.iter() { + elements.push(crate::output::ast::TemplateLiteralElement { + text: elem.text.clone(), + raw_text: elem.text.clone(), + source_span: elem.source_span, + }); + } + for expr in ttl.expressions.iter() { + expressions.push(convert_pure_function_body(allocator, expr, params)); + } + OutputExpression::TaggedTemplateLiteral(Box::new_in( + crate::output::ast::TaggedTemplateLiteralExpr { + tag: Box::new_in(tag, &allocator), + template: Box::new_in( + crate::output::ast::TemplateLiteralExpr { + elements, + expressions, + source_span: ttl.source_span, + }, + &allocator, + ), + source_span: ttl.source_span, + }, + &allocator, + )) + } + + // SpreadElement: convert the inner expression + IrExpression::SpreadElement(spread) => { + let inner = convert_pure_function_body(allocator, &spread.expr, params); + OutputExpression::SpreadElement(Box::new_in( + crate::output::ast::SpreadElementExpr { + expr: Box::new_in(inner, &allocator), + source_span: spread.source_span, + }, + &allocator, + )) + } + // Parenthesized expression - convert inner and wrap IrExpression::Parenthesized(paren) => { let inner = convert_pure_function_body(allocator, &paren.expr, params); diff --git a/crates/oxc_angular_compiler/src/pipeline/ingest.rs b/crates/oxc_angular_compiler/src/pipeline/ingest.rs index 10d95cc2b..a84b53950 100644 --- a/crates/oxc_angular_compiler/src/pipeline/ingest.rs +++ b/crates/oxc_angular_compiler/src/pipeline/ingest.rs @@ -37,7 +37,7 @@ use crate::ir::expression::{ BinaryExpr, ConditionalCaseExpr, EmptyExpr, IrBinaryOperator, IrExpression, LexicalReadExpr, PipeBindingExpr, ResolvedCallExpr, ResolvedKeyedReadExpr, ResolvedPropertyReadExpr, SafeInvokeFunctionExpr, SafeKeyedReadExpr, SafePropertyReadExpr, SlotHandle, - TwoWayBindingSetExpr, + TwoWayBindingSetExpr, VisitorContextFlag, transform_expressions_in_expression, }; use crate::ir::ops::{ BindingOp, ConditionalBranchCreateOp, ConditionalOp, ConditionalUpdateOp, CreateOp, @@ -47,9 +47,10 @@ use crate::ir::ops::{ RepeaterCreateOp, RepeaterOp, RepeaterVarNames, SlotId, StatementOp, StoreLetOp, TemplateOp, TextOp, TwoWayListenerOp, UpdateOp, UpdateOpBase, XrefId, }; -use crate::output::ast::OutputExpression; +use crate::output::ast::{OutputExpression, ReadVarExpr}; use crate::pipeline::compilation::{AliasVariable, ContextVariable}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::cell::RefCell; /// Options for ingesting a component template. /// @@ -630,11 +631,118 @@ fn convert_ast_to_ir<'a>( ) } + // Convert ArrowFunction - recursively convert the body so the names it reads are + // resolved, then keep reads of the arrow's own parameters as plain variables. + AngularExpression::ArrowFunction(arrow) => { + let arrow = arrow.unbox(); + let source_span = Some(arrow.source_span.to_span()); + let mut params = Vec::with_capacity_in(arrow.parameters.len(), &allocator); + for param in &arrow.parameters { + params.push(crate::output::ast::FnParam { name: param.name }); + } + let body = convert_ast_to_ir(job, arrow.body); + let mut expr = IrExpression::ArrowFunction(Box::new_in( + crate::ir::expression::ArrowFunctionExpr { + params, + body, + ops: Vec::new_in(&allocator), + var_offset: None, + source_span, + }, + &allocator, + )); + update_parameter_references(&mut expr, allocator); + Box::new_in(expr, &allocator) + } + + // Convert TaggedTemplateLiteral - recursively convert the tag and template + // expressions to preserve pipes and arrow functions (ingest.ts convertAst). + AngularExpression::TaggedTemplateLiteral(ttl) => { + let ttl = ttl.unbox(); + let tag = convert_ast_to_ir(job, ttl.tag); + let mut elements = Vec::with_capacity_in(ttl.template.elements.len(), &allocator); + for elem in ttl.template.elements.iter() { + elements.push(crate::ir::expression::IrTemplateLiteralElement { + text: elem.text.clone(), + source_span: Some(elem.source_span.to_span()), + }); + } + let mut expressions = Vec::with_capacity_in(ttl.template.expressions.len(), &allocator); + for expr in ttl.template.expressions { + let converted = convert_ast_to_ir(job, expr); + expressions.push(converted.unbox()); + } + Box::new_in( + IrExpression::TaggedTemplateLiteral(Box::new_in( + crate::ir::expression::IrTaggedTemplateLiteralExpr { + tag, + elements, + expressions, + source_span: Some(ttl.source_span.to_span()), + }, + &allocator, + )), + &allocator, + ) + } + + // Convert SpreadElement - recursively convert the inner expression so arrows + // and pipes inside spread positions (e.g. call arguments) are preserved + // (ingest.ts convertAst produces o.SpreadElementExpr). + AngularExpression::SpreadElement(spread) => { + let spread = spread.unbox(); + let expr = convert_ast_to_ir(job, spread.expression); + Box::new_in( + IrExpression::SpreadElement(Box::new_in( + crate::ir::expression::IrSpreadElementExpr { + expr, + source_span: Some(spread.source_span.to_span()), + }, + &allocator, + )), + &allocator, + ) + } + // For all other expressions, store in ExpressionStore and return reference. other => store_and_ref_expr(job, other), } } +/// Replaces reads of an arrow function's parameters with plain variable reads, so the +/// name resolution phases leave them alone instead of resolving them against template +/// variables or the component context. +/// +/// Like Angular, the parameter set only grows: parameters of a nested arrow are added +/// when that arrow is visited (after its body), and are not removed afterwards. +/// +/// Ported from Angular's `updateParameterReferences` in `ingest.ts`. +fn update_parameter_references<'a>(root: &mut IrExpression<'a>, allocator: &'a Allocator) { + let parameter_names: RefCell>> = RefCell::new(FxHashSet::default()); + if let IrExpression::ArrowFunction(arrow) = &*root { + parameter_names.borrow_mut().extend(arrow.params.iter().map(|p| p.name)); + } + transform_expressions_in_expression( + root, + &|expr, _flags| match expr { + IrExpression::ArrowFunction(arrow) => { + parameter_names.borrow_mut().extend(arrow.params.iter().map(|p| p.name)); + } + IrExpression::LexicalRead(read) if parameter_names.borrow().contains(&read.name) => { + *expr = IrExpression::OutputExpr(Box::new_in( + OutputExpression::ReadVar(Box::new_in( + ReadVarExpr { name: read.name, source_span: read.source_span }, + &allocator, + )), + &allocator, + )); + } + _ => {} + }, + VisitorContextFlag::NONE, + ); +} + /// Converts an AST binary operator to an IR binary operator. fn convert_binary_op( op: crate::ast::expression::BinaryOperator, @@ -3844,6 +3952,192 @@ fn host_convert_ast_to_ir<'a>( ) } + // Convert LiteralArray - recursively convert elements to preserve pipes. + // Spread elements (e.g. [...base, item]) are preserved via the spreads parallel vec. + AngularExpression::LiteralArray(arr) => { + let arr = arr.unbox(); + let mut elements = Vec::with_capacity_in(arr.expressions.len(), &allocator); + let mut spreads = Vec::with_capacity_in(arr.expressions.len(), &allocator); + for elem in arr.expressions { + let is_spread = matches!(elem, AngularExpression::SpreadElement(_)); + let inner = if let AngularExpression::SpreadElement(s) = elem { + host_convert_ast_to_ir(job, s.unbox().expression) + } else { + host_convert_ast_to_ir(job, elem) + }; + elements.push(inner.unbox()); + spreads.push(is_spread); + } + Box::new_in( + IrExpression::LiteralArray(Box::new_in( + crate::ir::expression::IrLiteralArrayExpr { + elements, + spreads, + source_span: Some(arr.source_span.to_span()), + }, + &allocator, + )), + &allocator, + ) + } + + // Convert LiteralMap (object literal) - recursively convert values to preserve pipes. + // Spread entries (e.g. { ...base, key: val }) are preserved: spread keys get a dummy + // empty Ident with spreads[i] = true so later phases can emit them correctly. + AngularExpression::LiteralMap(map) => { + use crate::ast::expression::LiteralMapKey; + let map = map.unbox(); + let mut keys = Vec::with_capacity_in(map.keys.len(), &allocator); + let mut values = Vec::with_capacity_in(map.values.len(), &allocator); + let mut quoted = Vec::with_capacity_in(map.keys.len(), &allocator); + let mut spreads = Vec::with_capacity_in(map.keys.len(), &allocator); + + for (key, value) in map.keys.into_iter().zip(map.values.into_iter()) { + match key { + LiteralMapKey::Property(prop) => { + keys.push(prop.key); + quoted.push(prop.quoted); + spreads.push(false); + } + LiteralMapKey::Spread(_) => { + keys.push(Ident::from("")); + quoted.push(false); + spreads.push(true); + } + } + let value_expr = host_convert_ast_to_ir(job, value); + values.push(value_expr.unbox()); + } + + Box::new_in( + IrExpression::LiteralMap(Box::new_in( + crate::ir::expression::IrLiteralMapExpr { + keys, + values, + quoted, + spreads, + source_span: Some(map.source_span.to_span()), + }, + &allocator, + )), + &allocator, + ) + } + + // Convert TemplateLiteral - recursively convert inner expressions to preserve pipes + // and arrow functions, matching the component-side converter. + AngularExpression::TemplateLiteral(tl) => { + let tl = tl.unbox(); + let mut elements = Vec::with_capacity_in(tl.elements.len(), &allocator); + for elem in tl.elements.iter() { + elements.push(crate::ir::expression::IrTemplateLiteralElement { + text: elem.text.clone(), + source_span: Some(elem.source_span.to_span()), + }); + } + let mut expressions = Vec::with_capacity_in(tl.expressions.len(), &allocator); + for expr in tl.expressions { + let converted = host_convert_ast_to_ir(job, expr); + expressions.push(converted.unbox()); + } + Box::new_in( + IrExpression::ResolvedTemplateLiteral(Box::new_in( + crate::ir::expression::ResolvedTemplateLiteralExpr { + elements, + expressions, + source_span: Some(tl.source_span.to_span()), + }, + &allocator, + )), + &allocator, + ) + } + + // Empty expression - convert directly to IrExpression::Empty, matching the + // component-side converter. + AngularExpression::Empty(empty) => { + let empty = empty.unbox(); + Box::new_in( + IrExpression::Empty(Box::new_in( + EmptyExpr { source_span: Some(empty.source_span.to_span()) }, + &allocator, + )), + &allocator, + ) + } + + // Convert TaggedTemplateLiteral - recursively convert the tag and template + // expressions to preserve pipes and arrow functions (ingest.ts convertAst). + AngularExpression::TaggedTemplateLiteral(ttl) => { + let ttl = ttl.unbox(); + let tag = host_convert_ast_to_ir(job, ttl.tag); + let mut elements = Vec::with_capacity_in(ttl.template.elements.len(), &allocator); + for elem in ttl.template.elements.iter() { + elements.push(crate::ir::expression::IrTemplateLiteralElement { + text: elem.text.clone(), + source_span: Some(elem.source_span.to_span()), + }); + } + let mut expressions = Vec::with_capacity_in(ttl.template.expressions.len(), &allocator); + for expr in ttl.template.expressions { + let converted = host_convert_ast_to_ir(job, expr); + expressions.push(converted.unbox()); + } + Box::new_in( + IrExpression::TaggedTemplateLiteral(Box::new_in( + crate::ir::expression::IrTaggedTemplateLiteralExpr { + tag, + elements, + expressions, + source_span: Some(ttl.source_span.to_span()), + }, + &allocator, + )), + &allocator, + ) + } + + // Convert SpreadElement - recursively convert the inner expression so arrows + // and pipes inside spread positions (e.g. call arguments) are preserved. + AngularExpression::SpreadElement(spread) => { + let spread = spread.unbox(); + let expr = host_convert_ast_to_ir(job, spread.expression); + Box::new_in( + IrExpression::SpreadElement(Box::new_in( + crate::ir::expression::IrSpreadElementExpr { + expr, + source_span: Some(spread.source_span.to_span()), + }, + &allocator, + )), + &allocator, + ) + } + + // Convert ArrowFunction - recursively convert the body so the names it reads are + // resolved, then keep reads of the arrow's own parameters as plain variables. + AngularExpression::ArrowFunction(arrow) => { + let arrow = arrow.unbox(); + let source_span = Some(arrow.source_span.to_span()); + let mut params = Vec::with_capacity_in(arrow.parameters.len(), &allocator); + for param in &arrow.parameters { + params.push(crate::output::ast::FnParam { name: param.name }); + } + let body = host_convert_ast_to_ir(job, arrow.body); + let mut expr = IrExpression::ArrowFunction(Box::new_in( + crate::ir::expression::ArrowFunctionExpr { + params, + body, + ops: Vec::new_in(&allocator), + var_offset: None, + source_span, + }, + &allocator, + )); + update_parameter_references(&mut expr, allocator); + Box::new_in(expr, &allocator) + } + // For all other expressions, store in ExpressionStore and return reference other => host_store_and_ref_expr(job, other), } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/generate_advance.rs b/crates/oxc_angular_compiler/src/pipeline/phases/generate_advance.rs index 3c47eaf0b..158f0e3b7 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/generate_advance.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/generate_advance.rs @@ -434,6 +434,18 @@ fn get_slot_dependency_from_ir_expr(expr: &IrExpression<'_>) -> Option { } None } + IrExpression::TaggedTemplateLiteral(ttl) => { + if let Some(target) = get_slot_dependency_from_ir_expr(&ttl.tag) { + return Some(target); + } + for expr in ttl.expressions.iter() { + if let Some(target) = get_slot_dependency_from_ir_expr(expr) { + return Some(target); + } + } + None + } + IrExpression::SpreadElement(spread) => get_slot_dependency_from_ir_expr(&spread.expr), // Expressions that don't contain slot dependencies IrExpression::LexicalRead(_) | IrExpression::Context(_) diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/generate_arrow_functions.rs b/crates/oxc_angular_compiler/src/pipeline/phases/generate_arrow_functions.rs index 0744db68d..2c1db128e 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/generate_arrow_functions.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/generate_arrow_functions.rs @@ -173,28 +173,65 @@ pub fn generate_arrow_functions_for_host(job: &mut HostBindingCompilationJob<'_> VisitorContextFlag::NONE, ); } + + // Collect arrow functions into the root unit's functions set + let ptrs = collect_arrow_function_ptrs(&job.root.create, &job.root.update); + for ptr in ptrs { + // SAFETY: see collect_arrow_functions_from_view. + unsafe { (*ptr).var_offset = Some(u32::MAX) }; + job.root.functions.push(ptr); + } } /// Collect arrow functions from a view's operations into its functions set. +/// +/// Only top-level arrows are collected: arrows inside child operations (listener +/// handlers, nested arrow bodies) are skipped, matching upstream where +/// `unit.functions` only holds `ir.ArrowFunctionExpr`s created by +/// `addArrowFunctions` for non-listener, non-nested positions. fn collect_arrow_functions_from_view( view: &mut crate::pipeline::compilation::ViewCompilationUnit<'_>, ) { // Clear existing functions view.functions.clear(); + let ptrs = collect_arrow_function_ptrs(&view.create, &view.update); + for ptr in ptrs { + // SAFETY: The pointer targets an ArrowFunctionExpr allocated in this + // job's allocator, which outlives the pipeline. + unsafe { (*ptr).var_offset = Some(u32::MAX) }; + view.functions.push(ptr); + } +} + +/// Collect arrow function pointers from create and update op lists. +/// +/// Only top-level arrows are collected: arrows inside child operations (listener +/// handlers, nested arrow bodies) are skipped, matching upstream where +/// `unit.functions` only holds `ir.ArrowFunctionExpr`s created by +/// `addArrowFunctions` for non-listener, non-nested positions. Collected arrows +/// are marked with a sentinel var_offset (u32::MAX) so var_counting can tell +/// them apart from arrows preserved in place. +fn collect_arrow_function_ptrs<'a>( + create: &crate::ir::list::CreateOpList<'a>, + update: &crate::ir::list::UpdateOpList<'a>, +) -> std::vec::Vec<*mut ArrowFunctionExpr<'a>> { // We use RefCell to allow mutable access from within the visitor closure use std::cell::RefCell; - let collected: RefCell>> = + let collected: RefCell>> = RefCell::new(std::vec::Vec::new()); // Collect from create ops - for op in view.create.iter() { + for op in create.iter() { crate::ir::expression::visit_expressions_in_create_op( op, - &|expr, _flags| { + &|expr, flags| { + if flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) { + return; + } if let IrExpression::ArrowFunction(arrow_fn) = expr { - let ptr = arrow_fn.as_ref() as *const ArrowFunctionExpr<'_> - as *mut ArrowFunctionExpr<'_>; + let ptr = arrow_fn.as_ref() as *const ArrowFunctionExpr<'a> + as *mut ArrowFunctionExpr<'a>; collected.borrow_mut().push(ptr); } }, @@ -203,13 +240,16 @@ fn collect_arrow_functions_from_view( } // Collect from update ops - for op in view.update.iter() { + for op in update.iter() { crate::ir::expression::visit_expressions_in_update_op( op, - &|expr, _flags| { + &|expr, flags| { + if flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) { + return; + } if let IrExpression::ArrowFunction(arrow_fn) = expr { - let ptr = arrow_fn.as_ref() as *const ArrowFunctionExpr<'_> - as *mut ArrowFunctionExpr<'_>; + let ptr = arrow_fn.as_ref() as *const ArrowFunctionExpr<'a> + as *mut ArrowFunctionExpr<'a>; collected.borrow_mut().push(ptr); } }, @@ -217,9 +257,5 @@ fn collect_arrow_functions_from_view( ); } - // Move collected pointers into the allocator Vec - let ptrs = collected.into_inner(); - for ptr in ptrs { - view.functions.push(ptr); - } + collected.into_inner() } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/next_context_merging.rs b/crates/oxc_angular_compiler/src/pipeline/phases/next_context_merging.rs index 05cefd2b2..473dff783 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/next_context_merging.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/next_context_merging.rs @@ -503,6 +503,26 @@ fn visit_nested_exprs_for_merge(expr: &mut IrExpression<'_>, merge_steps: u32) - MergeResult::Continue } + // TaggedTemplateLiteral: recurse into tag and embedded expressions + IrExpression::TaggedTemplateLiteral(ttl) => { + let result = visit_ir_expr_for_merge(&mut ttl.tag, merge_steps); + if result != MergeResult::Continue { + return result; + } + for e in ttl.expressions.iter_mut() { + let result = visit_ir_expr_for_merge(e, merge_steps); + if result != MergeResult::Continue { + return result; + } + } + MergeResult::Continue + } + + // SpreadElement: recurse into the inner expression + IrExpression::SpreadElement(spread) => { + visit_ir_expr_for_merge(&mut spread.expr, merge_steps) + } + // Leaf expressions with no sub-expressions - no need to recurse IrExpression::LexicalRead(_) | IrExpression::Context(_) diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/ir_expression.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/ir_expression.rs index 8b0c603ce..a336806d1 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/ir_expression.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/ir_expression.rs @@ -7,9 +7,10 @@ use crate::ast::expression::AngularExpression; use crate::ir::expression::{IrExpression, TwoWayBindingSetExpr}; use crate::ir::ops::XrefId; use crate::output::ast::{ - BinaryOperator, BinaryOperatorExpr, ConditionalExpr, InvokeFunctionExpr, LiteralArrayExpr, - LiteralExpr, LiteralMapEntry, LiteralMapExpr, LiteralValue, OutputExpression, - ParenthesizedExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, SpreadElementExpr, + ArrowFunctionBody, ArrowFunctionExpr, BinaryOperator, BinaryOperatorExpr, ConditionalExpr, + FnParam, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, LiteralMapEntry, LiteralMapExpr, + LiteralValue, OutputExpression, ParenthesizedExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, + SpreadElementExpr, }; use crate::pipeline::expression_store::ExpressionStore; use crate::r3::{Identifiers, get_pipe_bind_instruction, get_pure_function_instruction}; @@ -1161,6 +1162,63 @@ pub fn convert_ir_expression<'a>( )) } + // Tagged template literal: convert tag and template expressions + IrExpression::TaggedTemplateLiteral(ttl) => { + let tag = + convert_ir_expression(allocator, core_namespace, &ttl.tag, expressions, root_xref); + let mut elements = OxcVec::new_in(&allocator); + let mut output_expressions = OxcVec::new_in(&allocator); + for elem in ttl.elements.iter() { + elements.push(crate::output::ast::TemplateLiteralElement { + text: elem.text.clone(), + raw_text: elem.text.clone(), + source_span: elem.source_span, + }); + } + for expr in ttl.expressions.iter() { + output_expressions.push(convert_ir_expression( + &allocator, + core_namespace, + expr, + expressions, + root_xref, + )); + } + OutputExpression::TaggedTemplateLiteral(Box::new_in( + crate::output::ast::TaggedTemplateLiteralExpr { + tag: Box::new_in(tag, &allocator), + template: Box::new_in( + crate::output::ast::TemplateLiteralExpr { + elements, + expressions: output_expressions, + source_span: ttl.source_span, + }, + &allocator, + ), + source_span: ttl.source_span, + }, + &allocator, + )) + } + + // Spread element: convert the inner expression + IrExpression::SpreadElement(spread) => { + let inner = convert_ir_expression( + allocator, + core_namespace, + &spread.expr, + expressions, + root_xref, + ); + OutputExpression::SpreadElement(Box::new_in( + SpreadElementExpr { + expr: Box::new_in(inner, &allocator), + source_span: spread.source_span, + }, + &allocator, + )) + } + IrExpression::Parenthesized(paren) => { let inner = convert_ir_expression( allocator, @@ -1178,6 +1236,29 @@ pub fn convert_ir_expression<'a>( )) } + // Arrow function left in place: emit it with its converted body. + IrExpression::ArrowFunction(arrow_fn) => { + let mut params = OxcVec::with_capacity_in(arrow_fn.params.len(), &allocator); + for param in &arrow_fn.params { + params.push(FnParam { name: param.name }); + } + let body = convert_ir_expression( + allocator, + core_namespace, + &arrow_fn.body, + expressions, + root_xref, + ); + OutputExpression::ArrowFunction(Box::new_in( + ArrowFunctionExpr { + params, + body: ArrowFunctionBody::Expression(Box::new_in(body, &allocator)), + source_span: arrow_fn.source_span, + }, + &allocator, + )) + } + // For any remaining IR expressions, return null placeholder _ => OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Null, source_span: None }, diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/resolve_dollar_event.rs b/crates/oxc_angular_compiler/src/pipeline/phases/resolve_dollar_event.rs index e36fff975..6e9b69aed 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/resolve_dollar_event.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/resolve_dollar_event.rs @@ -18,40 +18,52 @@ const DOLLAR_EVENT: &str = "$event"; /// Resolves $event to the event parameter in handlers. /// /// This phase: -/// 1. Finds all `LexicalReadExpr('$event')` in event handlers +/// 1. Rewrites `LexicalReadExpr('$event')` in event handlers to an output +/// `ReadVarExpr('$event')` so it bypasses name resolution, matching the +/// TypeScript implementation. /// 2. Marks the listener as consuming $event /// -/// Note: Unlike the TypeScript implementation, we don't need to transform -/// the expression since LexicalRead will be converted to proper variable -/// access in the reify phase. +/// Expressions stored as raw AST (Ast/ExpressionRef) cannot be rewritten here; +/// they are still emitted correctly by the reify phase's `$event` special-case. pub fn resolve_dollar_event(job: &mut ComponentCompilationJob<'_>) { // We need to borrow the expression store separately from the views // to avoid mutable borrow conflicts. We use a raw pointer to the store // since transform_dollar_event_create only needs read access to expressions. let expressions_ptr = &job.expressions as *const ExpressionStore<'_>; + let allocator = job.allocator; // Process each view's create ops (listeners are CreateOps) for view in job.all_views_mut() { // SAFETY: We only read from expressions, never modify it, and job.expressions // outlives this function call. let expressions = unsafe { &*expressions_ptr }; - transform_dollar_event_create(&mut view.create, expressions); + transform_dollar_event_create(&mut view.create, expressions, allocator); } } /// Transform $event in create operations. +/// +/// Listener handler ops are transformed as child operations, matching +/// `transformExpressionsInOp`. Each `LexicalReadExpr('$event')` found is +/// rewritten to an output `ReadVarExpr`, like Angular's resolveDollarEvent. fn transform_dollar_event_create<'a>( ops: &mut crate::ir::list::CreateOpList<'a>, expressions: &ExpressionStore<'a>, + allocator: &'a oxc_allocator::Allocator, ) { for op in ops.iter_mut() { match op { CreateOp::Listener(listener) => { // Check handler expression for $event usage - if let Some(handler) = &listener.handler_expression { + if let Some(handler) = &mut listener.handler_expression { if expression_contains_dollar_event(handler, expressions) { listener.consumes_dollar_event = true; } + crate::ir::expression::transform_expressions_in_expression( + handler, + &|expr, _flags| replace_dollar_event(expr, allocator), + VisitorContextFlag::IN_CHILD_OPERATION, + ); } // Check handler ops for $event usage @@ -64,32 +76,29 @@ fn transform_dollar_event_create<'a>( found.set(true); } }, - VisitorContextFlag::NONE, + VisitorContextFlag::IN_CHILD_OPERATION, ); if found.get() { listener.consumes_dollar_event = true; - break; } } + // Rewrite $event reads in handler ops + for handler_op in listener.handler_ops.iter_mut() { + transform_expressions_in_update_op( + handler_op, + &|expr, _flags| replace_dollar_event(expr, allocator), + VisitorContextFlag::IN_CHILD_OPERATION, + ); + } } CreateOp::TwoWayListener(listener) => { // Check handler ops for $event usage for handler_op in listener.handler_ops.iter_mut() { - let found = Cell::new(false); transform_expressions_in_update_op( handler_op, - &|expr, _flags| { - if expression_contains_dollar_event(expr, expressions) { - found.set(true); - } - }, - VisitorContextFlag::NONE, + &|expr, _flags| replace_dollar_event(expr, allocator), + VisitorContextFlag::IN_CHILD_OPERATION, ); - if found.get() { - // TwoWayListener doesn't have consumes_dollar_event field - // since it always implicitly consumes $event - break; - } } } CreateOp::AnimationListener(listener) => { @@ -103,19 +112,45 @@ fn transform_dollar_event_create<'a>( found.set(true); } }, - VisitorContextFlag::NONE, + VisitorContextFlag::IN_CHILD_OPERATION, ); if found.get() { listener.consumes_dollar_event = true; - break; } } + // Rewrite $event reads in handler ops + for handler_op in listener.handler_ops.iter_mut() { + transform_expressions_in_update_op( + handler_op, + &|expr, _flags| replace_dollar_event(expr, allocator), + VisitorContextFlag::IN_CHILD_OPERATION, + ); + } } _ => {} } } } +/// Rewrites `LexicalReadExpr('$event')` to an output `ReadVarExpr` so the read +/// bypasses name resolution, like Angular's resolveDollarEvent. +fn replace_dollar_event<'a>(expr: &mut IrExpression<'a>, allocator: &'a oxc_allocator::Allocator) { + if let IrExpression::LexicalRead(lexical) = expr { + if lexical.name.as_str() == DOLLAR_EVENT { + *expr = IrExpression::OutputExpr(oxc_allocator::Box::new_in( + crate::output::ast::OutputExpression::ReadVar(oxc_allocator::Box::new_in( + crate::output::ast::ReadVarExpr { + name: lexical.name.clone(), + source_span: lexical.source_span, + }, + &allocator, + )), + &allocator, + )); + } + } +} + /// Check if an expression contains a $event reference. fn expression_contains_dollar_event<'a>( expr: &IrExpression<'a>, @@ -262,6 +297,17 @@ fn expression_contains_dollar_event<'a>( rtl.expressions.iter().any(|e| expression_contains_dollar_event(e, expressions)) } + // TaggedTemplateLiteral: check the tag and embedded expressions + IrExpression::TaggedTemplateLiteral(ttl) => { + expression_contains_dollar_event(&ttl.tag, expressions) + || ttl.expressions.iter().any(|e| expression_contains_dollar_event(e, expressions)) + } + + // SpreadElement: check the inner expression + IrExpression::SpreadElement(spread) => { + expression_contains_dollar_event(&spread.expr, expressions) + } + // ArrowFunction: check the body expression IrExpression::ArrowFunction(arrow_fn) => { expression_contains_dollar_event(&arrow_fn.body, expressions) @@ -397,5 +443,5 @@ pub fn resolve_dollar_event_for_host(job: &mut HostBindingCompilationJob<'_>) { let expressions = unsafe { &*expressions_ptr }; // Process create ops for listeners - transform_dollar_event_create(&mut job.root.create, expressions); + transform_dollar_event_create(&mut job.root.create, expressions, job.allocator); } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/resolve_names.rs b/crates/oxc_angular_compiler/src/pipeline/phases/resolve_names.rs index 89d4534a5..a4a95a280 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/resolve_names.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/resolve_names.rs @@ -857,6 +857,19 @@ fn resolve_expression<'a>( } } + // Arrow function - resolve the names its body reads. Reads of its own parameters + // were already turned into plain variables during ingestion. + IrExpression::ArrowFunction(arrow_fn) => { + resolve_expression( + arrow_fn.body.as_mut(), + scope, + root_xref, + saved_view, + allocator, + expressions, + ); + } + // Other expression types don't need resolution _ => {} } @@ -874,6 +887,22 @@ fn resolve_angular_expression<'a>( scope: &ScopeMaps<'a>, root_xref: XrefId, allocator: &'a oxc_allocator::Allocator, +) -> Option> { + resolve_angular_expression_with_params(ast_expr, scope, root_xref, allocator, None) +} + +/// Inner resolver that also tracks arrow function parameter names. +/// +/// `param_names` is the shared parameter set of the enclosing arrow function's +/// `updateParameterReferences` run: a `PropertyRead(ImplicitReceiver, name)` +/// whose name is in the set becomes a plain `ReadVarExpr` instead of resolving +/// against scope or the component context. `None` outside arrow subtrees. +fn resolve_angular_expression_with_params<'a>( + ast_expr: &AngularExpression<'a>, + scope: &ScopeMaps<'a>, + root_xref: XrefId, + allocator: &'a oxc_allocator::Allocator, + param_names: Option<&std::cell::RefCell>>>, ) -> Option> { match ast_expr { AngularExpression::PropertyRead(prop_read) => { @@ -882,6 +911,21 @@ fn resolve_angular_expression<'a>( let name = &prop_read.name; let source_span = Some(prop_read.source_span.to_span()); + // A read of an enclosing arrow function's own parameter becomes a plain + // variable read, matching Angular's updateParameterReferences which turns + // parameter LexicalReads into o.ReadVarExpr before name resolution. + if let Some(names) = param_names { + if names.borrow().contains(name) { + return Some(IrExpression::OutputExpr(Box::new_in( + crate::output::ast::OutputExpression::ReadVar(Box::new_in( + crate::output::ast::ReadVarExpr { name: name.clone(), source_span }, + &allocator, + )), + &allocator, + ))); + } + } + // Context properties ($implicit, $index, etc.) and $event are special: // - $event is the event handler parameter, never resolved to a variable // - Context properties may or may not be in scope: @@ -947,9 +991,13 @@ fn resolve_angular_expression<'a>( } else { // This is a nested property read like item.name // Try to resolve the receiver first - if let Some(resolved_receiver) = - resolve_angular_expression(&prop_read.receiver, scope, root_xref, &allocator) - { + if let Some(resolved_receiver) = resolve_angular_expression_with_params( + &prop_read.receiver, + scope, + root_xref, + allocator, + param_names, + ) { // Receiver was resolved - create ResolvedPropertyRead Some(IrExpression::ResolvedPropertyRead(Box::new_in( ResolvedPropertyReadExpr { @@ -977,17 +1025,26 @@ fn resolve_angular_expression<'a>( AngularExpression::Call(call) => { // Resolve the receiver - let resolved_receiver = - resolve_angular_expression(&call.receiver, scope, root_xref, &allocator); + let resolved_receiver = resolve_angular_expression_with_params( + &call.receiver, + scope, + root_xref, + allocator, + param_names, + ); // Resolve each argument let mut resolved_args = oxc_allocator::Vec::new_in(&allocator); let mut any_arg_resolved = false; for arg in call.args.iter() { - if let Some(resolved_arg) = - resolve_angular_expression(arg, scope, root_xref, &allocator) - { + if let Some(resolved_arg) = resolve_angular_expression_with_params( + arg, + scope, + root_xref, + allocator, + param_names, + ) { resolved_args.push(resolved_arg); any_arg_resolved = true; } else { @@ -1025,10 +1082,21 @@ fn resolve_angular_expression<'a>( AngularExpression::KeyedRead(keyed) => { // Resolve receiver for keyed reads like item[0] - let resolved_receiver = - resolve_angular_expression(&keyed.receiver, scope, root_xref, &allocator); + let resolved_receiver = resolve_angular_expression_with_params( + &keyed.receiver, + scope, + root_xref, + allocator, + param_names, + ); // Also try to resolve the key expression - let resolved_key = resolve_angular_expression(&keyed.key, scope, root_xref, &allocator); + let resolved_key = resolve_angular_expression_with_params( + &keyed.key, + scope, + root_xref, + allocator, + param_names, + ); if resolved_receiver.is_some() || resolved_key.is_some() { // At least one part was resolved, create a ResolvedKeyedRead @@ -1064,9 +1132,13 @@ fn resolve_angular_expression<'a>( AngularExpression::SafePropertyRead(safe) => { // Resolve receiver for safe property reads like item?.name - if let Some(resolved_receiver) = - resolve_angular_expression(&safe.receiver, scope, root_xref, &allocator) - { + if let Some(resolved_receiver) = resolve_angular_expression_with_params( + &safe.receiver, + scope, + root_xref, + allocator, + param_names, + ) { Some(IrExpression::ResolvedSafePropertyRead(Box::new_in( ResolvedSafePropertyReadExpr { receiver: Box::new_in(resolved_receiver, &allocator), @@ -1083,10 +1155,20 @@ fn resolve_angular_expression<'a>( AngularExpression::Binary(binary) => { // Handle binary expressions, especially assignments in event handlers // like `todo.done = $event` - let resolved_left = - resolve_angular_expression(&binary.left, scope, root_xref, &allocator); - let resolved_right = - resolve_angular_expression(&binary.right, scope, root_xref, &allocator); + let resolved_left = resolve_angular_expression_with_params( + &binary.left, + scope, + root_xref, + allocator, + param_names, + ); + let resolved_right = resolve_angular_expression_with_params( + &binary.right, + scope, + root_xref, + allocator, + param_names, + ); if resolved_left.is_some() || resolved_right.is_some() { // At least one side was resolved, create a ResolvedBinary @@ -1135,9 +1217,13 @@ fn resolve_angular_expression<'a>( let mut any_resolved = false; for expr in tl.expressions.iter() { - if let Some(resolved) = - resolve_angular_expression(expr, scope, root_xref, &allocator) - { + if let Some(resolved) = resolve_angular_expression_with_params( + expr, + scope, + root_xref, + allocator, + param_names, + ) { resolved_exprs.push(resolved); any_resolved = true; } else { @@ -1174,12 +1260,27 @@ fn resolve_angular_expression<'a>( AngularExpression::Conditional(cond) => { // Handle conditional (ternary) expressions - let resolved_condition = - resolve_angular_expression(&cond.condition, scope, root_xref, &allocator); - let resolved_true = - resolve_angular_expression(&cond.true_exp, scope, root_xref, &allocator); - let resolved_false = - resolve_angular_expression(&cond.false_exp, scope, root_xref, &allocator); + let resolved_condition = resolve_angular_expression_with_params( + &cond.condition, + scope, + root_xref, + allocator, + param_names, + ); + let resolved_true = resolve_angular_expression_with_params( + &cond.true_exp, + scope, + root_xref, + allocator, + param_names, + ); + let resolved_false = resolve_angular_expression_with_params( + &cond.false_exp, + scope, + root_xref, + allocator, + param_names, + ); if resolved_condition.is_some() || resolved_true.is_some() || resolved_false.is_some() { let condition = resolved_condition.unwrap_or_else(|| { @@ -1227,9 +1328,13 @@ fn resolve_angular_expression<'a>( let mut any_resolved = false; for value in map.values.iter() { - if let Some(resolved) = - resolve_angular_expression(value, scope, root_xref, &allocator) - { + if let Some(resolved) = resolve_angular_expression_with_params( + value, + scope, + root_xref, + allocator, + param_names, + ) { resolved_values.push(resolved); any_resolved = true; } else { @@ -1290,9 +1395,13 @@ fn resolve_angular_expression<'a>( } else { entry }; - if let Some(resolved) = - resolve_angular_expression(inner, scope, root_xref, &allocator) - { + if let Some(resolved) = resolve_angular_expression_with_params( + inner, + scope, + root_xref, + allocator, + param_names, + ) { resolved_entries.push(resolved); any_resolved = true; } else { @@ -1321,9 +1430,13 @@ fn resolve_angular_expression<'a>( AngularExpression::PrefixNot(prefix_not) => { // Handle prefix not expressions (!expr) - need to resolve variable references // in the operand. This is critical for expressions like `!bold` in listeners. - if let Some(resolved) = - resolve_angular_expression(&prefix_not.expression, scope, root_xref, &allocator) - { + if let Some(resolved) = resolve_angular_expression_with_params( + &prefix_not.expression, + scope, + root_xref, + allocator, + param_names, + ) { Some(IrExpression::Not(Box::new_in( crate::ir::expression::NotExpr { expr: Box::new_in(resolved, &allocator), @@ -1338,9 +1451,13 @@ fn resolve_angular_expression<'a>( AngularExpression::Unary(unary) => { // Handle unary expressions (+expr or -expr) - need to resolve variable references - if let Some(resolved) = - resolve_angular_expression(&unary.expr, scope, root_xref, &allocator) - { + if let Some(resolved) = resolve_angular_expression_with_params( + &unary.expr, + scope, + root_xref, + allocator, + param_names, + ) { Some(IrExpression::Unary(Box::new_in( crate::ir::expression::UnaryExpr { operator: match unary.operator { @@ -1363,9 +1480,13 @@ fn resolve_angular_expression<'a>( AngularExpression::TypeofExpression(typeof_expr) => { // Handle typeof expressions - need to resolve variable references - if let Some(resolved) = - resolve_angular_expression(&typeof_expr.expression, scope, root_xref, &allocator) - { + if let Some(resolved) = resolve_angular_expression_with_params( + &typeof_expr.expression, + scope, + root_xref, + allocator, + param_names, + ) { Some(IrExpression::Typeof(Box::new_in( crate::ir::expression::TypeofExpr { expr: Box::new_in(resolved, &allocator), @@ -1380,9 +1501,13 @@ fn resolve_angular_expression<'a>( AngularExpression::VoidExpression(void_expr) => { // Handle void expressions - need to resolve variable references - if let Some(resolved) = - resolve_angular_expression(&void_expr.expression, scope, root_xref, &allocator) - { + if let Some(resolved) = resolve_angular_expression_with_params( + &void_expr.expression, + scope, + root_xref, + allocator, + param_names, + ) { Some(IrExpression::Void(Box::new_in( crate::ir::expression::VoidExpr { expr: Box::new_in(resolved, &allocator), @@ -1398,49 +1523,63 @@ fn resolve_angular_expression<'a>( AngularExpression::NonNullAssert(nna) => { // Handle non-null assertion expressions (expr!) - need to resolve variable references // NonNullAssert expressions are wrapped in Ast since IrExpression doesn't have this variant - resolve_angular_expression(&nna.expression, scope, root_xref, &allocator) + resolve_angular_expression_with_params( + &nna.expression, + scope, + root_xref, + allocator, + param_names, + ) } AngularExpression::ParenthesizedExpression(paren) => { // Handle parenthesized expressions - need to resolve variable references // within the inner expression - resolve_angular_expression(&paren.expression, scope, root_xref, &allocator) + resolve_angular_expression_with_params( + &paren.expression, + scope, + root_xref, + allocator, + param_names, + ) } AngularExpression::Chain(chain) => { - // Handle chain expressions (expr1; expr2; ...) - need to resolve all expressions - // Chain is not directly supported in IrExpression, so we need to handle it specially - let mut any_resolved = false; - for expr in chain.expressions.iter() { - if resolve_angular_expression(expr, scope, root_xref, &allocator).is_some() { - any_resolved = true; - break; - } - } - // Chain expressions are not representable in IR, but we still want to - // flag if any inner expressions need resolution - // For now, return None and let the Ast path handle it - if any_resolved { - // This path will not be perfectly handled, but chain expressions - // in listeners are unusual; the primary path (Ast) will handle most cases - None - } else { - None + // Chains are not directly representable in IR. The reify path emits only + // the last member of a stored chain, so resolve that member here. + match chain.expressions.last() { + Some(last) => resolve_angular_expression_with_params( + last, + scope, + root_xref, + allocator, + param_names, + ), + None => None, } } AngularExpression::SafeCall(safe_call) => { // Handle safe function calls (fn?.()) - need to resolve receiver and arguments - let resolved_receiver = - resolve_angular_expression(&safe_call.receiver, scope, root_xref, &allocator); + let resolved_receiver = resolve_angular_expression_with_params( + &safe_call.receiver, + scope, + root_xref, + allocator, + param_names, + ); let mut resolved_args = oxc_allocator::Vec::new_in(&allocator); let mut any_arg_resolved = false; for arg in safe_call.args.iter() { - if let Some(resolved) = - resolve_angular_expression(arg, scope, root_xref, &allocator) - { + if let Some(resolved) = resolve_angular_expression_with_params( + arg, + scope, + root_xref, + allocator, + param_names, + ) { resolved_args.push(resolved); any_arg_resolved = true; } else { @@ -1478,10 +1617,20 @@ fn resolve_angular_expression<'a>( AngularExpression::SafeKeyedRead(safe_keyed) => { // Handle safe keyed read (obj?.[key]) - need to resolve receiver and key - let resolved_receiver = - resolve_angular_expression(&safe_keyed.receiver, scope, root_xref, &allocator); - let resolved_key = - resolve_angular_expression(&safe_keyed.key, scope, root_xref, &allocator); + let resolved_receiver = resolve_angular_expression_with_params( + &safe_keyed.receiver, + scope, + root_xref, + allocator, + param_names, + ); + let resolved_key = resolve_angular_expression_with_params( + &safe_keyed.key, + scope, + root_xref, + allocator, + param_names, + ); if resolved_receiver.is_some() || resolved_key.is_some() { let receiver = resolved_receiver.unwrap_or_else(|| { @@ -1516,6 +1665,133 @@ fn resolve_angular_expression<'a>( } } + AngularExpression::ArrowFunction(arrow) => { + // Mirror Angular's updateParameterReferences: reads of the arrow's own + // parameters become plain variable reads. The parameter set is shared + // with the enclosing arrow's run (if any) and only grows: this arrow's + // parameters are added after its body is resolved, matching the + // post-order traversal in ingest.ts. + let names = match param_names { + Some(shared) => std::cell::RefCell::new( + shared.borrow().iter().cloned().collect::>>(), + ), + None => std::cell::RefCell::new(rustc_hash::FxHashSet::default()), + }; + names.borrow_mut().extend(arrow.parameters.iter().map(|p| p.name.clone())); + + let body = resolve_angular_expression_with_params( + &arrow.body, + scope, + root_xref, + allocator, + Some(&names), + ) + .unwrap_or_else(|| { + IrExpression::Ast(Box::new_in( + crate::ir::expression::clone_angular_expression(&arrow.body, &allocator), + &allocator, + )) + }); + + if let Some(shared) = param_names { + shared.borrow_mut().extend(names.borrow().iter().cloned()); + } + + let mut params = oxc_allocator::Vec::new_in(&allocator); + for param in arrow.parameters.iter() { + params.push(crate::output::ast::FnParam { name: param.name.clone() }); + } + + Some(IrExpression::ArrowFunction(Box::new_in( + crate::ir::expression::ArrowFunctionExpr { + params, + body: Box::new_in(body, &allocator), + ops: oxc_allocator::Vec::new_in(&allocator), + var_offset: None, + source_span: Some(arrow.source_span.to_span()), + }, + &allocator, + ))) + } + + AngularExpression::TaggedTemplateLiteral(ttl) => { + // Resolve the tag and the template's embedded expressions. + let resolved_tag = resolve_angular_expression_with_params( + &ttl.tag, + scope, + root_xref, + allocator, + param_names, + ); + + let mut resolved_exprs = oxc_allocator::Vec::new_in(&allocator); + let mut any_resolved = resolved_tag.is_some(); + for expr in ttl.template.expressions.iter() { + if let Some(resolved) = resolve_angular_expression_with_params( + expr, + scope, + root_xref, + allocator, + param_names, + ) { + resolved_exprs.push(resolved); + any_resolved = true; + } else { + resolved_exprs.push(IrExpression::Ast(Box::new_in( + crate::ir::expression::clone_angular_expression(expr, &allocator), + &allocator, + ))); + } + } + + if any_resolved { + let tag = resolved_tag.unwrap_or_else(|| { + IrExpression::Ast(Box::new_in( + crate::ir::expression::clone_angular_expression(&ttl.tag, &allocator), + &allocator, + )) + }); + let mut elements = oxc_allocator::Vec::new_in(&allocator); + for elem in ttl.template.elements.iter() { + elements.push(crate::ir::expression::IrTemplateLiteralElement { + text: elem.text.clone(), + source_span: Some(elem.source_span.to_span()), + }); + } + Some(IrExpression::TaggedTemplateLiteral(Box::new_in( + crate::ir::expression::IrTaggedTemplateLiteralExpr { + tag: Box::new_in(tag, &allocator), + elements, + expressions: resolved_exprs, + source_span: Some(ttl.source_span.to_span()), + }, + &allocator, + ))) + } else { + None + } + } + + AngularExpression::SpreadElement(spread) => { + // Resolve the inner expression of a spread element. + resolve_angular_expression_with_params( + &spread.expression, + scope, + root_xref, + allocator, + param_names, + ) + .map(|resolved| { + IrExpression::SpreadElement(Box::new_in( + crate::ir::expression::IrSpreadElementExpr { + expr: Box::new_in(resolved, &allocator), + source_span: Some(spread.source_span.to_span()), + }, + &allocator, + )) + }) + } + // Other expression types don't need recursive resolution _ => None, } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/save_restore_view.rs b/crates/oxc_angular_compiler/src/pipeline/phases/save_restore_view.rs index f66e5d135..15c48bdd8 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/save_restore_view.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/save_restore_view.rs @@ -452,12 +452,13 @@ fn add_restore_view_to_listener<'a>( handler_ops.insert(0, restore_var); // Wrap handler_expression in ResetViewExpr (Angular's save_restore_view.ts lines 84-91) - // This resets the view context after the listener handler returns + // This resets the view context after the listener handler returns. The expression + // is moved in place (not cloned) so nodes referenced elsewhere, such as arrow + // functions tracked in view.functions, keep their identity. if let Some(expr) = handler_expression.take() { - let cloned_expr = expr.clone_in(allocator); *handler_expression = Some(Box::new_in( IrExpression::ResetView(Box::new_in( - ResetViewExpr { expr: Box::new_in(cloned_expr, &allocator), source_span: None }, + ResetViewExpr { expr, source_span: None }, &allocator, )), &allocator, diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/store_let_optimization.rs b/crates/oxc_angular_compiler/src/pipeline/phases/store_let_optimization.rs index 2cfa32216..c4e9425e9 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/store_let_optimization.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/store_let_optimization.rs @@ -682,6 +682,30 @@ fn transform_nested_expressions<'a>( ); } } + IrExpression::TaggedTemplateLiteral(ttl) => { + transform_store_let_in_expr_value( + &allocator, + &mut ttl.tag, + let_used_externally, + declare_lets_to_remove, + ); + for e in ttl.expressions.iter_mut() { + transform_store_let_in_expr_value( + &allocator, + e, + let_used_externally, + declare_lets_to_remove, + ); + } + } + IrExpression::SpreadElement(spread) => { + transform_store_let_in_expr_value( + &allocator, + &mut spread.expr, + let_used_externally, + declare_lets_to_remove, + ); + } IrExpression::ArrowFunction(arrow_fn) => { transform_store_let_in_expr_value( @@ -1084,6 +1108,30 @@ fn transform_nested_in_expr_value<'a>( ); } } + IrExpression::TaggedTemplateLiteral(ttl) => { + transform_store_let_in_expr_value( + &allocator, + &mut ttl.tag, + let_used_externally, + declare_lets_to_remove, + ); + for e in ttl.expressions.iter_mut() { + transform_store_let_in_expr_value( + &allocator, + e, + let_used_externally, + declare_lets_to_remove, + ); + } + } + IrExpression::SpreadElement(spread) => { + transform_store_let_in_expr_value( + &allocator, + &mut spread.expr, + let_used_externally, + declare_lets_to_remove, + ); + } IrExpression::ArrowFunction(arrow_fn) => { transform_store_let_in_expr_value( @@ -1152,6 +1200,10 @@ fn has_pipe(expr: &IrExpression<'_>) -> bool { IrExpression::Typeof(t) => has_pipe(&t.expr), IrExpression::Void(v) => has_pipe(&v.expr), IrExpression::ResolvedTemplateLiteral(rtl) => rtl.expressions.iter().any(has_pipe), + IrExpression::TaggedTemplateLiteral(ttl) => { + has_pipe(&ttl.tag) || ttl.expressions.iter().any(has_pipe) + } + IrExpression::SpreadElement(spread) => has_pipe(&spread.expr), // Leaf expressions don't contain pipes IrExpression::LexicalRead(_) | IrExpression::Reference(_) @@ -1398,6 +1450,15 @@ fn collect_context_let_refs_in_expr(expr: &IrExpression<'_>, refs: &mut HashSet< collect_context_let_refs_in_expr(e, refs); } } + IrExpression::TaggedTemplateLiteral(ttl) => { + collect_context_let_refs_in_expr(&ttl.tag, refs); + for e in ttl.expressions.iter() { + collect_context_let_refs_in_expr(e, refs); + } + } + IrExpression::SpreadElement(spread) => { + collect_context_let_refs_in_expr(&spread.expr, refs); + } IrExpression::ArrowFunction(arrow_fn) => { collect_context_let_refs_in_expr(&arrow_fn.body, refs); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/strip_nonrequired_parentheses.rs b/crates/oxc_angular_compiler/src/pipeline/phases/strip_nonrequired_parentheses.rs index 8a3476cd2..5f6914189 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/strip_nonrequired_parentheses.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/strip_nonrequired_parentheses.rs @@ -397,6 +397,15 @@ fn check_ir_expression_for_required_parens( check_ir_expression_for_required_parens(e, required); } } + IrExpression::TaggedTemplateLiteral(ttl) => { + check_ir_expression_for_required_parens(&ttl.tag, required); + for e in ttl.expressions.iter() { + check_ir_expression_for_required_parens(e, required); + } + } + IrExpression::SpreadElement(spread) => { + check_ir_expression_for_required_parens(&spread.expr, required); + } IrExpression::ArrowFunction(arrow_fn) => { check_ir_expression_for_required_parens(&arrow_fn.body, required); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/temporary_variables.rs b/crates/oxc_angular_compiler/src/pipeline/phases/temporary_variables.rs index becb984bc..f37195191 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/temporary_variables.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/temporary_variables.rs @@ -573,6 +573,15 @@ fn assign_temp_names<'a>( assign_temp_names(e, tracker, &allocator); } } + IrExpression::TaggedTemplateLiteral(ttl) => { + assign_temp_names(&mut ttl.tag, tracker, &allocator); + for e in ttl.expressions.iter_mut() { + assign_temp_names(e, tracker, &allocator); + } + } + IrExpression::SpreadElement(spread) => { + assign_temp_names(&mut spread.expr, tracker, &allocator); + } IrExpression::ArrowFunction(arrow_fn) => { assign_temp_names(&mut arrow_fn.body, tracker, &allocator); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/var_counting.rs b/crates/oxc_angular_compiler/src/pipeline/phases/var_counting.rs index d25afc0af..e5cf59681 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/var_counting.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/var_counting.rs @@ -641,15 +641,32 @@ fn assign_var_offsets_in_expr( assign_var_offsets_in_expr(e, var_count, pure_functions_only); } } + IrExpression::TaggedTemplateLiteral(ttl) => { + assign_var_offsets_in_expr(&mut ttl.tag, var_count, pure_functions_only); + for e in ttl.expressions.iter_mut() { + assign_var_offsets_in_expr(e, var_count, pure_functions_only); + } + } + IrExpression::SpreadElement(spread) => { + assign_var_offsets_in_expr(&mut spread.expr, var_count, pure_functions_only); + } IrExpression::ArrowFunction(arrow_fn) => { - // Arrow functions consume a var slot and have their own var_offset - if !pure_functions_only { + // Collected arrows (members of unit.functions, marked by + // generate_arrow_functions with a u32::MAX sentinel) consume a var + // slot upstream (var_counting.ts varsUsedByIrExpression), even though + // they are emitted in place here since `ɵɵarrowFunction` hoisting is + // not implemented. Arrows preserved in place (var_offset still None) + // consume nothing. Process ops and body for inner var consumers. + let is_collected = arrow_fn.var_offset == Some(u32::MAX); + for op in arrow_fn.ops.iter_mut() { + assign_var_offsets_in_op(op, var_count, pure_functions_only); + } + assign_var_offsets_in_expr(&mut arrow_fn.body, var_count, pure_functions_only); + if is_collected && !pure_functions_only { arrow_fn.var_offset = Some(*var_count); *var_count += 1; } - // Process the body expression - assign_var_offsets_in_expr(&mut arrow_fn.body, var_count, pure_functions_only); } IrExpression::Parenthesized(paren) => { assign_var_offsets_in_expr(&mut paren.expr, var_count, pure_functions_only); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs b/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs index 22649574f..1d70a6852 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs @@ -229,6 +229,15 @@ fn collect_fences(expr: &IrExpression<'_>) -> Fence { fences |= collect_fences(e); } } + IrExpression::TaggedTemplateLiteral(ttl) => { + fences |= collect_fences(&ttl.tag); + for e in ttl.expressions.iter() { + fences |= collect_fences(e); + } + } + IrExpression::SpreadElement(spread) => { + fences |= collect_fences(&spread.expr); + } IrExpression::ArrowFunction(arrow_fn) => { fences |= collect_fences(&arrow_fn.body); @@ -2691,6 +2700,15 @@ fn collect_variable_xrefs(expr: &IrExpression<'_>, xrefs: &mut Vec) { collect_variable_xrefs(e, xrefs); } } + IrExpression::TaggedTemplateLiteral(ttl) => { + collect_variable_xrefs(&ttl.tag, xrefs); + for e in ttl.expressions.iter() { + collect_variable_xrefs(e, xrefs); + } + } + IrExpression::SpreadElement(spread) => { + collect_variable_xrefs(&spread.expr, xrefs); + } IrExpression::ArrowFunction(arrow_fn) => { collect_variable_xrefs(&arrow_fn.body, xrefs); @@ -3749,6 +3767,43 @@ where &allocator, )) } + IrExpression::TaggedTemplateLiteral(ttl) => { + use crate::ir::expression::{IrTaggedTemplateLiteralExpr, IrTemplateLiteralElement}; + let tag = transform_expression(&ttl.tag, &allocator, transform); + let mut elements = OxcVec::with_capacity_in(ttl.elements.len(), &allocator); + for elem in ttl.elements.iter() { + elements.push(IrTemplateLiteralElement { + text: elem.text.clone(), + source_span: elem.source_span, + }); + } + let mut expressions = OxcVec::with_capacity_in(ttl.expressions.len(), &allocator); + for e in ttl.expressions.iter() { + expressions.push(transform_expression(e, &allocator, transform)); + } + IrExpression::TaggedTemplateLiteral(OxcBox::new_in( + IrTaggedTemplateLiteralExpr { + tag: OxcBox::new_in(tag, &allocator), + elements, + expressions, + source_span: ttl.source_span, + }, + &allocator, + )) + } + IrExpression::SpreadElement(spread) => { + use crate::ir::expression::IrSpreadElementExpr; + IrExpression::SpreadElement(OxcBox::new_in( + IrSpreadElementExpr { + expr: OxcBox::new_in( + transform_expression(&spread.expr, &allocator, transform), + &allocator, + ), + source_span: spread.source_span, + }, + &allocator, + )) + } IrExpression::ArrowFunction(arrow_fn) => { use crate::ir::expression::ArrowFunctionExpr; diff --git a/crates/oxc_angular_compiler/tests/arrow_function_scope_test.rs b/crates/oxc_angular_compiler/tests/arrow_function_scope_test.rs new file mode 100644 index 000000000..c7053caa7 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/arrow_function_scope_test.rs @@ -0,0 +1,255 @@ +//! A name read inside a template arrow function resolves to the arrow's own +//! parameter (or an enclosing arrow's) before template variables and the +//! component context: `open.update((o) => !o)` must not emit `(o) => !ctx.o`. +//! +//! Ported from Angular's `updateParameterReferences` in +//! `template/pipeline/src/ingest.ts`. The expected strings are ngtsc's output +//! for the same templates, in this emitter's spelling (parenthesized params +//! and binary operands). + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{ + CompilationMode, TransformOptions, + output::emitter::JsEmitter, + parser::html::HtmlParser, + pipeline::{emit::compile_template, ingest::ingest_component}, + transform::html_to_r3::{HtmlToR3Transform, TransformOptions as HtmlTransformOptions}, + transform_angular_file, +}; +use oxc_str::Ident; + +/// Compile a template through the IR pipeline, with whitespace removed. +fn compile_tpl(template: &str) -> String { + let allocator = Allocator::default(); + let parser = HtmlParser::with_expansion_forms(&allocator, template, "test.html"); + let html_result = parser.parse(); + assert!(html_result.errors.is_empty()); + let transformer = HtmlToR3Transform::new( + &allocator, + template, + HtmlTransformOptions { angular_version: None, ..HtmlTransformOptions::default() }, + ); + let r3_result = transformer.transform(&html_result.nodes); + assert!(r3_result.errors.is_empty()); + let mut job = ingest_component(&allocator, Ident::from("TestComponent"), r3_result.nodes); + let result = compile_template(&mut job); + let emitter = JsEmitter::new(); + let mut output = String::new(); + for decl in &result.declarations { + output.push_str(&emitter.emit_statement(decl)); + } + output.push_str(&emitter.emit_statements(&result.template_fn.statements)); + output.chars().filter(|c| !c.is_whitespace()).collect() +} + +#[track_caller] +fn assert_emits(template: &str, expected: &str) { + let code = compile_tpl(template); + assert!(code.contains(expected), "expected `{expected}` for `{template}`, got:\n{code}"); +} + +#[test] +fn listener_arrow_reads_its_parameter() { + assert_emits( + r#""#, + "returnctx.open.update((o)=>!o);", + ); +} + +#[test] +fn listener_arrow_without_parameters_reads_the_component() { + assert_emits( + r#""#, + "returnctx.open.update(()=>!ctx.open());", + ); +} + +#[test] +fn listener_arrow_reads_several_parameters() { + assert_emits( + r#""#, + "returnctx.run((a,b)=>(a+b));", + ); +} + +#[test] +fn parameter_wins_over_a_component_property_of_the_same_name() { + assert_emits( + r#""#, + "returnctx.open.update((open)=>!open);", + ); +} + +#[test] +fn listener_arrow_reads_a_parameter_and_a_component_property() { + assert_emits( + r#""#, + "returnctx.open.update((o)=>(o||ctx.fallback()));", + ); +} + +#[test] +fn nested_arrow_reads_the_outer_parameter() { + assert_emits( + r#""#, + "returnctx.run((a)=>(b)=>((a+b)+ctx.other));", + ); +} + +#[test] +fn parameter_shadows_a_let_declaration() { + assert_emits( + r#"@let v = 1; {{ v }}"#, + "returnctx.run((v)=>(v+ctx.other));", + ); +} + +#[test] +fn parameter_shadows_a_for_item() { + assert_emits( + r#"@for (item of items; track item) { }"#, + "i0.ɵɵrestoreView(_r1);constctx_r1=i0.ɵɵnextContext();\ + returni0.ɵɵresetView(ctx_r1.run((item)=>(item+ctx_r1.other)));", + ); +} + +#[test] +fn parameter_shadows_a_template_reference() { + assert_emits( + r#""#, + "returnctx.run((el)=>(el+ctx.other));", + ); +} + +#[test] +fn listener_arrow_reads_template_variables_that_are_not_parameters() { + assert_emits( + r#"@for (item of items; track item) { }"#, + "constitem_r2=i0.ɵɵrestoreView(_r1).$implicit;constctx_r2=i0.ɵɵnextContext();\ + constel_r4=i0.ɵɵreference(1);\ + returni0.ɵɵresetView(ctx_r2.run((x)=>(((x+item_r2)+el_r4.value)+ctx_r2.other)));", + ); +} + +#[test] +fn listener_arrow_reads_dollar_event() { + assert_emits( + r#""#, + "listener($event){returnctx.run((x)=>(x+$event.type));}", + ); +} + +/// ngtsc adds a nested arrow's parameters to the enclosing arrow's set once it has +/// visited that nested arrow, so a later read of the same name is left as a plain +/// variable while an earlier one still reads the component. +#[test] +fn nested_arrow_parameters_leak_to_later_reads_like_ngtsc() { + assert_emits( + r#""#, + "returnctx.run((a)=>(ctx.f((b)=>b)+b));", + ); + assert_emits( + r#""#, + "returnctx.run((a)=>(ctx.b+ctx.f((b)=>b)));", + ); +} + +// Outside listeners ngtsc hoists the arrow through `ɵɵarrowFunction`, which is not +// implemented here: the arrow is emitted in place. Its body must still match the +// body of ngtsc's hoisted function. + +#[test] +fn property_binding_arrow_reads_its_parameter() { + assert_emits( + r#"
"#, + r#"i0.ɵɵproperty("title",ctx.run((o)=>!o));"#, + ); + assert_emits( + r#"
"#, + r#"i0.ɵɵproperty("title",ctx.run((open)=>!open));"#, + ); + assert_emits( + r#"
"#, + r#"i0.ɵɵproperty("title",ctx.run((a)=>(b)=>((a+b)+ctx.other)));"#, + ); +} + +#[test] +fn property_binding_arrow_without_parameters_reads_the_component() { + assert_emits( + r#"
"#, + r#"i0.ɵɵproperty("title",ctx.run(()=>!ctx.open()));"#, + ); +} + +#[test] +fn property_binding_parameter_shadows_template_variables() { + assert_emits( + r#"@let v = 1;
{{ v }}"#, + r#"i0.ɵɵproperty("title",ctx.run((v)=>(v+ctx.other)));"#, + ); + assert_emits( + r#"@for (item of items; track item) {
}"#, + r#"i0.ɵɵproperty("title",ctx_r0.run((item)=>(item+ctx_r0.other)));"#, + ); + assert_emits( + r#"
"#, + r#"i0.ɵɵproperty("title",ctx.run((el)=>(el+ctx.other)));"#, + ); +} + +#[test] +fn property_binding_arrow_reads_template_variables_that_are_not_parameters() { + assert_emits( + r#"@for (item of items; track item) {
}"#, + "constitem_r1=ctx.$implicit;constctx_r1=i0.ɵɵnextContext();constel_r3=i0.ɵɵreference(1);", + ); + assert_emits( + r#"@for (item of items; track item) {
}"#, + "ctx_r1.run((x)=>(((x+item_r1)+el_r3.value)+ctx_r1.other))", + ); +} + +#[test] +fn interpolation_arrow_reads_its_parameters() { + assert_emits("{{ run((o) => !o) }}", "i0.ɵɵtextInterpolate(ctx.run((o)=>!o));"); + assert_emits("{{ run((a, b) => a + b) }}", "i0.ɵɵtextInterpolate(ctx.run((a,b)=>(a+b)));"); + assert_emits( + "{{ run((o) => o || fallback()) }}", + "i0.ɵɵtextInterpolate(ctx.run((o)=>(o||ctx.fallback())));", + ); + assert_emits("{{ run(() => !open()) }}", "i0.ɵɵtextInterpolate(ctx.run(()=>!ctx.open()));"); +} + +#[test] +fn let_declaration_arrow_reads_its_parameter() { + assert_emits("@let fn = (a) => a + other; {{ fn(1) }}", "constfn_r1=(a)=>(a+ctx.other);"); +} + +#[test] +fn host_listener_arrow_reads_its_parameter() { + let allocator = Allocator::default(); + let options = + TransformOptions { compilation_mode: CompilationMode::Full, ..Default::default() }; + let source = "import {Directive, signal} from '@angular/core'; +@Directive({ + selector: '[d]', + host: { + '(click)': 'someSignal.update(prev => prev + 1)', + '(mousedown)': 'someSignal.update(() => componentProp + 1)', + }, +}) +export class D { + someSignal = signal(0); + componentProp = 1; +} +"; + let result = transform_angular_file(&allocator, "test.ts", source, Some(&options), None); + assert!(!result.has_errors(), "should not have errors, got: {:?}", result.diagnostics); + let code: String = result.code.chars().filter(|c| !c.is_whitespace()).collect(); + assert!(code.contains("returnctx.someSignal.update((prev)=>(prev+1));"), "got:\n{code}"); + assert!( + code.contains("returnctx.someSignal.update(()=>(ctx.componentProp+1));"), + "got:\n{code}" + ); +} diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 7738daf01..c2c88b359 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -1259,11 +1259,8 @@ fn test_object_spread_at_end() { #[test] fn test_spread_in_arrow_function_body() { - // Array spread inside an arrow function binding. Arrow functions fall through to the - // ExpressionStore in ingest (not explicitly handled), so the LiteralArray with SpreadElement - // reaches convert_angular_expression_with_ctx directly. Before the fix to the LiteralArray - // arm in reify/angular_expression.rs, SpreadElement entries were silently unwrapped, - // resulting in `() => [ctx.base,"extra"]` instead of `() => [...ctx.base,"extra"]`. + // Array spread inside an arrow function binding: the spread must survive, giving + // `() => [...ctx.base,"extra"]` rather than `() => [ctx.base,"extra"]`. let js = compile_template_to_js( r#""#, "TestComponent", diff --git a/napi/angular-compiler/e2e/compare/fixtures/templates/arrow-functions.fixture.ts b/napi/angular-compiler/e2e/compare/fixtures/templates/arrow-functions.fixture.ts new file mode 100644 index 000000000..5abc853b4 --- /dev/null +++ b/napi/angular-compiler/e2e/compare/fixtures/templates/arrow-functions.fixture.ts @@ -0,0 +1,169 @@ +/** + * Arrow functions in template expressions. + * + * A name read inside an arrow body resolves to the arrow's own parameter (or + * an enclosing arrow's) before template variables and the component context. + * + * Only event bindings are covered: there the arrow stays in place. Elsewhere + * Angular hoists it through ɵɵarrowFunction, which is not implemented yet. + * + * The `ng-*` fixtures are ported from Angular's compliance suite: + * packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_arrow_functions + */ +import type { Fixture } from '../types.js' + +const MEMBERS = ` + open = signal(false); + items: any[] = []; + other = 1; + fallback() { return true; } + run(fn: any): any {} +` + +function arrow(name: string, description: string, template: string, members = MEMBERS): Fixture { + const className = `${name.replace(/(^|-)(\w)/g, (_, _dash, c) => c.toUpperCase())}Component` + return { + name, + category: 'templates', + description, + className, + type: 'full-transform', + sourceCode: ` +import { Component, signal } from '@angular/core'; + +@Component({ + selector: 'app-${name}', + standalone: true, + template: \`${template}\`, +}) +export class ${className} {${members}} + `.trim(), + expectedFeatures: [], + } +} + +export const fixtures: Fixture[] = [ + // Event bindings: the arrow stays in place inside the listener. + arrow( + 'arrow-listener-one-param', + 'Listener arrow reading its own parameter', + ``, + ), + arrow( + 'arrow-listener-no-param', + 'Listener arrow reading only the component', + ``, + ), + arrow( + 'arrow-listener-multiple-params', + 'Listener arrow reading several parameters', + ``, + ), + arrow( + 'arrow-listener-param-shadows-property', + 'Listener arrow parameter named like a component property', + ``, + ), + arrow( + 'arrow-listener-param-and-property', + 'Listener arrow reading a parameter and a component property', + ``, + ), + arrow( + 'arrow-listener-nested', + 'Nested listener arrow reading the outer parameter', + ``, + ), + arrow( + 'arrow-listener-param-shadows-let', + 'Listener arrow parameter shadowing an @let', + `@let v = 1; {{ v }}`, + ), + arrow( + 'arrow-listener-param-shadows-for-item', + 'Listener arrow parameter shadowing a @for item', + `@for (item of items; track item) { }`, + ), + arrow( + 'arrow-listener-param-shadows-ref', + 'Listener arrow parameter shadowing a template reference', + ``, + ), + arrow( + 'arrow-listener-reads-template-variables', + 'Listener arrow reading a parameter, a @for item, a reference and the component', + `@for (item of items; track item) { }`, + ), + + // Ported from Angular's compliance suite. + arrow( + 'ng-arrow-function-no-context', + 'arrow_function_no_context.ts', + ` + + + + `, + ` + sigA = signal(1); + sigB = signal(2); +`, + ), + arrow( + 'ng-arrow-function-dollar-event', + 'arrow_function_dollar_event.ts', + ` + @let topLevelLet = 1; + @if (true) { + @let innerLet = 2; + + } + `, + ` + componentProp = 0; + result = signal(''); +`, + ), + arrow( + 'ng-arrow-function-nested-listeners', + 'arrow_function_nested_listeners.ts', + ` + @let a = 1; + @if (true) { + + @if (true) { + @let c = 3; + + } + } + `, + ` + someSignal = signal(''); + componentProp = 0; +`, + ), + { + name: 'ng-arrow-function-host-listener', + category: 'templates', + description: 'arrow_function_host_listener.ts', + className: 'TestDir', + type: 'full-transform', + sourceCode: ` +import { Directive, signal } from '@angular/core'; + +@Directive({ + selector: '[appTestDir]', + standalone: true, + host: { + '(click)': 'someSignal.update(prev => prev + 1)', + '(mousedown)': 'someSignal.update(() => componentProp + 1)', + }, +}) +export class TestDir { + someSignal = signal(0); + componentProp = 1; +} + `.trim(), + expectedFeatures: ['ɵɵlistener'], + }, +]