Skip to content

Commit ac7b95c

Browse files
Brooooooklynclaude
andcommitted
fix: process handler_expression after handler_ops to avoid duplicate tmp declarations
handler_expression was being processed inside the per-op loop, causing it to be visited/transformed once per handler_op. When handler_ops had multiple entries (e.g., restoreView + nextContext), this produced spurious declarations (let tmp_0_0; let tmp_1_0;) and wrong variable names. Now handler_expression is processed once after the loop with op_count set to handler_ops.len(), matching Angular TS where the return expression is the last entry in handlerOps. For a listener with 2 handler_ops, this correctly generates tmp_2_0 instead of tmp_1_0. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 565ef32 commit ac7b95c

File tree

3 files changed

+53
-81
lines changed

3 files changed

+53
-81
lines changed

crates/oxc_angular_compiler/src/pipeline/phases/temporary_variables.rs

Lines changed: 16 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -235,21 +235,28 @@ fn generate_temporaries_for_handler_ops<'a>(
235235
/// temporary variable processing so that `let tmp_N_0;` declarations are generated inside
236236
/// the listener function scope rather than the parent create scope.
237237
///
238-
/// The algorithm mirrors `generate_temporaries_for_handler_ops` but additionally visits/transforms
239-
/// the handler_expression alongside the ops.
238+
/// The handler_ops are processed first (same as `generate_temporaries_for_handler_ops`),
239+
/// then handler_expression is processed once AFTER the loop with op_count equal to
240+
/// handler_ops.len(). This matches Angular TS where the return expression is the last
241+
/// entry in handlerOps at index N.
240242
fn generate_temporaries_for_handler_ops_with_expression<'a>(
241243
ops: &mut [UpdateOp<'a>],
242244
handler_expression: &mut Option<oxc_allocator::Box<'a, IrExpression<'a>>>,
243245
allocator: &'a Allocator,
244246
) -> Vec<UpdateOp<'a>> {
245-
let mut op_count = 0;
246-
let mut generated_statements = Vec::new();
247+
// First, process handler_ops exactly like generate_temporaries_for_handler_ops
248+
let mut generated_statements = generate_temporaries_for_handler_ops(ops, allocator);
249+
250+
// Then process handler_expression once, with op_count = ops.len().
251+
// In Angular TS, the return expression is the last entry in handlerOps,
252+
// so its op index equals the number of preceding ops.
253+
if let Some(handler_expr) = handler_expression.as_mut() {
254+
let op_count = ops.len();
247255

248-
for op in ops.iter_mut() {
249256
// Pass 1: Count reads per xref to determine final reads
250257
let read_counts = RefCell::new(FxHashMap::<XrefId, usize>::default());
251-
visit_expressions_in_update_op(
252-
op,
258+
visit_expressions_in_expression(
259+
handler_expr,
253260
&|expr, flags| {
254261
if flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) {
255262
return;
@@ -260,28 +267,12 @@ fn generate_temporaries_for_handler_ops_with_expression<'a>(
260267
},
261268
VisitorContextFlag::NONE,
262269
);
263-
// Also count reads in handler_expression
264-
if let Some(handler_expr) = handler_expression.as_ref() {
265-
visit_expressions_in_expression(
266-
handler_expr,
267-
&|expr, flags| {
268-
if flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) {
269-
return;
270-
}
271-
if let IrExpression::ReadTemporary(read) = expr {
272-
*read_counts.borrow_mut().entry(read.xref).or_insert(0) += 1;
273-
}
274-
},
275-
VisitorContextFlag::NONE,
276-
);
277-
}
278270
let total_reads = read_counts.into_inner();
279271

280272
// Pass 2: Assign names with reuse when final read is encountered
281273
let tracker = RefCell::new(TempVarTracker::new(op_count, total_reads));
282-
283-
transform_expressions_in_update_op(
284-
op,
274+
transform_expressions_in_expression(
275+
handler_expr,
285276
&|expr, flags| {
286277
if flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) {
287278
return;
@@ -290,19 +281,6 @@ fn generate_temporaries_for_handler_ops_with_expression<'a>(
290281
},
291282
VisitorContextFlag::NONE,
292283
);
293-
// Also assign names in handler_expression
294-
if let Some(handler_expr) = handler_expression.as_mut() {
295-
transform_expressions_in_expression(
296-
handler_expr,
297-
&|expr, flags| {
298-
if flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) {
299-
return;
300-
}
301-
assign_temp_names(expr, &tracker, allocator);
302-
},
303-
VisitorContextFlag::NONE,
304-
);
305-
}
306284

307285
// Collect unique names and create declarations
308286
let defs = tracker.into_inner().defs;
@@ -313,49 +291,6 @@ fn generate_temporaries_for_handler_ops_with_expression<'a>(
313291
statement: stmt,
314292
}));
315293
}
316-
317-
op_count += 1;
318-
}
319-
320-
// If there are no ops but handler_expression has temporaries, process it standalone
321-
if ops.is_empty() {
322-
if let Some(handler_expr) = handler_expression.as_mut() {
323-
let read_counts = RefCell::new(FxHashMap::<XrefId, usize>::default());
324-
visit_expressions_in_expression(
325-
handler_expr,
326-
&|expr, flags| {
327-
if flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) {
328-
return;
329-
}
330-
if let IrExpression::ReadTemporary(read) = expr {
331-
*read_counts.borrow_mut().entry(read.xref).or_insert(0) += 1;
332-
}
333-
},
334-
VisitorContextFlag::NONE,
335-
);
336-
let total_reads = read_counts.into_inner();
337-
338-
let tracker = RefCell::new(TempVarTracker::new(op_count, total_reads));
339-
transform_expressions_in_expression(
340-
handler_expr,
341-
&|expr, flags| {
342-
if flags.contains(VisitorContextFlag::IN_CHILD_OPERATION) {
343-
return;
344-
}
345-
assign_temp_names(expr, &tracker, allocator);
346-
},
347-
VisitorContextFlag::NONE,
348-
);
349-
350-
let defs = tracker.into_inner().defs;
351-
for name in collect_unique_names(&defs) {
352-
let stmt = create_declare_var_statement(allocator, &name);
353-
generated_statements.push(UpdateOp::Statement(StatementOp {
354-
base: UpdateOpBase::default(),
355-
statement: stmt,
356-
}));
357-
}
358-
}
359294
}
360295

