Skip to content

Commit 78b7a27

Browse files
authored
fix: ctx.X vs ctx_rN.X context references (#2)
`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)
1 parent c19950c commit 78b7a27

4 files changed

Lines changed: 118 additions & 0 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,18 @@ fn resolve_expression<'a>(
980980
);
981981
}
982982

983+
// Parenthesized expression - resolve the inner expression
984+
IrExpression::Parenthesized(paren) => {
985+
resolve_expression(
986+
paren.expr.as_mut(),
987+
scope,
988+
root_xref,
989+
saved_view,
990+
allocator,
991+
expressions,
992+
);
993+
}
994+
983995
// Other expression types don't need resolution
984996
_ => {}
985997
}

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3650,3 +3650,36 @@ export class TestComponent {
36503650
result.code
36513651
);
36523652
}
3653+
3654+
#[test]
3655+
fn test_nested_if_listener_ctx_reference() {
3656+
// Test: nested @if where a listener in the inner @if accesses component properties.
3657+
// The listener should use nextContext() to get the component context,
3658+
// not bare `ctx` which would be the inner embedded view's context.
3659+
let js = compile_template_to_js(
3660+
r#"@if (show) {
3661+
@if (active) {
3662+
<button (click)="handleClick()">Click</button>
3663+
}
3664+
}"#,
3665+
"TestComponent",
3666+
);
3667+
insta::assert_snapshot!("nested_if_listener_ctx_reference", js);
3668+
}
3669+
3670+
#[test]
3671+
fn test_nested_if_alias_listener_ctx_reference() {
3672+
// Test: @if with alias, nested @if where listener accesses both
3673+
// the alias from the outer @if and a method from the component.
3674+
// All context references inside the listener should use named variables (ctx_rN),
3675+
// not bare `ctx`.
3676+
let js = compile_template_to_js(
3677+
r#"@if (getItem(); as item) {
3678+
@if (item.active) {
3679+
<button (click)="makePrivate(!(item.private && !item.shareWithTeam))">Toggle</button>
3680+
}
3681+
}"#,
3682+
"TestComponent",
3683+
);
3684+
insta::assert_snapshot!("nested_if_alias_listener_ctx_reference", js);
3685+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
source: crates/oxc_angular_compiler/tests/integration_test.rs
3+
expression: js
4+
---
5+
function TestComponent_Conditional_0_Conditional_1_Template(rf,ctx) {
6+
if ((rf & 1)) {
7+
const _r1 = i0.ɵɵgetCurrentView();
8+
i0.ɵɵtext(0,"\n ");
9+
i0.ɵɵelementStart(1,"button",0);
10+
i0.ɵɵlistener("click",function TestComponent_Conditional_0_Conditional_1_Template_button_click_1_listener() {
11+
i0.ɵɵrestoreView(_r1);
12+
const item_r2 = i0.ɵɵnextContext();
13+
const ctx_r2 = i0.ɵɵnextContext();
14+
return i0.ɵɵresetView(ctx_r2.makePrivate(!(item_r2.private && !item_r2.shareWithTeam)));
15+
});
16+
i0.ɵɵtext(2,"Toggle");
17+
i0.ɵɵelementEnd();
18+
i0.ɵɵtext(3,"\n ");
19+
}
20+
}
21+
function TestComponent_Conditional_0_Template(rf,ctx) {
22+
if ((rf & 1)) {
23+
i0.ɵɵtext(0,"\n ");
24+
i0.ɵɵconditionalCreate(1,TestComponent_Conditional_0_Conditional_1_Template,4,
25+
0);
26+
}
27+
if ((rf & 2)) {
28+
i0.ɵɵadvance();
29+
i0.ɵɵconditional((ctx.active? 1: -1));
30+
}
31+
}
32+
function TestComponent_Template(rf,ctx) {
33+
if ((rf & 1)) { i0.ɵɵconditionalCreate(0,TestComponent_Conditional_0_Template,2,1); }
34+
if ((rf & 2)) {
35+
let tmp_0_0;
36+
i0.ɵɵconditional(((tmp_0_0 = ctx.getItem())? 0: -1),tmp_0_0);
37+
}
38+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
source: crates/oxc_angular_compiler/tests/integration_test.rs
3+
expression: js
4+
---
5+
function TestComponent_Conditional_0_Conditional_1_Template(rf,ctx) {
6+
if ((rf & 1)) {
7+
const _r1 = i0.ɵɵgetCurrentView();
8+
i0.ɵɵtext(0,"\n ");
9+
i0.ɵɵelementStart(1,"button",0);
10+
i0.ɵɵlistener("click",function TestComponent_Conditional_0_Conditional_1_Template_button_click_1_listener() {
11+
i0.ɵɵrestoreView(_r1);
12+
const ctx_r1 = i0.ɵɵnextContext(2);
13+
return i0.ɵɵresetView(ctx_r1.handleClick());
14+
});
15+
i0.ɵɵtext(2,"Click");
16+
i0.ɵɵelementEnd();
17+
i0.ɵɵtext(3,"\n ");
18+
}
19+
}
20+
function TestComponent_Conditional_0_Template(rf,ctx) {
21+
if ((rf & 1)) {
22+
i0.ɵɵtext(0,"\n ");
23+
i0.ɵɵconditionalCreate(1,TestComponent_Conditional_0_Conditional_1_Template,4,
24+
0);
25+
}
26+
if ((rf & 2)) {
27+
const ctx_r1 = i0.ɵɵnextContext();
28+
i0.ɵɵadvance();
29+
i0.ɵɵconditional((ctx_r1.active? 1: -1));
30+
}
31+
}
32+
function TestComponent_Template(rf,ctx) {
33+
if ((rf & 1)) { i0.ɵɵconditionalCreate(0,TestComponent_Conditional_0_Template,2,1); }
34+
if ((rf & 2)) { i0.ɵɵconditional((ctx.show? 0: -1)); }
35+
}

0 commit comments

Comments
 (0)