Skip to content

Commit 34a4669

Browse files
fix: preserve object and array spread syntax in template bindings (#260)
* fix: preserve object and array spread syntax in template bindings Object and array spread expressions in Angular template bindings were silently dropped during compilation, e.g. `[prop]="{ ...base, key: val }"`` would emit `{ key: val }` with the spread lost entirely. The fix spans the full compilation pipeline: - ingest: handle LiteralMapKey::Spread when converting AST→IR - ir/expression: add spreads: Vec<bool> parallel field to all literal map/array IR types to track which entries are spreads - resolve_names: propagate spreads through name resolution - pure_literal_structures: emit spread entries in pure function wrappers - reify/ir_expression: emit spread entries when lowering IR→output AST - reify/angular_expression: handle SpreadElement in LiteralArray arm - emit: fix convert_ast_for_pure_function_body to handle spreads in both LiteralMap and LiteralArray - output/ast: clone_in and chaining preserve is_spread on LiteralMapEntry - output/oxc_converter: handle SpreadProperty in object expressions - defer_configs, pipe_variadic: maintain spreads parallel-vec invariant Adds 9 snapshot tests covering object spread, array spread, multiple spreads, spread-with-pipe, spread-at-end, chained bindings, and arrow function body spread. Adds TS NAPI tests for both object and array spread. * fix: include spread metadata in pure function pooling key Pooled pure-function bodies for `[a]` and `[...a]` (and object equivalents) collided in the constant pool because the key generation for DerivedLiteralArray/DerivedLiteralMap ignored the `spreads` parallel array, causing one binding to silently inherit the other's runtime semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style: apply cargo fmt to integration test assertions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 48f9fe7 commit 34a4669

37 files changed

Lines changed: 954 additions & 537 deletions

crates/oxc_angular_compiler/src/class_debug_info/compiler.rs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,36 +53,36 @@ fn internal_compile_class_debug_info<'a>(
5353
let mut entries = Vec::new_in(allocator);
5454

5555
// className
56-
entries.push(LiteralMapEntry {
57-
key: Ident::from("className"),
58-
value: literal_string_atom(allocator, debug_info.class_name.clone()),
59-
quoted: false,
60-
});
56+
entries.push(LiteralMapEntry::new(
57+
Ident::from("className"),
58+
literal_string_atom(allocator, debug_info.class_name.clone()),
59+
false,
60+
));
6161

6262
// Include filePath and lineNumber only if filePath is set
6363
// (matching Angular's behavior - if filePath is null, downstream consumers
6464
// will typically ignore lineNumber as well)
6565
if let Some(file_path) = &debug_info.file_path {
66-
entries.push(LiteralMapEntry {
67-
key: Ident::from("filePath"),
68-
value: literal_string_atom(allocator, file_path.clone()),
69-
quoted: false,
70-
});
71-
72-
entries.push(LiteralMapEntry {
73-
key: Ident::from("lineNumber"),
74-
value: literal_number(allocator, debug_info.line_number),
75-
quoted: false,
76-
});
66+
entries.push(LiteralMapEntry::new(
67+
Ident::from("filePath"),
68+
literal_string_atom(allocator, file_path.clone()),
69+
false,
70+
));
71+
72+
entries.push(LiteralMapEntry::new(
73+
Ident::from("lineNumber"),
74+
literal_number(allocator, debug_info.line_number),
75+
false,
76+
));
7777
}
7878

7979
// Include forbidOrphanRendering only if it's true (to reduce generated code)
8080
if debug_info.forbid_orphan_rendering {
81-
entries.push(LiteralMapEntry {
82-
key: Ident::from("forbidOrphanRendering"),
83-
value: literal_bool(allocator, true),
84-
quoted: false,
85-
});
81+
entries.push(LiteralMapEntry::new(
82+
Ident::from("forbidOrphanRendering"),
83+
literal_bool(allocator, true),
84+
false,
85+
));
8686
}
8787

8888
let debug_info_object = OutputExpression::LiteralMap(Box::new_in(

crates/oxc_angular_compiler/src/class_metadata/builders.rs

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,7 @@ pub fn build_decorator_metadata_array<'a>(
6565
};
6666

6767
// Add "type" entry
68-
map_entries.push(LiteralMapEntry {
69-
key: Ident::from("type"),
70-
value: type_expr,
71-
quoted: false,
72-
});
68+
map_entries.push(LiteralMapEntry::new(Ident::from("type"), type_expr, false));
7369

7470
// Add "args" entry if the decorator has arguments
7571
if let Expression::CallExpression(call) = &decorator.expression
@@ -84,14 +80,14 @@ pub fn build_decorator_metadata_array<'a>(
8480
}
8581

8682
if !args.is_empty() {
87-
map_entries.push(LiteralMapEntry {
88-
key: Ident::from("args"),
89-
value: OutputExpression::LiteralArray(Box::new_in(
83+
map_entries.push(LiteralMapEntry::new(
84+
Ident::from("args"),
85+
OutputExpression::LiteralArray(Box::new_in(
9086
LiteralArrayExpr { entries: args, source_span: None },
9187
allocator,
9288
)),
93-
quoted: false,
94-
});
89+
false,
90+
));
9591
}
9692
}
9793

@@ -156,22 +152,18 @@ pub fn build_ctor_params_metadata<'a>(
156152
))
157153
});
158154

159-
map_entries.push(LiteralMapEntry {
160-
key: Ident::from("type"),
161-
value: type_expr,
162-
quoted: false,
163-
});
155+
map_entries.push(LiteralMapEntry::new(Ident::from("type"), type_expr, false));
164156

165157
// Extract decorators from the parameter
166158
let param_decorators = extract_angular_decorators_from_param(param);
167159
if !param_decorators.is_empty() {
168160
let decorators_array =
169161
build_decorator_metadata_array(allocator, &param_decorators, source_text);
170-
map_entries.push(LiteralMapEntry {
171-
key: Ident::from("decorators"),
172-
value: decorators_array,
173-
quoted: false,
174-
});
162+
map_entries.push(LiteralMapEntry::new(
163+
Ident::from("decorators"),
164+
decorators_array,
165+
false,
166+
));
175167
}
176168

177169
param_entries.push(OutputExpression::LiteralMap(Box::new_in(
@@ -258,11 +250,7 @@ pub fn build_prop_decorators_metadata<'a>(
258250
let decorators_array =
259251
build_decorator_metadata_array(allocator, &angular_decorators, source_text);
260252

261-
prop_entries.push(LiteralMapEntry {
262-
key: prop_name,
263-
value: decorators_array,
264-
quoted: false,
265-
});
253+
prop_entries.push(LiteralMapEntry::new(prop_name, decorators_array, false));
266254
}
267255

268256
if prop_entries.is_empty() {

0 commit comments

Comments
 (0)