361296
generated_statements

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,19 @@ fn test_safe_property_read_with_call_receiver_in_listener() {
10621062
insta::assert_snapshot!("safe_property_read_with_call_receiver_in_listener", js);
10631063
}
10641064

1065+
#[test]
1066+
fn test_safe_call_in_listener_inside_conditional() {
1067+
// When a listener is inside an embedded view (e.g., @if), handler_ops contains
1068+
// restoreView and nextContext statements. The handler_expression (return value)
1069+
// must be processed AFTER those ops, not once per op, to get the correct
1070+
// tmp variable name (tmp_2_0, matching the op index after restoreView and nextContext).
1071+
let js = compile_template_to_js(
1072+
r#"@if (show) { <button (click)="getPopover()?.close()">Close</button> }"#,
1073+
"TestComponent",
1074+
);
1075+
insta::assert_snapshot!("safe_call_in_listener_inside_conditional", js);
1076+
}
1077+
10651078
// ============================================================================
10661079
// Event Modifier Tests
10671080
// ============================================================================
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
source: crates/oxc_angular_compiler/tests/integration_test.rs
3+
expression: js
4+
---
5+
function TestComponent_Conditional_0_Template(rf,ctx) {
6+
if ((rf & 1)) {
7+
const _r1 = i0.ɵɵgetCurrentView();
8+
i0.ɵɵtext(0," ");
9+
i0.ɵɵelementStart(1,"button",0);
10+
i0.ɵɵlistener("click",function TestComponent_Conditional_0_Template_button_click_1_listener() {
11+
let tmp_2_0;
12+
i0.ɵɵrestoreView(_r1);
13+
const ctx_r1 = i0.ɵɵnextContext();
14+
return i0.ɵɵresetView((((tmp_2_0 = ctx_r1.getPopover()) == null)? null: tmp_2_0.close()));
15+
});
16+
i0.ɵɵtext(2,"Close");
17+
i0.ɵɵelementEnd();
18+
i0.ɵɵtext(3," ");
19+
}
20+
}
21+
function TestComponent_Template(rf,ctx) {
22+
if ((rf & 1)) { i0.ɵɵconditionalCreate(0,TestComponent_Conditional_0_Template,4,0); }
23+
if ((rf & 2)) { i0.ɵɵconditional((ctx.show? 0: -1)); }
24+
}

0 commit comments

Comments
 (0)