From eada62871fd899a0ed2158eeae3ba48cc9419e31 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 4 Feb 2026 08:48:49 +0800 Subject: [PATCH] fix: ctx.X vs ctx_rN.X context references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resolve_expression` in `resolve_names.rs` was missing a handler for `IrExpression::Parenthesized`. When `convert_ast_to_ir` converts Angular template expressions, parenthesized sub-expressions like `!(item.private && !item.shareWithTeam)` become `Not(Parenthesized(Binary(...)))` in the IR. Since `Parenthesized` hit the `_ => {}` catch-all, all sub-expressions inside parentheses were never resolved — `LexicalRead("item")` references stayed unresolved and were later emitted as bare `ctx.item` instead of being resolved to the proper alias variable (e.g. `item_r2` via `nextContext()`). The fix adds `IrExpression::Parenthesized` handling to `resolve_expression`, recursing into the inner expression so that all nested variable references are properly resolved through the scope chain. ClickUp comparison: 203 → 177 mismatches (26 files fixed, 97.0% match) --- .../src/pipeline/phases/resolve_names.rs | 12 ++++++ .../tests/integration_test.rs | 33 ++++++++++++++++ ...ested_if_alias_listener_ctx_reference.snap | 38 +++++++++++++++++++ ...est__nested_if_listener_ctx_reference.snap | 35 +++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__nested_if_alias_listener_ctx_reference.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__nested_if_listener_ctx_reference.snap 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 70a8feb64..3722d461d 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/resolve_names.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/resolve_names.rs @@ -980,6 +980,18 @@ fn resolve_expression<'a>( ); } + // Parenthesized expression - resolve the inner expression + IrExpression::Parenthesized(paren) => { + resolve_expression( + paren.expr.as_mut(), + scope, + root_xref, + saved_view, + allocator, + expressions, + ); + } + // Other expression types don't need resolution _ => {} } diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 88d35a968..690b98a35 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -3650,3 +3650,36 @@ export class TestComponent { result.code ); } + +#[test] +fn test_nested_if_listener_ctx_reference() { + // Test: nested @if where a listener in the inner @if accesses component properties. + // The listener should use nextContext() to get the component context, + // not bare `ctx` which would be the inner embedded view's context. + let js = compile_template_to_js( + r#"@if (show) { + @if (active) { + + } +}"#, + "TestComponent", + ); + insta::assert_snapshot!("nested_if_listener_ctx_reference", js); +} + +#[test] +fn test_nested_if_alias_listener_ctx_reference() { + // Test: @if with alias, nested @if where listener accesses both + // the alias from the outer @if and a method from the component. + // All context references inside the listener should use named variables (ctx_rN), + // not bare `ctx`. + let js = compile_template_to_js( + r#"@if (getItem(); as item) { + @if (item.active) { + + } +}"#, + "TestComponent", + ); + insta::assert_snapshot!("nested_if_alias_listener_ctx_reference", js); +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__nested_if_alias_listener_ctx_reference.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__nested_if_alias_listener_ctx_reference.snap new file mode 100644 index 000000000..908f8c057 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__nested_if_alias_listener_ctx_reference.snap @@ -0,0 +1,38 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +function TestComponent_Conditional_0_Conditional_1_Template(rf,ctx) { + if ((rf & 1)) { + const _r1 = i0.ɵɵgetCurrentView(); + i0.ɵɵtext(0,"\n "); + i0.ɵɵelementStart(1,"button",0); + i0.ɵɵlistener("click",function TestComponent_Conditional_0_Conditional_1_Template_button_click_1_listener() { + i0.ɵɵrestoreView(_r1); + const item_r2 = i0.ɵɵnextContext(); + const ctx_r2 = i0.ɵɵnextContext(); + return i0.ɵɵresetView(ctx_r2.makePrivate(!(item_r2.private && !item_r2.shareWithTeam))); + }); + i0.ɵɵtext(2,"Toggle"); + i0.ɵɵelementEnd(); + i0.ɵɵtext(3,"\n "); + } +} +function TestComponent_Conditional_0_Template(rf,ctx) { + if ((rf & 1)) { + i0.ɵɵtext(0,"\n "); + i0.ɵɵconditionalCreate(1,TestComponent_Conditional_0_Conditional_1_Template,4, + 0); + } + if ((rf & 2)) { + i0.ɵɵadvance(); + i0.ɵɵconditional((ctx.active? 1: -1)); + } +} +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵconditionalCreate(0,TestComponent_Conditional_0_Template,2,1); } + if ((rf & 2)) { + let tmp_0_0; + i0.ɵɵconditional(((tmp_0_0 = ctx.getItem())? 0: -1),tmp_0_0); + } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__nested_if_listener_ctx_reference.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__nested_if_listener_ctx_reference.snap new file mode 100644 index 000000000..3094fbe89 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__nested_if_listener_ctx_reference.snap @@ -0,0 +1,35 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +function TestComponent_Conditional_0_Conditional_1_Template(rf,ctx) { + if ((rf & 1)) { + const _r1 = i0.ɵɵgetCurrentView(); + i0.ɵɵtext(0,"\n "); + i0.ɵɵelementStart(1,"button",0); + i0.ɵɵlistener("click",function TestComponent_Conditional_0_Conditional_1_Template_button_click_1_listener() { + i0.ɵɵrestoreView(_r1); + const ctx_r1 = i0.ɵɵnextContext(2); + return i0.ɵɵresetView(ctx_r1.handleClick()); + }); + i0.ɵɵtext(2,"Click"); + i0.ɵɵelementEnd(); + i0.ɵɵtext(3,"\n "); + } +} +function TestComponent_Conditional_0_Template(rf,ctx) { + if ((rf & 1)) { + i0.ɵɵtext(0,"\n "); + i0.ɵɵconditionalCreate(1,TestComponent_Conditional_0_Conditional_1_Template,4, + 0); + } + if ((rf & 2)) { + const ctx_r1 = i0.ɵɵnextContext(); + i0.ɵɵadvance(); + i0.ɵɵconditional((ctx_r1.active? 1: -1)); + } +} +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { i0.ɵɵconditionalCreate(0,TestComponent_Conditional_0_Template,2,1); } + if ((rf & 2)) { i0.ɵɵconditional((ctx.show? 0: -1)); } +}