diff --git a/crates/oxc_angular_compiler/src/class_debug_info/compiler.rs b/crates/oxc_angular_compiler/src/class_debug_info/compiler.rs index 38cdc0ec1..667f92c80 100644 --- a/crates/oxc_angular_compiler/src/class_debug_info/compiler.rs +++ b/crates/oxc_angular_compiler/src/class_debug_info/compiler.rs @@ -53,36 +53,36 @@ fn internal_compile_class_debug_info<'a>( let mut entries = Vec::new_in(allocator); // className - entries.push(LiteralMapEntry { - key: Ident::from("className"), - value: literal_string_atom(allocator, debug_info.class_name.clone()), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("className"), + literal_string_atom(allocator, debug_info.class_name.clone()), + false, + )); // Include filePath and lineNumber only if filePath is set // (matching Angular's behavior - if filePath is null, downstream consumers // will typically ignore lineNumber as well) if let Some(file_path) = &debug_info.file_path { - entries.push(LiteralMapEntry { - key: Ident::from("filePath"), - value: literal_string_atom(allocator, file_path.clone()), - quoted: false, - }); - - entries.push(LiteralMapEntry { - key: Ident::from("lineNumber"), - value: literal_number(allocator, debug_info.line_number), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("filePath"), + literal_string_atom(allocator, file_path.clone()), + false, + )); + + entries.push(LiteralMapEntry::new( + Ident::from("lineNumber"), + literal_number(allocator, debug_info.line_number), + false, + )); } // Include forbidOrphanRendering only if it's true (to reduce generated code) if debug_info.forbid_orphan_rendering { - entries.push(LiteralMapEntry { - key: Ident::from("forbidOrphanRendering"), - value: literal_bool(allocator, true), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("forbidOrphanRendering"), + literal_bool(allocator, true), + false, + )); } let debug_info_object = OutputExpression::LiteralMap(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index 6662bc5f7..cf04251d0 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/builders.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/builders.rs @@ -65,11 +65,7 @@ pub fn build_decorator_metadata_array<'a>( }; // Add "type" entry - map_entries.push(LiteralMapEntry { - key: Ident::from("type"), - value: type_expr, - quoted: false, - }); + map_entries.push(LiteralMapEntry::new(Ident::from("type"), type_expr, false)); // Add "args" entry if the decorator has arguments if let Expression::CallExpression(call) = &decorator.expression @@ -84,14 +80,14 @@ pub fn build_decorator_metadata_array<'a>( } if !args.is_empty() { - map_entries.push(LiteralMapEntry { - key: Ident::from("args"), - value: OutputExpression::LiteralArray(Box::new_in( + map_entries.push(LiteralMapEntry::new( + Ident::from("args"), + OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries: args, source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } } @@ -156,22 +152,18 @@ pub fn build_ctor_params_metadata<'a>( )) }); - map_entries.push(LiteralMapEntry { - key: Ident::from("type"), - value: type_expr, - quoted: false, - }); + map_entries.push(LiteralMapEntry::new(Ident::from("type"), type_expr, false)); // Extract decorators from the parameter let param_decorators = extract_angular_decorators_from_param(param); if !param_decorators.is_empty() { let decorators_array = build_decorator_metadata_array(allocator, ¶m_decorators, source_text); - map_entries.push(LiteralMapEntry { - key: Ident::from("decorators"), - value: decorators_array, - quoted: false, - }); + map_entries.push(LiteralMapEntry::new( + Ident::from("decorators"), + decorators_array, + false, + )); } param_entries.push(OutputExpression::LiteralMap(Box::new_in( @@ -258,11 +250,7 @@ pub fn build_prop_decorators_metadata<'a>( let decorators_array = build_decorator_metadata_array(allocator, &angular_decorators, source_text); - prop_entries.push(LiteralMapEntry { - key: prop_name, - value: decorators_array, - quoted: false, - }); + prop_entries.push(LiteralMapEntry::new(prop_name, decorators_array, false)); } if prop_entries.is_empty() { diff --git a/crates/oxc_angular_compiler/src/component/definition.rs b/crates/oxc_angular_compiler/src/component/definition.rs index 034ab1e7d..908a035c5 100644 --- a/crates/oxc_angular_compiler/src/component/definition.rs +++ b/crates/oxc_angular_compiler/src/component/definition.rs @@ -128,14 +128,14 @@ fn generate_cmp_definition<'a>( // ========================================================================= // 1. type: ComponentClass - entries.push(LiteralMapEntry { - key: Ident::from("type"), - value: OutputExpression::ReadVar(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("type"), + OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: metadata.class_name.clone(), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); // 2. selectors: [["selector"]] or [["ng-component"]] if no selector // Angular uses "ng-component" as the default selector for components without an explicit selector. @@ -144,22 +144,14 @@ fn generate_cmp_definition<'a>( let selector_value = metadata.selector.as_ref().map_or_else(|| Ident::from("ng-component"), |s| s.clone()); let selector_entries = parse_selector_to_array(allocator, &selector_value); - entries.push(LiteralMapEntry { - key: Ident::from("selectors"), - value: selector_entries, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("selectors"), selector_entries, false)); // 3. contentQueries: function(rf, ctx, dirIndex) { ... } (if any) // This handles @ContentChild/@ContentChildren decorators and signal-based queries // (contentChild(), contentChildren()). // Per Angular compiler.ts lines 57-63 (baseDirectiveFields) if let Some(content_queries) = content_queries_fn { - entries.push(LiteralMapEntry { - key: Ident::from("contentQueries"), - value: content_queries, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("contentQueries"), content_queries, false)); } // 4. viewQuery: function(rf, ctx) { ... } (if any) @@ -167,11 +159,7 @@ fn generate_cmp_definition<'a>( // The predicate arrays are pre-pooled to ensure correct constant ordering. // Per Angular compiler.ts lines 65-70 (baseDirectiveFields) if let Some(view_query) = view_query_fn { - entries.push(LiteralMapEntry { - key: Ident::from("viewQuery"), - value: view_query, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("viewQuery"), view_query, false)); } // 5-7. Host binding fields (hostAttrs, hostVars, hostBindings) @@ -181,35 +169,31 @@ fn generate_cmp_definition<'a>( if let Some(host_result) = host_binding_result { // 5. hostAttrs: [...] - static host attributes if let Some(host_attrs) = host_result.host_attrs { - entries.push(LiteralMapEntry { - key: Ident::from("hostAttrs"), - value: host_attrs, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("hostAttrs"), host_attrs, false)); } // 6. hostVars: number - only if > 0 if let Some(host_vars) = host_result.host_vars { - entries.push(LiteralMapEntry { - key: Ident::from("hostVars"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("hostVars"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Number(host_vars as f64), source_span: None, }, allocator, )), - quoted: false, - }); + false, + )); } // 7. hostBindings: function(rf, ctx) { ... } (if any) if let Some(host_fn) = host_result.host_binding_fn { - entries.push(LiteralMapEntry { - key: Ident::from("hostBindings"), - value: OutputExpression::Function(Box::new_in(host_fn, allocator)), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("hostBindings"), + OutputExpression::Function(Box::new_in(host_fn, allocator)), + false, + )); } } @@ -217,11 +201,7 @@ fn generate_cmp_definition<'a>( // Per Angular compiler.ts lines 86-87 (baseDirectiveFields) if !metadata.inputs.is_empty() { if let Some(inputs_expr) = create_inputs_literal(allocator, &metadata.inputs) { - entries.push(LiteralMapEntry { - key: Ident::from("inputs"), - value: inputs_expr, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("inputs"), inputs_expr, false)); } } @@ -229,11 +209,7 @@ fn generate_cmp_definition<'a>( // Per Angular compiler.ts lines 89-90 (baseDirectiveFields) if !metadata.outputs.is_empty() { if let Some(outputs_expr) = create_outputs_literal(allocator, &metadata.outputs) { - entries.push(LiteralMapEntry { - key: Ident::from("outputs"), - value: outputs_expr, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("outputs"), outputs_expr, false)); } } @@ -247,40 +223,40 @@ fn generate_cmp_definition<'a>( allocator, ))); } - entries.push(LiteralMapEntry { - key: Ident::from("exportAs"), - value: OutputExpression::LiteralArray(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("exportAs"), + OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries: export_items, source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } // 11. standalone: false - only emit when NOT standalone (true is the default in Angular v17+) // Per Angular compiler.ts lines 96-98 (baseDirectiveFields) if !metadata.standalone { - entries.push(LiteralMapEntry { - key: Ident::from("standalone"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("standalone"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Boolean(false), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } // 12. signals: true (if isSignal) // Per Angular compiler.ts lines 99-101 (baseDirectiveFields) if metadata.is_signal { - entries.push(LiteralMapEntry { - key: Ident::from("signals"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("signals"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Boolean(true), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } // ========================================================================= @@ -290,11 +266,7 @@ fn generate_cmp_definition<'a>( // 13. features: [...] - component features like providers, lifecycle hooks, inheritance // See: packages/compiler/src/render3/view/compiler.ts:119-161 if let Some(features) = generate_features_array(allocator, metadata, namespace_registry) { - entries.push(LiteralMapEntry { - key: Ident::from("features"), - value: features, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("features"), features, false)); } // ========================================================================= @@ -307,42 +279,42 @@ fn generate_cmp_definition<'a>( // The attrs_ref is pre-pooled BEFORE template compilation to ensure correct constant ordering. // TypeScript Angular adds attrs to the pool BEFORE template ingestion/compilation. if let Some(attrs) = attrs_ref { - entries.push(LiteralMapEntry { key: Ident::from("attrs"), value: attrs, quoted: false }); + entries.push(LiteralMapEntry::new(Ident::from("attrs"), attrs, false)); } // 15. ngContentSelectors: [...] - content projection selectors // Per Angular compiler.ts lines 254-256 if let Some(content_selectors) = job.content_selectors.take() { - entries.push(LiteralMapEntry { - key: Ident::from("ngContentSelectors"), - value: content_selectors, - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("ngContentSelectors"), + content_selectors, + false, + )); } // 16. decls: number (from compilation) // Per Angular compiler.ts line 258 let decls = job.root.decl_count.unwrap_or(0); - entries.push(LiteralMapEntry { - key: Ident::from("decls"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("decls"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Number(decls as f64), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); // 17. vars: number (from compilation) // Per Angular compiler.ts line 259 let vars = job.root.vars.unwrap_or(0); - entries.push(LiteralMapEntry { - key: Ident::from("vars"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("vars"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Number(vars as f64), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); // 18. consts: [...] or consts: function() { ...initializers...; return [...]; } // Per Angular compiler.ts lines 260-268: @@ -394,31 +366,23 @@ fn generate_cmp_definition<'a>( )) }; - entries.push(LiteralMapEntry { - key: Ident::from("consts"), - value: consts_value, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("consts"), consts_value, false)); } // 19. template: function(rf, ctx) { ... } // Per Angular compiler.ts line 270 - entries.push(LiteralMapEntry { - key: Ident::from("template"), - value: OutputExpression::Function(Box::new_in(template_fn, allocator)), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("template"), + OutputExpression::Function(Box::new_in(template_fn, allocator)), + false, + )); // 20. dependencies: [...] - template dependencies (directives and pipes) // Per Angular compiler.ts lines 272-289 if let Some(dependencies) = generate_dependencies_expression(allocator, metadata, namespace_registry) { - entries.push(LiteralMapEntry { - key: Ident::from("dependencies"), - value: dependencies, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("dependencies"), dependencies, false)); } // 21. styles: [...] @@ -459,14 +423,14 @@ fn generate_cmp_definition<'a>( if !style_entries.is_empty() { has_styles = true; - entries.push(LiteralMapEntry { - key: Ident::from("styles"), - value: OutputExpression::LiteralArray(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("styles"), + OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries: style_entries, source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } } @@ -485,17 +449,17 @@ fn generate_cmp_definition<'a>( ViewEncapsulation::None => 2, ViewEncapsulation::ShadowDom => 3, }; - entries.push(LiteralMapEntry { - key: Ident::from("encapsulation"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("encapsulation"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Number(encapsulation_value as f64), source_span: None, }, allocator, )), - quoted: false, - }); + false, + )); } // 23. data: {animation: [...]} - animation triggers @@ -504,21 +468,20 @@ fn generate_cmp_definition<'a>( // Create the inner map: {animation: animationsExpr} let mut data_entries: OxcVec<'a, LiteralMapEntry<'a>> = OxcVec::with_capacity_in(1, allocator); - data_entries.push(LiteralMapEntry { - key: Ident::from("animation"), - // Use the full animations expression directly - value: animations.clone_in(allocator), - quoted: false, - }); - - entries.push(LiteralMapEntry { - key: Ident::from("data"), - value: OutputExpression::LiteralMap(Box::new_in( + data_entries.push(LiteralMapEntry::new( + Ident::from("animation"), + animations.clone_in(allocator), + false, + )); + + entries.push(LiteralMapEntry::new( + Ident::from("data"), + OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries: data_entries, source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } // 24. changeDetection: ChangeDetectionStrategy.OnPush - only emit if not Default @@ -549,11 +512,11 @@ fn generate_cmp_definition<'a>( }, allocator, )); - entries.push(LiteralMapEntry { - key: Ident::from("changeDetection"), - value: strategy_value_expr, - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("changeDetection"), + strategy_value_expr, + false, + )); } // Create the config object @@ -1193,32 +1156,20 @@ fn create_host_directives_arg<'a>( let mut entries: OxcVec<'a, LiteralMapEntry<'a>> = OxcVec::new_in(allocator); // directive: DirectiveClass (or i1.DirectiveClass for imports) - entries.push(LiteralMapEntry { - key: Ident::from("directive"), - value: directive_ref, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("directive"), directive_ref, false)); // inputs: ['internalName', 'publicName', ...] if !directive.inputs.is_empty() { let inputs_array = create_host_directive_mappings_array(allocator, &directive.inputs); - entries.push(LiteralMapEntry { - key: Ident::from("inputs"), - value: inputs_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("inputs"), inputs_array, false)); } // outputs: ['internalName', 'publicName', ...] if !directive.outputs.is_empty() { let outputs_array = create_host_directive_mappings_array(allocator, &directive.outputs); - entries.push(LiteralMapEntry { - key: Ident::from("outputs"), - value: outputs_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("outputs"), outputs_array, false)); } expressions.push(OutputExpression::LiteralMap(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/directive/compiler.rs b/crates/oxc_angular_compiler/src/directive/compiler.rs index 6e9957750..3fbb1b29f 100644 --- a/crates/oxc_angular_compiler/src/directive/compiler.rs +++ b/crates/oxc_angular_compiler/src/directive/compiler.rs @@ -108,20 +108,16 @@ fn build_base_directive_fields<'a>( let mut host_declarations = oxc_allocator::Vec::new_in(allocator); // type: MyDirective - entries.push(LiteralMapEntry { - key: Ident::from("type"), - value: metadata.r#type.clone_in(allocator), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("type"), + metadata.r#type.clone_in(allocator), + false, + )); // selectors: [['', 'myDir', '']] if let Some(selector) = &metadata.selector { if let Some(selectors_expr) = parse_selector_to_r3_selector(allocator, selector) { - entries.push(LiteralMapEntry { - key: Ident::from("selectors"), - value: selectors_expr, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("selectors"), selectors_expr, false)); } } @@ -135,11 +131,11 @@ fn build_base_directive_fields<'a>( Some(metadata.name.as_str()), None, ); - entries.push(LiteralMapEntry { - key: Ident::from("contentQueries"), - value: content_queries_fn, - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("contentQueries"), + content_queries_fn, + false, + )); } // viewQuery: (rf, ctx) => { ... } @@ -152,11 +148,7 @@ fn build_base_directive_fields<'a>( Some(metadata.name.as_str()), None, ); - entries.push(LiteralMapEntry { - key: Ident::from("viewQuery"), - value: view_queries_fn, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("viewQuery"), view_queries_fn, false)); } // hostBindings: (rf, ctx) => { ... } @@ -175,35 +167,31 @@ fn build_base_directive_fields<'a>( // Note: Property/TwoWayProperty bindings are excluded from hostAttrs // as they are dynamic bindings handled by hostBindings function if let Some(host_attrs) = result.host_attrs { - entries.push(LiteralMapEntry { - key: Ident::from("hostAttrs"), - value: host_attrs, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("hostAttrs"), host_attrs, false)); } // hostVars: number - only if > 0 if let Some(host_vars) = result.host_vars { - entries.push(LiteralMapEntry { - key: Ident::from("hostVars"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("hostVars"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Number(host_vars as f64), source_span: None, }, allocator, )), - quoted: false, - }); + false, + )); } // hostBindings: function(rf, ctx) { ... } if let Some(host_fn) = result.host_binding_fn { - entries.push(LiteralMapEntry { - key: Ident::from("hostBindings"), - value: OutputExpression::Function(Box::new_in(host_fn, allocator)), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("hostBindings"), + OutputExpression::Function(Box::new_in(host_fn, allocator)), + false, + )); } // Collect host binding pool declarations (pure functions, etc.) @@ -214,22 +202,14 @@ fn build_base_directive_fields<'a>( // inputs: { prop: 'prop', aliased: ['publicName', 'privateField'] } if !metadata.inputs.is_empty() { if let Some(inputs_expr) = create_inputs_literal(allocator, &metadata.inputs) { - entries.push(LiteralMapEntry { - key: Ident::from("inputs"), - value: inputs_expr, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("inputs"), inputs_expr, false)); } } // outputs: { click: 'click' } if !metadata.outputs.is_empty() { if let Some(outputs_expr) = create_outputs_literal(allocator, &metadata.outputs) { - entries.push(LiteralMapEntry { - key: Ident::from("outputs"), - value: outputs_expr, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("outputs"), outputs_expr, false)); } } @@ -242,38 +222,38 @@ fn build_base_directive_fields<'a>( allocator, ))); } - entries.push(LiteralMapEntry { - key: Ident::from("exportAs"), - value: OutputExpression::LiteralArray(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("exportAs"), + OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries: export_items, source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } // standalone: false (only if not standalone, since true is default) if !metadata.is_standalone { - entries.push(LiteralMapEntry { - key: Ident::from("standalone"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("standalone"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Boolean(false), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } // signals: true (only if signal-based) if metadata.is_signal { - entries.push(LiteralMapEntry { - key: Ident::from("signals"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("signals"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Boolean(true), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } (entries, next_pool_index, host_declarations) @@ -316,14 +296,14 @@ fn add_features<'a>( } if !features.is_empty() { - definition_map.push(LiteralMapEntry { - key: Ident::from("features"), - value: OutputExpression::LiteralArray(Box::new_in( + definition_map.push(LiteralMapEntry::new( + Ident::from("features"), + OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries: features, source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } } @@ -521,7 +501,7 @@ pub fn create_inputs_literal<'a>( }; let quoted = needs_object_key_quoting(declared_name); - entries.push(LiteralMapEntry { key: declared_name.clone(), value, quoted }); + entries.push(LiteralMapEntry::new(declared_name.clone(), value, quoted)); } Some(OutputExpression::LiteralMap(Box::new_in( @@ -543,9 +523,9 @@ pub fn create_outputs_literal<'a>( for (class_name, binding_name) in outputs { let quoted = needs_object_key_quoting(class_name); - entries.push(LiteralMapEntry { - key: class_name.clone(), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + class_name.clone(), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::String(binding_name.clone()), source_span: None, @@ -553,7 +533,7 @@ pub fn create_outputs_literal<'a>( allocator, )), quoted, - }); + )); } Some(OutputExpression::LiteralMap(Box::new_in( @@ -934,30 +914,18 @@ fn create_host_directives_feature_arg<'a>( hd.directive.clone_in(allocator) }; - entries.push(LiteralMapEntry { - key: Ident::from("directive"), - value: directive_expr, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("directive"), directive_expr, false)); // inputs (if any) if !hd.inputs.is_empty() { let inputs_array = create_host_directive_mappings_array(allocator, &hd.inputs); - entries.push(LiteralMapEntry { - key: Ident::from("inputs"), - value: inputs_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("inputs"), inputs_array, false)); } // outputs (if any) if !hd.outputs.is_empty() { let outputs_array = create_host_directive_mappings_array(allocator, &hd.outputs); - entries.push(LiteralMapEntry { - key: Ident::from("outputs"), - value: outputs_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("outputs"), outputs_array, false)); } items.push(OutputExpression::LiteralMap(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/injectable/compiler.rs b/crates/oxc_angular_compiler/src/injectable/compiler.rs index 49b63ab66..eafea83b0 100644 --- a/crates/oxc_angular_compiler/src/injectable/compiler.rs +++ b/crates/oxc_angular_compiler/src/injectable/compiler.rs @@ -404,66 +404,62 @@ fn build_definition_map<'a>( let mut entries = Vec::new_in(allocator); // token: MyService - entries.push(LiteralMapEntry { - key: Ident::from("token"), - value: metadata.r#type.clone_in(allocator), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("token"), + metadata.r#type.clone_in(allocator), + false, + )); // factory: - entries.push(LiteralMapEntry { - key: Ident::from("factory"), - value: factory_expr, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("factory"), factory_expr, false)); // providedIn: 'root' (only if not None) match &metadata.provided_in { ProvidedIn::Root => { - entries.push(LiteralMapEntry { - key: Ident::from("providedIn"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("providedIn"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::String(Ident::from("root")), source_span: None, }, allocator, )), - quoted: false, - }); + false, + )); } ProvidedIn::Platform => { - entries.push(LiteralMapEntry { - key: Ident::from("providedIn"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("providedIn"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::String(Ident::from("platform")), source_span: None, }, allocator, )), - quoted: false, - }); + false, + )); } ProvidedIn::Any => { - entries.push(LiteralMapEntry { - key: Ident::from("providedIn"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("providedIn"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::String(Ident::from("any")), source_span: None, }, allocator, )), - quoted: false, - }); + false, + )); } ProvidedIn::Module(module_expr) => { - entries.push(LiteralMapEntry { - key: Ident::from("providedIn"), - value: module_expr.clone_in(allocator), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("providedIn"), + module_expr.clone_in(allocator), + false, + )); } ProvidedIn::None => { // Don't add providedIn field diff --git a/crates/oxc_angular_compiler/src/injector/compiler.rs b/crates/oxc_angular_compiler/src/injector/compiler.rs index 63062135d..5651d191f 100644 --- a/crates/oxc_angular_compiler/src/injector/compiler.rs +++ b/crates/oxc_angular_compiler/src/injector/compiler.rs @@ -63,11 +63,11 @@ fn build_definition_map<'a>( // providers: [...] (only if present) if let Some(providers) = &metadata.providers { - entries.push(LiteralMapEntry { - key: Ident::from("providers"), - value: providers.clone_in(allocator), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("providers"), + providers.clone_in(allocator), + false, + )); } // imports: [...] (only if non-empty) @@ -86,11 +86,7 @@ fn build_definition_map<'a>( )) }; - entries.push(LiteralMapEntry { - key: Ident::from("imports"), - value: imports_value, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("imports"), imports_value, false)); } entries diff --git a/crates/oxc_angular_compiler/src/ir/expression.rs b/crates/oxc_angular_compiler/src/ir/expression.rs index d63fb20ae..e5fc56e16 100644 --- a/crates/oxc_angular_compiler/src/ir/expression.rs +++ b/crates/oxc_angular_compiler/src/ir/expression.rs @@ -643,8 +643,12 @@ impl<'a> IrExpression<'a> { for entry in e.entries.iter() { entries.push(entry.clone_in(allocator)); } + let mut spreads = Vec::with_capacity_in(e.spreads.len(), allocator); + for s in e.spreads.iter() { + spreads.push(*s); + } IrExpression::DerivedLiteralArray(Box::new_in( - DerivedLiteralArrayExpr { entries, source_span: e.source_span }, + DerivedLiteralArrayExpr { entries, spreads, source_span: e.source_span }, allocator, )) } @@ -661,8 +665,18 @@ impl<'a> IrExpression<'a> { for q in e.quoted.iter() { quoted.push(*q); } + let mut spreads = Vec::with_capacity_in(e.spreads.len(), allocator); + for s in e.spreads.iter() { + spreads.push(*s); + } IrExpression::DerivedLiteralMap(Box::new_in( - DerivedLiteralMapExpr { keys, values, quoted, source_span: e.source_span }, + DerivedLiteralMapExpr { + keys, + values, + quoted, + spreads, + source_span: e.source_span, + }, allocator, )) } @@ -671,8 +685,12 @@ impl<'a> IrExpression<'a> { for elem in e.elements.iter() { elements.push(elem.clone_in(allocator)); } + let mut spreads = Vec::with_capacity_in(e.spreads.len(), allocator); + for s in e.spreads.iter() { + spreads.push(*s); + } IrExpression::LiteralArray(Box::new_in( - IrLiteralArrayExpr { elements, source_span: e.source_span }, + IrLiteralArrayExpr { elements, spreads, source_span: e.source_span }, allocator, )) } @@ -689,8 +707,12 @@ impl<'a> IrExpression<'a> { for q in e.quoted.iter() { quoted.push(*q); } + let mut spreads = Vec::with_capacity_in(e.spreads.len(), allocator); + for s in e.spreads.iter() { + spreads.push(*s); + } IrExpression::LiteralMap(Box::new_in( - IrLiteralMapExpr { keys, values, quoted, source_span: e.source_span }, + IrLiteralMapExpr { keys, values, quoted, spreads, source_span: e.source_span }, allocator, )) } @@ -998,6 +1020,8 @@ pub struct IrTemplateLiteralElement<'a> { pub struct DerivedLiteralArrayExpr<'a> { /// Array entries - can be Ast (constants) or PureFunctionParameter (refs). pub entries: Vec<'a, IrExpression<'a>>, + /// Whether each entry is a spread element (parallel to entries). + pub spreads: Vec<'a, bool>, /// Source span. pub source_span: Option, } @@ -1013,6 +1037,8 @@ pub struct DerivedLiteralMapExpr<'a> { pub values: Vec<'a, IrExpression<'a>>, /// Whether each key is quoted. pub quoted: Vec<'a, bool>, + /// Whether each entry is a spread (parallel to keys/values/quoted). + pub spreads: Vec<'a, bool>, /// Source span. pub source_span: Option, } @@ -1023,6 +1049,8 @@ pub struct DerivedLiteralMapExpr<'a> { pub struct IrLiteralArrayExpr<'a> { /// Array elements as IR expressions. pub elements: Vec<'a, IrExpression<'a>>, + /// Whether each element is a spread element (parallel to elements). + pub spreads: Vec<'a, bool>, /// Source span. pub source_span: Option, } @@ -1037,6 +1065,8 @@ pub struct IrLiteralMapExpr<'a> { pub values: Vec<'a, IrExpression<'a>>, /// Whether each key is quoted. pub quoted: Vec<'a, bool>, + /// Whether each entry is a spread (parallel to keys/values/quoted). + pub spreads: Vec<'a, bool>, /// Source span. pub source_span: Option, } diff --git a/crates/oxc_angular_compiler/src/ng_module/compiler.rs b/crates/oxc_angular_compiler/src/ng_module/compiler.rs index dffae683d..fe59e92df 100644 --- a/crates/oxc_angular_compiler/src/ng_module/compiler.rs +++ b/crates/oxc_angular_compiler/src/ng_module/compiler.rs @@ -81,21 +81,17 @@ fn build_definition_map<'a>( let mut entries = Vec::new_in(allocator); // type: ModuleClass - entries.push(LiteralMapEntry { - key: Ident::from("type"), - value: metadata.r#type.value.clone_in(allocator), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("type"), + metadata.r#type.value.clone_in(allocator), + false, + )); // bootstrap: [ComponentClass, ...] if metadata.has_bootstrap() { let bootstrap_array = create_reference_array(allocator, &metadata.bootstrap, metadata.contains_forward_decls); - entries.push(LiteralMapEntry { - key: Ident::from("bootstrap"), - value: bootstrap_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("bootstrap"), bootstrap_array, false)); } // Inline scope if mode is Inline @@ -107,11 +103,11 @@ fn build_definition_map<'a>( &metadata.declarations, metadata.contains_forward_decls, ); - entries.push(LiteralMapEntry { - key: Ident::from("declarations"), - value: declarations_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("declarations"), + declarations_array, + false, + )); } // imports: [ImportedModule, ...] @@ -121,11 +117,7 @@ fn build_definition_map<'a>( &metadata.imports, metadata.contains_forward_decls, ); - entries.push(LiteralMapEntry { - key: Ident::from("imports"), - value: imports_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("imports"), imports_array, false)); } // exports: [ExportedClass, ...] @@ -135,11 +127,7 @@ fn build_definition_map<'a>( &metadata.exports, metadata.contains_forward_decls, ); - entries.push(LiteralMapEntry { - key: Ident::from("exports"), - value: exports_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("exports"), exports_array, false)); } } @@ -147,20 +135,12 @@ fn build_definition_map<'a>( if metadata.has_schemas() { let schemas_array = create_reference_array(allocator, &metadata.schemas, metadata.contains_forward_decls); - entries.push(LiteralMapEntry { - key: Ident::from("schemas"), - value: schemas_array, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("schemas"), schemas_array, false)); } // id: 'unique-module-id' if let Some(id) = &metadata.id { - entries.push(LiteralMapEntry { - key: Ident::from("id"), - value: id.clone_in(allocator), - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("id"), id.clone_in(allocator), false)); } entries @@ -276,31 +256,19 @@ fn create_set_scope_side_effect<'a>( &metadata.declarations, metadata.contains_forward_decls, ); - scope_entries.push(LiteralMapEntry { - key: Ident::from("declarations"), - value: decls, - quoted: false, - }); + scope_entries.push(LiteralMapEntry::new(Ident::from("declarations"), decls, false)); } if metadata.has_imports() { let imports = create_reference_array(allocator, &metadata.imports, metadata.contains_forward_decls); - scope_entries.push(LiteralMapEntry { - key: Ident::from("imports"), - value: imports, - quoted: false, - }); + scope_entries.push(LiteralMapEntry::new(Ident::from("imports"), imports, false)); } if metadata.has_exports() { let exports = create_reference_array(allocator, &metadata.exports, metadata.contains_forward_decls); - scope_entries.push(LiteralMapEntry { - key: Ident::from("exports"), - value: exports, - quoted: false, - }); + scope_entries.push(LiteralMapEntry::new(Ident::from("exports"), exports, false)); } let scope_map = OutputExpression::LiteralMap(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/output/ast.rs b/crates/oxc_angular_compiler/src/output/ast.rs index de22b8ef1..08b3d8ff1 100644 --- a/crates/oxc_angular_compiler/src/output/ast.rs +++ b/crates/oxc_angular_compiler/src/output/ast.rs @@ -505,12 +505,20 @@ pub struct LiteralArrayExpr<'a> { /// Object literal entry. #[derive(Debug)] pub struct LiteralMapEntry<'a> { - /// Property key. pub key: Ident<'a>, - /// Property value. pub value: OutputExpression<'a>, - /// Whether the key is quoted. pub quoted: bool, + pub is_spread: bool, +} + +impl<'a> LiteralMapEntry<'a> { + pub fn new(key: Ident<'a>, value: OutputExpression<'a>, quoted: bool) -> Self { + Self { key, value, quoted, is_spread: false } + } + + pub fn spread(value: OutputExpression<'a>) -> Self { + Self { key: Ident::from(""), value, quoted: false, is_spread: true } + } } /// Object literal expression. @@ -1129,6 +1137,7 @@ impl<'a> OutputExpression<'a> { key: entry.key.clone(), value: entry.value.clone_in(allocator), quoted: entry.quoted, + is_spread: entry.is_spread, }); } OutputExpression::LiteralMap(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/output/emitter.rs b/crates/oxc_angular_compiler/src/output/emitter.rs index ff124af8d..2b0df6f95 100644 --- a/crates/oxc_angular_compiler/src/output/emitter.rs +++ b/crates/oxc_angular_compiler/src/output/emitter.rs @@ -953,10 +953,16 @@ impl JsEmitter { ctx.print(","); } } - let key = escape_identifier(&entry.key, self.escape_dollar_in_strings, entry.quoted); - ctx.print(&key); - ctx.print(":"); - self.visit_expression(&entry.value, ctx); + if entry.is_spread { + ctx.print("..."); + self.visit_expression(&entry.value, ctx); + } else { + let key = + escape_identifier(&entry.key, self.escape_dollar_in_strings, entry.quoted); + ctx.print(&key); + ctx.print(":"); + self.visit_expression(&entry.value, ctx); + } } if incremented_indent { ctx.dec_indent(); @@ -2660,11 +2666,7 @@ mod tests { )); let mut entries = oxc_allocator::Vec::new_in(&alloc); - entries.push(LiteralMapEntry { - key: Ident::from("showMenu"), - value: signal_call, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("showMenu"), signal_call, false)); let obj_literal = OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries, source_span: None }, @@ -2725,11 +2727,7 @@ mod tests { )); let mut entries = oxc_allocator::Vec::new_in(&alloc); - entries.push(LiteralMapEntry { - key: Ident::from("showMenu"), - value: signal_call, - quoted: false, - }); + entries.push(LiteralMapEntry::new(Ident::from("showMenu"), signal_call, false)); let obj_literal = OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries, source_span: None }, diff --git a/crates/oxc_angular_compiler/src/output/oxc_converter.rs b/crates/oxc_angular_compiler/src/output/oxc_converter.rs index bd297d6ab..4a085d91e 100644 --- a/crates/oxc_angular_compiler/src/output/oxc_converter.rs +++ b/crates/oxc_angular_compiler/src/output/oxc_converter.rs @@ -324,12 +324,11 @@ fn convert_object_expression<'a>( // Convert the value let value = convert_oxc_expression(allocator, &p.value, source_text)?; - entries.push(LiteralMapEntry { key, value, quoted }); + entries.push(LiteralMapEntry::new(key, value, quoted)); } - ObjectPropertyKind::SpreadProperty(_) => { - // Spread properties are not directly supported in LiteralMap - // Skip for now - continue; + ObjectPropertyKind::SpreadProperty(spread) => { + let value = convert_oxc_expression(allocator, &spread.argument, source_text)?; + entries.push(LiteralMapEntry::spread(value)); } } } @@ -913,6 +912,28 @@ mod tests { } } + #[test] + fn test_convert_spread_in_object() { + // SpreadProperty in object expressions was previously skipped (// Skip for now). + // Now that LiteralMapEntry::spread() exists, it should be preserved. + let allocator = Allocator::default(); + let expr = parse_expression(&allocator, "{ ...base, key: 'val' }"); + let result = convert_oxc_expression(&allocator, &expr, None); + assert!(result.is_some(), "Expected Some result"); + if let Some(OutputExpression::LiteralMap(map)) = result { + assert_eq!(map.entries.len(), 2, "Expected 2 entries (spread + property)"); + // First entry should be a spread + let first = &map.entries[0]; + assert!(first.is_spread, "Expected first entry to be a spread"); + // Second entry should be a regular property + let second = &map.entries[1]; + assert!(!second.is_spread, "Expected second entry to be a regular property"); + assert_eq!(second.key.as_str(), "key"); + } else { + panic!("Expected LiteralMap expression, got: {:?}", result); + } + } + #[test] fn test_convert_optional_chaining_property() { let allocator = Allocator::default(); diff --git a/crates/oxc_angular_compiler/src/pipe/compiler.rs b/crates/oxc_angular_compiler/src/pipe/compiler.rs index a260af04f..f6df09aaf 100644 --- a/crates/oxc_angular_compiler/src/pipe/compiler.rs +++ b/crates/oxc_angular_compiler/src/pipe/compiler.rs @@ -64,42 +64,42 @@ fn build_definition_map<'a>( // name: literal(metadata.pipeName ?? metadata.name) let pipe_name = metadata.pipe_name.clone().unwrap_or_else(|| metadata.name.clone()); - entries.push(LiteralMapEntry { - key: Ident::from("name"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("name"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::String(pipe_name), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); // type: metadata.type.value - entries.push(LiteralMapEntry { - key: Ident::from("type"), - value: metadata.r#type.clone_in(allocator), - quoted: false, - }); + entries.push(LiteralMapEntry::new( + Ident::from("type"), + metadata.r#type.clone_in(allocator), + false, + )); // pure: literal(metadata.pure) - entries.push(LiteralMapEntry { - key: Ident::from("pure"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("pure"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Boolean(metadata.pure), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); // standalone: only included if false (Angular's runtime defaults standalone to true) if !metadata.is_standalone { - entries.push(LiteralMapEntry { - key: Ident::from("standalone"), - value: OutputExpression::Literal(Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from("standalone"), + OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Boolean(false), source_span: None }, allocator, )), - quoted: false, - }); + false, + )); } entries diff --git a/crates/oxc_angular_compiler/src/pipeline/conversion.rs b/crates/oxc_angular_compiler/src/pipeline/conversion.rs index 0a43fcb71..0ddd0240e 100644 --- a/crates/oxc_angular_compiler/src/pipeline/conversion.rs +++ b/crates/oxc_angular_compiler/src/pipeline/conversion.rs @@ -506,15 +506,18 @@ pub fn convert_ast<'a>( let mut entries = Vec::with_capacity_in(map.keys.len(), allocator); for (key, value) in map.keys.iter().zip(map.values.iter()) { let converted_value = convert_ast(allocator, value, root_xref, allocate_xref_id); - // Only handle property keys for now; spread keys need special handling - if let LiteralMapKey::Property(prop) = key { - entries.push(LiteralMapEntry { - key: prop.key.clone(), - value: converted_value.to_output(allocator), - quoted: prop.quoted, - }); + match key { + LiteralMapKey::Property(prop) => { + entries.push(LiteralMapEntry::new( + prop.key.clone(), + converted_value.to_output(allocator), + prop.quoted, + )); + } + LiteralMapKey::Spread(_) => { + entries.push(LiteralMapEntry::spread(converted_value.to_output(allocator))); + } } - // TODO: Handle spread keys when needed } ConvertedExpression::output(OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries, source_span: convert_source_span(map.source_span) }, diff --git a/crates/oxc_angular_compiler/src/pipeline/emit.rs b/crates/oxc_angular_compiler/src/pipeline/emit.rs index 57aa06c22..24103bee5 100644 --- a/crates/oxc_angular_compiler/src/pipeline/emit.rs +++ b/crates/oxc_angular_compiler/src/pipeline/emit.rs @@ -1143,8 +1143,19 @@ fn convert_pure_function_body<'a>( // DerivedLiteralArray: convert to a literal array with nested conversions IrExpression::DerivedLiteralArray(arr) => { let mut entries = OxcVec::with_capacity_in(arr.entries.len(), allocator); - for entry in arr.entries.iter() { - entries.push(convert_pure_function_body(allocator, entry, params)); + for (i, entry) in arr.entries.iter().enumerate() { + let converted = convert_pure_function_body(allocator, entry, params); + if arr.spreads.get(i).copied().unwrap_or(false) { + entries.push(OutputExpression::SpreadElement(Box::new_in( + SpreadElementExpr { + expr: Box::new_in(converted, allocator), + source_span: None, + }, + allocator, + ))); + } else { + entries.push(converted); + } } OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries, source_span: None }, @@ -1159,7 +1170,8 @@ fn convert_pure_function_body<'a>( let key = map.keys[i].clone(); let value = convert_pure_function_body(allocator, &map.values[i], params); let quoted = map.quoted.get(i).copied().unwrap_or(false); - entries.push(LiteralMapEntry { key, value, quoted }); + let is_spread = map.spreads.get(i).copied().unwrap_or(false); + entries.push(LiteralMapEntry { key, value, quoted, is_spread }); } OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries, source_span: None }, @@ -1170,8 +1182,19 @@ fn convert_pure_function_body<'a>( // LiteralArray: convert to a literal array with nested conversions IrExpression::LiteralArray(arr) => { let mut entries = OxcVec::with_capacity_in(arr.elements.len(), allocator); - for elem in arr.elements.iter() { - entries.push(convert_pure_function_body(allocator, elem, params)); + for (i, elem) in arr.elements.iter().enumerate() { + let converted = convert_pure_function_body(allocator, elem, params); + if arr.spreads.get(i).copied().unwrap_or(false) { + entries.push(OutputExpression::SpreadElement(Box::new_in( + SpreadElementExpr { + expr: Box::new_in(converted, allocator), + source_span: None, + }, + allocator, + ))); + } else { + entries.push(converted); + } } OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries, source_span: None }, @@ -1186,7 +1209,8 @@ fn convert_pure_function_body<'a>( let key = map.keys[i].clone(); let value = convert_pure_function_body(allocator, &map.values[i], params); let quoted = map.quoted.get(i).copied().unwrap_or(false); - entries.push(LiteralMapEntry { key, value, quoted }); + let is_spread = map.spreads.get(i).copied().unwrap_or(false); + entries.push(LiteralMapEntry { key, value, quoted, is_spread }); } OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries, source_span: None }, @@ -1318,7 +1342,9 @@ fn convert_ast_for_pure_function_body<'a>( params: &[Ident<'a>], ) -> OutputExpression<'a> { use crate::ast::expression::{AngularExpression, LiteralMapKey}; - use crate::output::ast::{LiteralArrayExpr, LiteralMapEntry, LiteralMapExpr}; + use crate::output::ast::{ + LiteralArrayExpr, LiteralMapEntry, LiteralMapExpr, SpreadElementExpr, + }; match ast { AngularExpression::LiteralPrimitive(lit) => { @@ -1337,7 +1363,19 @@ fn convert_ast_for_pure_function_body<'a>( AngularExpression::LiteralArray(arr) => { let mut entries = OxcVec::with_capacity_in(arr.expressions.len(), allocator); for entry in arr.expressions.iter() { - entries.push(convert_ast_for_pure_function_body(allocator, entry, params)); + if let AngularExpression::SpreadElement(spread) = entry { + let inner = + convert_ast_for_pure_function_body(allocator, &spread.expression, params); + entries.push(OutputExpression::SpreadElement(Box::new_in( + SpreadElementExpr { + expr: Box::new_in(inner, allocator), + source_span: None, + }, + allocator, + ))); + } else { + entries.push(convert_ast_for_pure_function_body(allocator, entry, params)); + } } OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries, source_span: None }, @@ -1347,18 +1385,21 @@ fn convert_ast_for_pure_function_body<'a>( AngularExpression::LiteralMap(map) => { let mut entries = OxcVec::with_capacity_in(map.keys.len(), allocator); for (i, key) in map.keys.iter().enumerate() { - // Only handle property keys; skip spread keys - if let LiteralMapKey::Property(prop) = key { - let key_value = prop.key.clone(); - let value = if i < map.values.len() { - convert_ast_for_pure_function_body(allocator, &map.values[i], params) - } else { - OutputExpression::Literal(Box::new_in( - LiteralExpr { value: LiteralValue::Undefined, source_span: None }, - allocator, - )) - }; - entries.push(LiteralMapEntry { key: key_value, value, quoted: prop.quoted }); + let value = if i < map.values.len() { + convert_ast_for_pure_function_body(allocator, &map.values[i], params) + } else { + OutputExpression::Literal(Box::new_in( + LiteralExpr { value: LiteralValue::Undefined, source_span: None }, + allocator, + )) + }; + match key { + LiteralMapKey::Property(prop) => { + entries.push(LiteralMapEntry::new(prop.key.clone(), value, prop.quoted)); + } + LiteralMapKey::Spread(_) => { + entries.push(LiteralMapEntry::spread(value)); + } } } OutputExpression::LiteralMap(Box::new_in( @@ -1451,11 +1492,11 @@ fn emit_pooled_constant_value<'a>( // Emit object literal let mut map_entries = OxcVec::with_capacity_in(entries.len(), allocator); for (key, value) in entries.iter_mut() { - map_entries.push(LiteralMapEntry { - key: key.clone(), - value: emit_pooled_constant_value(allocator, value), - quoted: false, - }); + map_entries.push(LiteralMapEntry::new( + key.clone(), + emit_pooled_constant_value(allocator, value), + false, + )); } OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries: map_entries, source_span: None }, diff --git a/crates/oxc_angular_compiler/src/pipeline/ingest.rs b/crates/oxc_angular_compiler/src/pipeline/ingest.rs index 551de83d3..44cb8da94 100644 --- a/crates/oxc_angular_compiler/src/pipeline/ingest.rs +++ b/crates/oxc_angular_compiler/src/pipeline/ingest.rs @@ -264,18 +264,27 @@ fn convert_ast_to_ir<'a>( ) } - // Convert LiteralArray - recursively convert elements to preserve pipes + // 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 elem_expr = convert_ast_to_ir(job, elem); - elements.push(elem_expr.unbox()); + let is_spread = matches!(elem, AngularExpression::SpreadElement(_)); + let inner = if let AngularExpression::SpreadElement(s) = elem { + convert_ast_to_ir(job, s.unbox().expression) + } else { + 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, @@ -284,22 +293,32 @@ fn convert_ast_to_ir<'a>( ) } - // Convert LiteralMap (object literal) - recursively convert values to preserve pipes + // 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()) { - // Only handle property keys; spread keys need special handling - if let LiteralMapKey::Property(prop) = key { - keys.push(prop.key); - quoted.push(prop.quoted); - let value_expr = convert_ast_to_ir(job, value); - values.push(value_expr.unbox()); + 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 = convert_ast_to_ir(job, value); + values.push(value_expr.unbox()); } Box::new_in( @@ -308,6 +327,7 @@ fn convert_ast_to_ir<'a>( keys, values, quoted, + spreads, source_span: Some(map.source_span.to_span()), }, allocator, diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/chaining.rs b/crates/oxc_angular_compiler/src/pipeline/phases/chaining.rs index 3fee3589f..4cf686557 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/chaining.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/chaining.rs @@ -491,6 +491,7 @@ fn clone_expression<'a>( key: entry.key.clone(), value: clone_expression(allocator, &entry.value, diagnostics), quoted: entry.quoted, + is_spread: entry.is_spread, }); } OutputExpression::LiteralMap(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/defer_configs.rs b/crates/oxc_angular_compiler/src/pipeline/phases/defer_configs.rs index 9d5027f69..46a8d0153 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/defer_configs.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/defer_configs.rs @@ -87,6 +87,7 @@ pub fn configure_defer_instructions(job: &mut ComponentCompilationJob<'_>) { // which emits `null` for missing values, not `0`. if config.loading_minimum_time.is_some() || config.loading_after_time.is_some() { let mut elements = OxcVec::with_capacity_in(2, allocator); + let mut spreads = oxc_allocator::Vec::with_capacity_in(2, allocator); // minimumTime: number or null let min_val = match config.loading_minimum_time { @@ -100,6 +101,7 @@ pub fn configure_defer_instructions(job: &mut ComponentCompilationJob<'_>) { )), allocator, ))); + spreads.push(false); // afterTime: number or null let after_val = match config.loading_after_time { @@ -113,9 +115,10 @@ pub fn configure_defer_instructions(job: &mut ComponentCompilationJob<'_>) { )), allocator, ))); + spreads.push(false); let array_expr = IrExpression::LiteralArray(Box::new_in( - IrLiteralArrayExpr { elements, source_span: None }, + IrLiteralArrayExpr { elements, spreads, source_span: None }, allocator, )); @@ -134,6 +137,7 @@ pub fn configure_defer_instructions(job: &mut ComponentCompilationJob<'_>) { // Create placeholder config: [minimumTime] if let Some(min_time) = config.placeholder_minimum_time { let mut elements = OxcVec::with_capacity_in(1, allocator); + let mut spreads = oxc_allocator::Vec::with_capacity_in(1, allocator); elements.push(IrExpression::Ast(Box::new_in( crate::ast::expression::AngularExpression::LiteralPrimitive(Box::new_in( LiteralPrimitive { @@ -145,9 +149,10 @@ pub fn configure_defer_instructions(job: &mut ComponentCompilationJob<'_>) { )), allocator, ))); + spreads.push(false); let array_expr = IrExpression::LiteralArray(Box::new_in( - IrLiteralArrayExpr { elements, source_span: None }, + IrLiteralArrayExpr { elements, spreads, source_span: None }, allocator, )); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/i18n_closure.rs b/crates/oxc_angular_compiler/src/pipeline/phases/i18n_closure.rs index ec4acb81f..682f34f16 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/i18n_closure.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/i18n_closure.rs @@ -194,17 +194,17 @@ pub fn create_goog_get_msg_statements<'a>( let formatted_name = format_i18n_placeholder_name(name, true); let key_str = allocator.alloc_str(&formatted_name); let value_str = allocator.alloc_str(value); - entries.push(LiteralMapEntry { - key: Ident::from(key_str), - value: OutputExpression::Literal(AllocBox::new_in( + entries.push(LiteralMapEntry::new( + Ident::from(key_str), + OutputExpression::Literal(AllocBox::new_in( LiteralExpr { value: LiteralValue::String(Ident::from(value_str)), source_span: None, }, allocator, )), - quoted: true, - }); + true, + )); } goog_args.push(OutputExpression::LiteralMap(AllocBox::new_in( LiteralMapExpr { entries, source_span: None }, diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/i18n_const_collection.rs b/crates/oxc_angular_compiler/src/pipeline/phases/i18n_const_collection.rs index d44f19f52..a23b76442 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/i18n_const_collection.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/i18n_const_collection.rs @@ -863,14 +863,14 @@ fn wrap_with_postprocess<'a>( ))); } - entries.push(LiteralMapEntry { - key: Ident::from(key_str), - value: OutputExpression::LiteralArray(oxc_allocator::Box::new_in( + entries.push(LiteralMapEntry::new( + Ident::from(key_str), + OutputExpression::LiteralArray(oxc_allocator::Box::new_in( LiteralArrayExpr { entries: var_refs, source_span: None }, allocator, )), - quoted: true, - }); + true, + )); } args.push(OutputExpression::LiteralMap(oxc_allocator::Box::new_in( diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/pipe_variadic.rs b/crates/oxc_angular_compiler/src/pipeline/phases/pipe_variadic.rs index d291c1945..940d6eb8d 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/pipe_variadic.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/pipe_variadic.rs @@ -55,15 +55,17 @@ fn transform_pipe_binding<'a>( // Clone all arguments into a DerivedLiteralArray, preserving all IrExpression types let mut entries = ArenaVec::with_capacity_in(pipe.args.len(), allocator); + let mut spreads = oxc_allocator::Vec::with_capacity_in(pipe.args.len(), allocator); for arg in pipe.args.iter() { entries.push(arg.clone_in(allocator)); + spreads.push(false); } // Create a DerivedLiteralArray to hold the arguments // This properly handles all IrExpression types, not just Ast let args_array = Box::new_in( IrExpression::DerivedLiteralArray(Box::new_in( - DerivedLiteralArrayExpr { entries, source_span: pipe.source_span }, + DerivedLiteralArrayExpr { entries, spreads, source_span: pipe.source_span }, allocator, )), allocator, diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/pure_function_extraction.rs b/crates/oxc_angular_compiler/src/pipeline/phases/pure_function_extraction.rs index 6a58ec49c..505b29518 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/pure_function_extraction.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/pure_function_extraction.rs @@ -150,22 +150,39 @@ fn generate_expression_key(expr: &IrExpression<'_>) -> String { // Handle AST expressions - these are the main expression types that need key generation IrExpression::Ast(ast) => generate_angular_expression_key(ast), - // DerivedLiteralArray -> `[${entries.join(',')}]` + // DerivedLiteralArray -> `[${entries.join(',')}]` with `...` prefix on spread entries. + // The spread flag must participate in the key so `[a]` and `[...a]` don't collide in + // the pure-function pool and silently swap runtime semantics. IrExpression::DerivedLiteralArray(arr) => { - let entries: Vec<_> = arr.entries.iter().map(generate_expression_key).collect(); + let entries: Vec<_> = arr + .entries + .iter() + .zip(arr.spreads.iter()) + .map(|(entry, is_spread)| { + let key = generate_expression_key(entry); + if *is_spread { format!("...{}", key) } else { key } + }) + .collect(); format!("[{}]", entries.join(",")) } - // DerivedLiteralMap -> `{${entries.join(',')}}` + // DerivedLiteralMap -> `{${entries.join(',')}}` with `...value` for spread entries. IrExpression::DerivedLiteralMap(map) => { let entries: Vec<_> = map .keys .iter() .zip(map.values.iter()) .zip(map.quoted.iter()) - .map(|((key, value), quoted)| { - let key_str = if *quoted { format!("\"{}\"", key) } else { key.to_string() }; - format!("{}:{}", key_str, generate_expression_key(value)) + .zip(map.spreads.iter()) + .map(|(((key, value), quoted), is_spread)| { + let value_key = generate_expression_key(value); + if *is_spread { + format!("...{}", value_key) + } else { + let key_str = + if *quoted { format!("\"{}\"", key) } else { key.to_string() }; + format!("{}:{}", key_str, value_key) + } }) .collect(); format!("{{{}}}", entries.join(",")) diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/pure_literal_structures.rs b/crates/oxc_angular_compiler/src/pipeline/phases/pure_literal_structures.rs index 9ce7f0c1d..c1fffffc9 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/pure_literal_structures.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/pure_literal_structures.rs @@ -90,7 +90,7 @@ fn transform_literal_structure<'a>( // Handle IrExpression::LiteralArray - elements are already IR expressions // TypeScript always creates a PureFunction for literal arrays, even if all constant IrExpression::LiteralArray(arr) => { - create_pure_function_for_ir_array(&arr.elements, allocator, expressions) + create_pure_function_for_ir_array(&arr.elements, &arr.spreads, allocator, expressions) } // Handle IrExpression::LiteralMap - values are already IR expressions // TypeScript always creates a PureFunction for literal maps, even if all constant @@ -98,19 +98,24 @@ fn transform_literal_structure<'a>( &map.keys, &map.values, &map.quoted, + &map.spreads, allocator, expressions, ), // Handle IrExpression::DerivedLiteralArray - created by pipe_variadic phase // for variadic pipe arguments - IrExpression::DerivedLiteralArray(arr) => { - create_pure_function_for_derived_array(&arr.entries, allocator, expressions) - } + IrExpression::DerivedLiteralArray(arr) => create_pure_function_for_derived_array( + &arr.entries, + &arr.spreads, + allocator, + expressions, + ), // Handle IrExpression::DerivedLiteralMap - created for variadic maps IrExpression::DerivedLiteralMap(map) => create_pure_function_for_ir_map( &map.keys, &map.values, &map.quoted, + &map.spreads, allocator, expressions, ), @@ -211,33 +216,32 @@ fn resolve_expression_for_body<'a>( /// This is used for variadic pipe arguments created by the pipe_variadic phase. fn create_pure_function_for_derived_array<'a>( entries: &oxc_allocator::Vec<'a, IrExpression<'a>>, + spreads: &oxc_allocator::Vec<'a, bool>, allocator: &'a oxc_allocator::Allocator, expressions: &ExpressionStore<'a>, ) -> Option> { let mut args: AllocVec<'a, IrExpression<'a>> = AllocVec::new_in(allocator); let mut body_entries: AllocVec<'a, IrExpression<'a>> = AllocVec::new_in(allocator); + let mut body_spreads: AllocVec<'a, bool> = AllocVec::new_in(allocator); let mut param_index: u32 = 0; - for expr in entries.iter() { + for (i, expr) in entries.iter().enumerate() { + let is_spread = spreads.get(i).copied().unwrap_or(false); if is_constant_ir_expression(expr, expressions) { - // Constant entry: resolve and clone the expression body_entries.push(resolve_expression_for_body(expr, allocator, expressions)); } else { - // Non-constant entry: add to args and replace with PureFunctionParameterExpr args.push(expr.clone_in(allocator)); - body_entries.push(IrExpression::PureFunctionParameter(AllocBox::new_in( PureFunctionParameterExpr { index: param_index, source_span: None }, allocator, ))); param_index += 1; } + body_spreads.push(is_spread); } - // Create the derived array body - // TypeScript always creates a PureFunction, even with 0 args (all constant) let body = IrExpression::DerivedLiteralArray(AllocBox::new_in( - DerivedLiteralArrayExpr { entries: body_entries, source_span: None }, + DerivedLiteralArrayExpr { entries: body_entries, spreads: body_spreads, source_span: None }, allocator, )); @@ -253,33 +257,32 @@ fn create_pure_function_for_derived_array<'a>( /// Create a PureFunctionExpr for a literal array with IR expression elements. fn create_pure_function_for_ir_array<'a>( elements: &oxc_allocator::Vec<'a, IrExpression<'a>>, + spreads: &oxc_allocator::Vec<'a, bool>, allocator: &'a oxc_allocator::Allocator, expressions: &ExpressionStore<'a>, ) -> Option> { let mut args: AllocVec<'a, IrExpression<'a>> = AllocVec::new_in(allocator); let mut body_entries: AllocVec<'a, IrExpression<'a>> = AllocVec::new_in(allocator); + let mut body_spreads: AllocVec<'a, bool> = AllocVec::new_in(allocator); let mut param_index: u32 = 0; - for expr in elements.iter() { + for (i, expr) in elements.iter().enumerate() { + let is_spread = spreads.get(i).copied().unwrap_or(false); if is_constant_ir_expression(expr, expressions) { - // Constant entry: resolve and clone the expression body_entries.push(resolve_expression_for_body(expr, allocator, expressions)); } else { - // Non-constant entry: add to args and replace with PureFunctionParameterExpr args.push(expr.clone_in(allocator)); - body_entries.push(IrExpression::PureFunctionParameter(AllocBox::new_in( PureFunctionParameterExpr { index: param_index, source_span: None }, allocator, ))); param_index += 1; } + body_spreads.push(is_spread); } - // Create the derived array body - // TypeScript always creates a PureFunction, even with 0 args (all constant) let body = IrExpression::DerivedLiteralArray(AllocBox::new_in( - DerivedLiteralArrayExpr { entries: body_entries, source_span: None }, + DerivedLiteralArrayExpr { entries: body_entries, spreads: body_spreads, source_span: None }, allocator, )); @@ -297,6 +300,7 @@ fn create_pure_function_for_ir_map<'a>( keys: &oxc_allocator::Vec<'a, Ident<'a>>, values: &oxc_allocator::Vec<'a, IrExpression<'a>>, quoted: &oxc_allocator::Vec<'a, bool>, + spreads: &oxc_allocator::Vec<'a, bool>, allocator: &'a oxc_allocator::Allocator, expressions: &ExpressionStore<'a>, ) -> Option> { @@ -304,14 +308,17 @@ fn create_pure_function_for_ir_map<'a>( let mut body_keys: AllocVec<'a, Ident<'a>> = AllocVec::new_in(allocator); let mut body_values: AllocVec<'a, IrExpression<'a>> = AllocVec::new_in(allocator); let mut body_quoted: AllocVec<'a, bool> = AllocVec::new_in(allocator); + let mut body_spreads: AllocVec<'a, bool> = AllocVec::new_in(allocator); let mut param_index: u32 = 0; for (i, value) in values.iter().enumerate() { // Get key and quoted from the arrays let key = keys.get(i).cloned().unwrap_or_else(|| Ident::from("")); let is_quoted = quoted.get(i).copied().unwrap_or(false); + let is_spread = spreads.get(i).copied().unwrap_or(false); body_keys.push(key); body_quoted.push(is_quoted); + body_spreads.push(is_spread); if is_constant_ir_expression(value, expressions) { // Constant value: resolve and clone @@ -335,6 +342,7 @@ fn create_pure_function_for_ir_map<'a>( keys: body_keys, values: body_values, quoted: body_quoted, + spreads: body_spreads, source_span: None, }, allocator, @@ -357,32 +365,33 @@ fn create_pure_function_for_array<'a>( expressions: &AllocVec<'a, AngularExpression<'a>>, allocator: &'a oxc_allocator::Allocator, ) -> Option> { + use crate::ast::expression::AngularExpression; let mut args: AllocVec<'a, IrExpression<'a>> = AllocVec::new_in(allocator); let mut body_entries: AllocVec<'a, IrExpression<'a>> = AllocVec::new_in(allocator); + let mut body_spreads: AllocVec<'a, bool> = AllocVec::new_in(allocator); let mut param_index: u32 = 0; for expr in expressions.iter() { - if is_constant_expression(expr) { - // Constant entry: clone the AST expression and wrap in IrExpression::Ast - let cloned = clone_angular_expression(expr, allocator); + let is_spread = matches!(expr, AngularExpression::SpreadElement(_)); + let inner = + if let AngularExpression::SpreadElement(s) = expr { &s.expression } else { expr }; + if is_constant_expression(inner) { + let cloned = clone_angular_expression(inner, allocator); body_entries.push(IrExpression::Ast(AllocBox::new_in(cloned, allocator))); } else { - // Non-constant entry: add to args and replace with PureFunctionParameterExpr - let cloned = clone_angular_expression(expr, allocator); + let cloned = clone_angular_expression(inner, allocator); args.push(IrExpression::Ast(AllocBox::new_in(cloned, allocator))); - body_entries.push(IrExpression::PureFunctionParameter(AllocBox::new_in( PureFunctionParameterExpr { index: param_index, source_span: None }, allocator, ))); param_index += 1; } + body_spreads.push(is_spread); } - // Create the derived array body - // TypeScript always creates a PureFunction, even with 0 args (all constant) let body = IrExpression::DerivedLiteralArray(AllocBox::new_in( - DerivedLiteralArrayExpr { entries: body_entries, source_span: None }, + DerivedLiteralArrayExpr { entries: body_entries, spreads: body_spreads, source_span: None }, allocator, )); @@ -404,24 +413,23 @@ fn create_pure_function_for_map<'a>( let mut body_keys: AllocVec<'a, Ident<'a>> = AllocVec::new_in(allocator); let mut body_values: AllocVec<'a, IrExpression<'a>> = AllocVec::new_in(allocator); let mut body_quoted: AllocVec<'a, bool> = AllocVec::new_in(allocator); + let mut body_spreads: AllocVec<'a, bool> = AllocVec::new_in(allocator); let mut param_index: u32 = 0; for (i, value) in map.values.iter().enumerate() { use crate::ast::expression::LiteralMapKey; - // Extract key and quoted from LiteralMapKey - let (key, quoted) = map + // Extract key, quoted, and spread flag from LiteralMapKey + let (key, quoted, is_spread) = map .keys .get(i) - .and_then(|k| { - if let LiteralMapKey::Property(prop) = k { - Some((prop.key.clone(), prop.quoted)) - } else { - None // Skip spread keys - } + .map(|k| match k { + LiteralMapKey::Property(prop) => (prop.key.clone(), prop.quoted, false), + LiteralMapKey::Spread(_) => (Ident::from(""), false, true), }) - .unwrap_or_else(|| (Ident::from(""), false)); + .unwrap_or_else(|| (Ident::from(""), false, false)); body_keys.push(key); body_quoted.push(quoted); + body_spreads.push(is_spread); if is_constant_expression(value) { // Constant value: clone and wrap in IrExpression::Ast @@ -447,6 +455,7 @@ fn create_pure_function_for_map<'a>( keys: body_keys, values: body_values, quoted: body_quoted, + spreads: body_spreads, source_span: None, }, allocator, diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/angular_expression.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/angular_expression.rs index 84e1dcfcf..4e510fb7d 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/angular_expression.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/angular_expression.rs @@ -11,8 +11,9 @@ use crate::output::ast::{ ArrowFunctionBody, ArrowFunctionExpr, BinaryOperator, BinaryOperatorExpr, ConditionalExpr, FnParam, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, LiteralMapEntry, LiteralMapExpr, LiteralValue, NotExpr, OutputExpression, ParenthesizedExpr, ReadKeyExpr, ReadPropExpr, - ReadVarExpr, RegularExpressionLiteralExpr, TaggedTemplateLiteralExpr, TemplateLiteralElement, - TemplateLiteralExpr, TypeofExpr, UnaryOperator, UnaryOperatorExpr, VoidExpr, + ReadVarExpr, RegularExpressionLiteralExpr, SpreadElementExpr, TaggedTemplateLiteralExpr, + TemplateLiteralElement, TemplateLiteralExpr, TypeofExpr, UnaryOperator, UnaryOperatorExpr, + VoidExpr, }; /// Context for safe navigation expression conversion, providing temp variable allocation. @@ -306,7 +307,25 @@ fn convert_angular_expression_with_ctx<'a>( AngularExpression::LiteralArray(arr) => { let mut entries = OxcVec::new_in(allocator); for entry in arr.expressions.iter() { - entries.push(convert_angular_expression_with_ctx(allocator, entry, root_xref, ctx)); + if let AngularExpression::SpreadElement(spread) = entry { + let inner = convert_angular_expression_with_ctx( + allocator, + &spread.expression, + root_xref, + ctx, + ); + entries.push(OutputExpression::SpreadElement(Box::new_in( + SpreadElementExpr { + expr: Box::new_in(inner, allocator), + source_span: None, + }, + allocator, + ))); + } else { + entries.push(convert_angular_expression_with_ctx( + allocator, entry, root_xref, ctx, + )); + } } OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries, source_span: Some(arr.source_span.to_span()) }, @@ -318,19 +337,30 @@ fn convert_angular_expression_with_ctx<'a>( use crate::ast::expression::LiteralMapKey; let mut entries = OxcVec::new_in(allocator); for (i, key) in map.keys.iter().enumerate() { - // Only handle property keys; skip spread keys - if let LiteralMapKey::Property(prop) = key { - if i < map.values.len() { - entries.push(LiteralMapEntry { - key: prop.key.clone(), - value: convert_angular_expression_with_ctx( - allocator, - &map.values[i], - root_xref, - ctx, - ), - quoted: prop.quoted, - }); + if i < map.values.len() { + match key { + LiteralMapKey::Property(prop) => { + entries.push(LiteralMapEntry::new( + prop.key.clone(), + convert_angular_expression_with_ctx( + allocator, + &map.values[i], + root_xref, + ctx, + ), + prop.quoted, + )); + } + LiteralMapKey::Spread(_) => { + entries.push(LiteralMapEntry::spread( + convert_angular_expression_with_ctx( + allocator, + &map.values[i], + root_xref, + ctx, + ), + )); + } } } } 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 41fefee13..9b4d26ba0 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 @@ -9,7 +9,7 @@ use crate::ir::ops::XrefId; use crate::output::ast::{ BinaryOperator, BinaryOperatorExpr, ConditionalExpr, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, LiteralMapEntry, LiteralMapExpr, LiteralValue, OutputExpression, - ParenthesizedExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, + ParenthesizedExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, SpreadElementExpr, }; use crate::pipeline::expression_store::ExpressionStore; use crate::r3::{Identifiers, get_pipe_bind_instruction, get_pure_function_instruction}; @@ -808,8 +808,19 @@ pub fn convert_ir_expression<'a>( IrExpression::LiteralArray(arr) => { let mut entries = OxcVec::with_capacity_in(arr.elements.len(), allocator); - for elem in arr.elements.iter() { - entries.push(convert_ir_expression(allocator, elem, expressions, root_xref)); + for (i, elem) in arr.elements.iter().enumerate() { + let converted = convert_ir_expression(allocator, elem, expressions, root_xref); + if arr.spreads.get(i).copied().unwrap_or(false) { + entries.push(OutputExpression::SpreadElement(Box::new_in( + SpreadElementExpr { + expr: Box::new_in(converted, allocator), + source_span: None, + }, + allocator, + ))); + } else { + entries.push(converted); + } } OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries, source_span: arr.source_span }, @@ -822,9 +833,10 @@ pub fn convert_ir_expression<'a>( for (i, value) in map.values.iter().enumerate() { let key = map.keys.get(i).cloned().unwrap_or_else(|| Ident::from("")); let quoted = map.quoted.get(i).copied().unwrap_or(false); + let is_spread = map.spreads.get(i).copied().unwrap_or(false); let converted_value = convert_ir_expression(allocator, value, expressions, root_xref); - entries.push(LiteralMapEntry { key, value: converted_value, quoted }); + entries.push(LiteralMapEntry { key, value: converted_value, quoted, is_spread }); } OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries, source_span: map.source_span }, @@ -834,8 +846,19 @@ pub fn convert_ir_expression<'a>( IrExpression::DerivedLiteralArray(arr) => { let mut entries = OxcVec::with_capacity_in(arr.entries.len(), allocator); - for entry in arr.entries.iter() { - entries.push(convert_ir_expression(allocator, entry, expressions, root_xref)); + for (i, entry) in arr.entries.iter().enumerate() { + let converted = convert_ir_expression(allocator, entry, expressions, root_xref); + if arr.spreads.get(i).copied().unwrap_or(false) { + entries.push(OutputExpression::SpreadElement(Box::new_in( + SpreadElementExpr { + expr: Box::new_in(converted, allocator), + source_span: None, + }, + allocator, + ))); + } else { + entries.push(converted); + } } OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries, source_span: arr.source_span }, @@ -848,9 +871,10 @@ pub fn convert_ir_expression<'a>( for (i, value) in map.values.iter().enumerate() { let key = map.keys.get(i).cloned().unwrap_or_else(|| Ident::from("")); let quoted = map.quoted.get(i).copied().unwrap_or(false); + let is_spread = map.spreads.get(i).copied().unwrap_or(false); let converted_value = convert_ir_expression(allocator, value, expressions, root_xref); - entries.push(LiteralMapEntry { key, value: converted_value, quoted }); + entries.push(LiteralMapEntry { key, value: converted_value, quoted, is_spread }); } OutputExpression::LiteralMap(Box::new_in( LiteralMapExpr { entries, source_span: map.source_span }, 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 837a36f99..069455278 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/resolve_names.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/resolve_names.rs @@ -1223,11 +1223,19 @@ fn resolve_angular_expression<'a>( // Create a DerivedLiteralMap with the resolved values let mut keys = oxc_allocator::Vec::new_in(allocator); let mut quoted = oxc_allocator::Vec::new_in(allocator); + let mut spreads = oxc_allocator::Vec::new_in(allocator); for key in map.keys.iter() { - // Only handle property keys; skip spread keys - if let LiteralMapKey::Property(prop) = key { - keys.push(prop.key.clone()); - quoted.push(prop.quoted); + match key { + LiteralMapKey::Property(prop) => { + keys.push(prop.key.clone()); + quoted.push(prop.quoted); + spreads.push(false); + } + LiteralMapKey::Spread(_) => { + keys.push(Ident::from("")); + quoted.push(false); + spreads.push(true); + } } } @@ -1236,6 +1244,7 @@ fn resolve_angular_expression<'a>( keys, values: resolved_values, quoted, + spreads, source_span: Some(map.source_span.to_span()), }, allocator, @@ -1248,27 +1257,35 @@ fn resolve_angular_expression<'a>( AngularExpression::LiteralArray(arr) => { // Handle array literals - need to resolve variable references in entries let mut resolved_entries = oxc_allocator::Vec::new_in(allocator); + let mut spreads = oxc_allocator::Vec::new_in(allocator); let mut any_resolved = false; for entry in arr.expressions.iter() { + let is_spread = matches!(entry, AngularExpression::SpreadElement(_)); + let inner = if let AngularExpression::SpreadElement(s) = entry { + &s.expression + } else { + entry + }; if let Some(resolved) = - resolve_angular_expression(entry, scope, root_xref, allocator) + resolve_angular_expression(inner, scope, root_xref, allocator) { resolved_entries.push(resolved); any_resolved = true; } else { - // Keep the original entry wrapped as Ast resolved_entries.push(IrExpression::Ast(Box::new_in( - crate::ir::expression::clone_angular_expression(entry, allocator), + crate::ir::expression::clone_angular_expression(inner, allocator), allocator, ))); } + spreads.push(is_spread); } if any_resolved { Some(IrExpression::DerivedLiteralArray(Box::new_in( crate::ir::expression::DerivedLiteralArrayExpr { entries: resolved_entries, + spreads, source_span: Some(arr.source_span.to_span()), }, allocator, 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 0b37468cd..21dcf89b6 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs @@ -3579,8 +3579,12 @@ where for entry in arr.entries.iter() { entries.push(transform_expression(entry, allocator, transform)); } + let mut spreads = OxcVec::with_capacity_in(arr.spreads.len(), allocator); + for s in arr.spreads.iter() { + spreads.push(*s); + } IrExpression::DerivedLiteralArray(OxcBox::new_in( - DerivedLiteralArrayExpr { entries, source_span: arr.source_span }, + DerivedLiteralArrayExpr { entries, spreads, source_span: arr.source_span }, allocator, )) } @@ -3598,8 +3602,18 @@ where for q in map.quoted.iter() { quoted.push(*q); } + let mut spreads = OxcVec::with_capacity_in(map.spreads.len(), allocator); + for s in map.spreads.iter() { + spreads.push(*s); + } IrExpression::DerivedLiteralMap(OxcBox::new_in( - DerivedLiteralMapExpr { keys, values, quoted, source_span: map.source_span }, + DerivedLiteralMapExpr { + keys, + values, + quoted, + spreads, + source_span: map.source_span, + }, allocator, )) } @@ -3609,8 +3623,12 @@ where for elem in arr.elements.iter() { elements.push(transform_expression(elem, allocator, transform)); } + let mut spreads = OxcVec::with_capacity_in(arr.spreads.len(), allocator); + for s in arr.spreads.iter() { + spreads.push(*s); + } IrExpression::LiteralArray(OxcBox::new_in( - IrLiteralArrayExpr { elements, source_span: arr.source_span }, + IrLiteralArrayExpr { elements, spreads, source_span: arr.source_span }, allocator, )) } @@ -3628,8 +3646,12 @@ where for q in map.quoted.iter() { quoted.push(*q); } + let mut spreads = OxcVec::with_capacity_in(map.spreads.len(), allocator); + for s in map.spreads.iter() { + spreads.push(*s); + } IrExpression::LiteralMap(OxcBox::new_in( - IrLiteralMapExpr { keys, values, quoted, source_span: map.source_span }, + IrLiteralMapExpr { keys, values, quoted, spreads, source_span: map.source_span }, allocator, )) } diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index e65809f40..4a69bdb87 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -776,6 +776,178 @@ fn test_let_self_reference_replaced_with_undefined() { insta::assert_snapshot!("let_self_reference", js); } +// ============================================================================ +// Object Spread Tests +// ============================================================================ + +#[test] +fn test_object_spread_in_binding() { + // { ...base, extra: 'val' } — spread was silently dropped, resulting in { extra: 'val' } + // Keys/values in LiteralMap are parallel arrays; LiteralMapKey::Spread was skipped in + // convert_ast_to_ir so the spread expression never reached the IR or emitter. + let js = compile_template_to_js( + r#"
"#, + "TestComponent", + ); + // Angular wraps object literals in pure functions; spread appears as ...a0 in the + // pure function body and ctx.base is passed as the argument. + assert!(js.contains("...a0"), "object spread should be preserved in output. Output:\n{js}"); + assert!(js.contains("ctx.base"), "spread variable should be referenced. Output:\n{js}"); + insta::assert_snapshot!("object_spread_in_binding", js); +} + +#[test] +fn test_object_spread_only() { + let js = compile_template_to_js(r#"
"#, "TestComponent"); + assert!(js.contains("...a0"), "spread-only object should emit spread. Output:\n{js}"); + assert!(js.contains("ctx.base"), "spread variable should be referenced. Output:\n{js}"); + insta::assert_snapshot!("object_spread_only", js); +} + +#[test] +fn test_object_multiple_spreads() { + let js = compile_template_to_js( + r#"
"#, + "TestComponent", + ); + assert!( + js.contains("...a0") && js.contains("...a1"), + "multiple spreads should both appear. Output:\n{js}" + ); + assert!( + js.contains("ctx.a") && js.contains("ctx.b"), + "both spread variables should be referenced. Output:\n{js}" + ); + insta::assert_snapshot!("object_multiple_spreads", js); +} + +#[test] +fn test_object_spread_with_pipe() { + // Pipe inside the same object literal as a spread — pipe must still be registered. + let js = compile_template_to_js( + r#"
"#, + "TestComponent", + ); + assert!(js.contains("...a0"), "spread should be preserved alongside pipe. Output:\n{js}"); + assert!( + js.contains("ɵɵpipeBind1"), + "pipe inside object literal with spread should still be registered. Output:\n{js}" + ); + insta::assert_snapshot!("object_spread_with_pipe", js); +} + +#[test] +fn test_object_spread_at_end() { + let js = + compile_template_to_js(r#"
"#, "TestComponent"); + assert!(js.contains("...a0"), "trailing spread should be preserved. Output:\n{js}"); + assert!(js.contains("ctx.base"), "spread variable should be referenced. Output:\n{js}"); + insta::assert_snapshot!("object_spread_at_end", js); +} + +// ============================================================================ +// Spread in Complex Expressions +// ============================================================================ + +#[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"]`. + let js = compile_template_to_js( + r#""#, + "TestComponent", + ); + assert!( + js.contains("...ctx.base"), + "spread inside arrow function body should be preserved. Output:\n{js}" + ); + insta::assert_snapshot!("spread_in_arrow_function_body", js); +} + +#[test] +fn test_object_spread_chained_bindings() { + // Two property bindings on the same element force the chaining phase to run. + // The chaining phase clones instruction args via clone_expression. Before the fix to + // chaining.rs, LiteralMapEntry::new() was used (which always sets is_spread: false), + // silently dropping spread info from any LiteralMap that clone_expression encountered. + let js = compile_template_to_js( + r#"
"#, + "TestComponent", + ); + assert!( + js.contains("...a0"), + "spread should be preserved when bindings are chained. Output:\n{js}" + ); + assert!( + js.contains("ctx.base"), + "spread variable should be referenced when bindings are chained. Output:\n{js}" + ); + insta::assert_snapshot!("object_spread_chained_bindings", js); +} + +// ============================================================================ +// Array Spread Tests +// ============================================================================ + +#[test] +fn test_array_spread_in_binding() { + let js = compile_template_to_js(r#"
"#, "TestComponent"); + assert!(js.contains("...a0"), "array spread should be preserved in output. Output:\n{js}"); + assert!(js.contains("ctx.base"), "spread variable should be referenced. Output:\n{js}"); + insta::assert_snapshot!("array_spread_in_binding", js); +} + +#[test] +fn test_array_multiple_spreads() { + let js = + compile_template_to_js(r#"
"#, "TestComponent"); + assert!( + js.contains("...a0") && js.contains("...a1"), + "multiple array spreads should both appear. Output:\n{js}" + ); + assert!( + js.contains("ctx.a") && js.contains("ctx.b"), + "both spread variables should be referenced. Output:\n{js}" + ); + insta::assert_snapshot!("array_multiple_spreads", js); +} + +#[test] +fn test_array_spread_vs_non_spread_pooling_distinct() { + // Two array bindings whose entries are identical except for spread shape: `[a]` vs `[...a]`. + // The pure-function pool deduplicates by body key, so if the key generation ignores the + // spread metadata on DerivedLiteralArray entries, both bindings collide on the same pooled + // helper and one binding gets the other's runtime semantics. + let js = compile_template_to_js( + r#"
"#, + "TestComponent", + ); + // Each binding must produce its own pure function: one emitting `[a0]`, the other `[...a0]`. + assert!(js.contains("[a0]"), "non-spread array binding should emit `[a0]` body. Output:\n{js}"); + assert!( + js.contains("[...a0]"), + "spread array binding should emit `[...a0]` body. Output:\n{js}" + ); +} + +#[test] +fn test_object_spread_vs_non_spread_pooling_distinct() { + // Object literal counterpart of the array test above. `{k: a}` and `{...a}` would collide + // on the same pooled helper if spread metadata is excluded from the key. + let js = compile_template_to_js( + r#"
"#, + "TestComponent", + ); + assert!(js.contains("...a0"), "object spread binding should emit `...a0`. Output:\n{js}"); + assert!( + js.contains("k: a0") || js.contains("k:a0"), + "non-spread object binding should emit `k: a0`. Output:\n{js}" + ); +} + // ============================================================================ // ng-content Tests // ============================================================================ diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__array_multiple_spreads.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__array_multiple_spreads.snap new file mode 100644 index 000000000..d3741d7d3 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__array_multiple_spreads.snap @@ -0,0 +1,9 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = (a0,a1) =>[...a0,...a1,"val"]; +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵelement(0,"div",0); } + if ((rf & 2)) { i0.ɵɵproperty("title",i0.ɵɵpureFunction2(1,_c0,ctx.a,ctx.b)); } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__array_spread_in_binding.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__array_spread_in_binding.snap new file mode 100644 index 000000000..448cb4ef1 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__array_spread_in_binding.snap @@ -0,0 +1,9 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = (a0) =>[...a0,"extra"]; +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵelement(0,"div",0); } + if ((rf & 2)) { i0.ɵɵproperty("title",i0.ɵɵpureFunction1(1,_c0,ctx.base)); } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_multiple_spreads.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_multiple_spreads.snap new file mode 100644 index 000000000..c7bf883eb --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_multiple_spreads.snap @@ -0,0 +1,9 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = (a0,a1) =>({...a0,...a1,key:"val"}); +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵelement(0,"div",0); } + if ((rf & 2)) { i0.ɵɵproperty("title",i0.ɵɵpureFunction2(1,_c0,ctx.a,ctx.b)); } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_at_end.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_at_end.snap new file mode 100644 index 000000000..c6f753c4c --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_at_end.snap @@ -0,0 +1,9 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = (a0) =>({key:"val",...a0}); +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵelement(0,"div",0); } + if ((rf & 2)) { i0.ɵɵproperty("title",i0.ɵɵpureFunction1(1,_c0,ctx.base)); } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_chained_bindings.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_chained_bindings.snap new file mode 100644 index 000000000..840c4bef6 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_chained_bindings.snap @@ -0,0 +1,9 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = (a0) =>({...a0,extra:"val"}); +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵelement(0,"div",0); } + if ((rf & 2)) { i0.ɵɵproperty("title",i0.ɵɵpureFunction1(2,_c0,ctx.base))("id",ctx.myId); } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_in_binding.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_in_binding.snap new file mode 100644 index 000000000..f88063cf4 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_in_binding.snap @@ -0,0 +1,9 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = (a0) =>({...a0,extra:"val"}); +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵelement(0,"div",0); } + if ((rf & 2)) { i0.ɵɵproperty("title",i0.ɵɵpureFunction1(1,_c0,ctx.base)); } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_only.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_only.snap new file mode 100644 index 000000000..5ecc8bc4d --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_only.snap @@ -0,0 +1,9 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = (a0) =>({...a0}); +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵelement(0,"div",0); } + if ((rf & 2)) { i0.ɵɵproperty("title",i0.ɵɵpureFunction1(1,_c0,ctx.base)); } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_with_pipe.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_with_pipe.snap new file mode 100644 index 000000000..90d76e3d3 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__object_spread_with_pipe.snap @@ -0,0 +1,13 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = (a0,a1) =>({...a0,val:a1}); +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { + i0.ɵɵelement(0,"div",0); + i0.ɵɵpipe(1,"percent"); + } + if ((rf & 2)) { i0.ɵɵproperty("title",i0.ɵɵpureFunction2(3,_c0,ctx.base,i0.ɵɵpipeBind1(1, + 1,ctx.num))); } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__spread_in_arrow_function_body.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__spread_in_arrow_function_body.snap new file mode 100644 index 000000000..5ad1eb477 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__spread_in_arrow_function_body.snap @@ -0,0 +1,14 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { + i0.ɵɵelementStart(0,"button",0); + i0.ɵɵlistener("click",function TestComponent_Template_button_click_0_listener() { + return ctx.handler(() =>[...ctx.base,"extra"]); + }); + i0.ɵɵtext(1,"click"); + i0.ɵɵelementEnd(); + } +} diff --git a/napi/angular-compiler/test/transform.test.ts b/napi/angular-compiler/test/transform.test.ts index d51778fd0..2517cf3ca 100644 --- a/napi/angular-compiler/test/transform.test.ts +++ b/napi/angular-compiler/test/transform.test.ts @@ -540,3 +540,42 @@ describe('animation host listeners', () => { expect(result.code).not.toMatch(/ɵɵlistener\(\s*"anim"[\s\S]*?,\s*null\s*,\s*true\s*\)/) }) }) + +describe('object spread in template bindings', () => { + it('should preserve spread syntax in object literal bindings', async () => { + const source = ` + import { Component } from '@angular/core' + @Component({ + template: \`
\`, + }) + export class TestComponent { + base = {} + } + ` + const result = await transformAngularFile(source, 'test.component.ts', {}) + expect(result.errors).toHaveLength(0) + // Spread is inside a pure function body (Angular memoizes object literals). + // The emitted code should contain spread syntax, not an empty-string key. + expect(result.code).toContain('...') + expect(result.code).not.toMatch(/""\s*:/) + }) + + it('should preserve multiple spreads in object literal bindings', async () => { + const source = ` + import { Component } from '@angular/core' + @Component({ + template: \`
\`, + }) + export class TestComponent { + a = {} + b = {} + } + ` + const result = await transformAngularFile(source, 'test.component.ts', {}) + expect(result.errors).toHaveLength(0) + expect(result.code).not.toMatch(/""\s*:/) + // Both spread variables should appear in the output + expect(result.code).toContain('ctx.a') + expect(result.code).toContain('ctx.b') + }) +})