Repository navigation
fix(compiler): resolve template locals inside $safeNavigationMigration in listeners and arrow functions - #587
Merged
Brooooooklyn merged 3 commits intoOct 20, 2026
Conversation
…on` in listeners A listener's `handler_expression` is resolved by calling `resolve_expression` directly rather than through the generic expression visitor, and its explicit match had no arm for the `SafeNavigationMigration` wrapper. Template locals in the wrapped subtree (`@for` items, `@if ... as` aliases) were left unresolved and later read from the listener context (`ctx.option`), so `(click)="pick($safeNavigationMigration(option?.value))"` always passed `null`. Fixes voidzero-dev#585 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…safeNavigationMigration` - resolve_names: the wrapper arm comment now names both direct roots (a listener's handler_expression and hoisted arrow-function bodies). - Listener test: add ng-template `let-`, root-view `#ref` and `@let` cases; assert the exact declaration and read for each local. - New test for arrow functions: the hoisted factory restores the local from its own view and returns through `ɵɵresetView`. - Move the compare fixture into safe-navigation-migration.fixture.ts and add more listener locals plus an arrow-function entry; both match Angular 22.2.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…igration` test Assert every statement of each listener body as one block, as the arrow-function test does. The old checks still passed when the root-view `#ref`/`@let` listeners had no `ɵɵrestoreView`/`ɵɵresetView`, which is the pre-fix shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
$safeNavigationMigration in listeners$safeNavigationMigration in listeners and arrow functions
Brooooooklyn
approved these changes
Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #585. Inside an event listener,
$safeNavigationMigration(expr)read the template locals inexpr(@foritems,@if … asaliases,let-variables,#refs,@lets) fromctxinstead of from the local restored from the view. So the argument was alwaysundefined, and for example(click)="pick($safeNavigationMigration(option?.value))"passednull. Only the listener's returned (last) expression was affected. Earlier statements were already correct.The same gap also broke arrow functions in bindings, for example
[title]="fmt(() => $safeNavigationMigration(option?.value))". The hoisted arrow factory read a variable that exists only inside the template function, so a call threw aReferenceError. Plain property bindings were not affected.Cause
resolve_namesresolves most expressions through the generic visitor (transform_expressions_in_*), which goes into every IR node. Two kinds of roots callresolve_expressiondirectly instead:handler_expression(the returned expression)The explicit match in
resolve_expressionhad no arm forIrExpression::SafeNavigationMigration, so the wrapped subtree hit_ => {}:LexicalReads, and the unusedrestoreView(...).$implicitvariable was dropped. Reify then emitted the leftover reads asctx.<name>, wherectxis the parameter of the enclosing template function.option_r1, which is not in its scope.The fix adds the missing arm. It goes into the wrapped expression, like the
Parenthesizedarm does. This matches upstream:resolveNameswalks these expressions withtransformExpressionsInOp, andSafeNavigationMigrationExprpasses the transform on to its argument.While the wrapper exists (from
removeSafeNavigationMigrationtoexpandSafeReads), the only other hand-written walker is the$eventscan inresolve_dollar_event, and it already goes into the wrapper.expandSafeReadsremoves the wrapper before the later phases run.Listener,
@for (option of options; track $index) { <button (click)="pick($safeNavigationMigration(option?.value))"> }:i0.ɵɵlistener("click",function TestComponent_For_1_Template_button_click_1_listener() { - i0.ɵɵrestoreView(_r1); - const ctx_r1 = i0.ɵɵnextContext(); - return i0.ɵɵresetView(ctx_r1.pick(((ctx.option == null)? null: ctx.option.value))); + const option_r2 = i0.ɵɵrestoreView(_r1).$implicit; + const ctx_r2 = i0.ɵɵnextContext(); + return i0.ɵɵresetView(ctx_r2.pick(((option_r2 == null)? null: option_r2.value))); });Arrow function in a binding,
@for (option of options; track $index) { <div [title]="fmt(() => $safeNavigationMigration(option?.value))"> }:Why there was no compile error: for the listener case, the check in
resolve_names(verify_no_lexical_reads_remain) did push an error diagnostic. Butcompile_component_fulldrops job diagnostics when compilation succeeds, sotransformAngularFilereturned no error. This PR does not change that. The check does not catch the arrow case anyway, because there noLexicalReadis left.Test plan
test_safe_navigation_migration_in_listener_reads_template_locals:@foritem,@if … asalias,let-variable, root-view#ref, root-view@let. Each case asserts the whole listener body.test_safe_navigation_migration_in_arrow_function_reads_template_locals:@foritem and@if … asalias in a binding arrow function. Each case asserts the whole hoisted factory.regressions/safe-navigation-migration.fixture.ts:safe-nav-migration-in-listenerandsafe-nav-migration-in-arrow-function. Both mismatch without the fix and match@angular/compiler22.2.2 with it.cargo test --all-features: 3267 passed, 0 failed.cargo fmt --all -- --check,cargo run -p oxc_angular_conformance(1264/1264, no snapshot diff).pnpm test(441/441),pnpm test:e2e(43 passed),pnpm --filter @oxc-angular/compare compare --fixtures(1091 total, 0 mismatched).@if … asactivation toggles). A local build with this fix and the linker fix for Linker always uses the latest security schema:img|src/video|srcsanitizers dropped for Angular < 22.2.2 #586 makes all of them pass.🤖 Generated with Claude Code