fix(safe-nav): version-aware optional chaining for Angular v22+ (#317)#330
fix(safe-nav): version-aware optional chaining for Angular v22+ (#317)#330brandonroberts wants to merge 1 commit into
Conversation
Angular v22 changed the safe-navigation operator (`?.`) in template expressions to yield `undefined` via native optional chaining, gated by the `legacyOptionalChaining` compiler option. OXC unconditionally emitted the legacy `== null ? null` ternary, so v22+ projects got the wrong runtime value for any `?.` expression. Changes: - Add `legacyOptionalChaining` to `TransformOptions` (NAPI + Rust) and thread it through ingest into the compilation jobs. The effective default is derived from `angularVersion`: legacy for < v22, modern (native `?.`) for >= v22, and legacy when the version is unknown (matches Angular's conservative fallback). - Add an `optional` flag to the resolved IR read/call nodes (`ResolvedPropertyRead`/`ResolvedKeyedRead`/`ResolvedCall`) and pass it through reify so it renders as native `?.` / `?.[]` / `?.()`. - Rewrite `expand_safe_reads` to branch per node: legacy builds the `SafeTernary` (`== null ? null`); modern rewrites each safe access into the equivalent optional resolved read (no temporaries needed). - Support the `$safeNavigationMigration(...)` escape hatch: a wrapped subtree is forced back to legacy null semantics even on a modern target, and the wrapper is stripped. Two deviations from the issue text, both to match the reference compiler (angular/angular@2896c93cc1): - The modern form is native optional chaining (`ctx.user?.name`), not the `== null ? undefined` ternary the issue described. Both yield `undefined` at runtime; native `?.` matches Angular's emitted output. - The magic function shipped in v22 is `$safeNavigationMigration(...)`, not `$null(...)` (the commit message named `$null` but the code renamed it). Partial/linker output keeps legacy semantics for now; threading the facade field through partial emit is deferred (issue required-work #4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1269b230ee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| call.receiver.as_ref(), | ||
| IrExpression::ResolvedPropertyRead(p) | ||
| if p.name.as_str() == "$safeNavigationMigration" |
There was a problem hiding this comment.
Restrict migration marker to the top-level helper
This marker check matches any one-argument call whose callee property is named $safeNavigationMigration, so a legitimate template call such as svc.$safeNavigationMigration(user?.name) is stripped and replaced with the argument instead of invoking svc. The magic wrapper should only apply to the unqualified migration helper on the component context; otherwise object methods/properties with the same name are silently dropped in both legacy and modern compilations.
Useful? React with 👍 / 👎.
Summary
Angular v22 changed the safe-navigation operator (
?.) in template expressions to yieldundefinedvia native optional chaining, gated behind thelegacyOptionalChainingcompiler option (angular/angular@2896c93cc1). OXC unconditionally emitted the legacy== null ? nullternary, so every?.expression in a v22+ project got the wrong runtime value (nullinstead ofundefined). This is a silent, time-bombed regression for anyone upgrading to v22.This PR makes OXC version-aware and faithful to the reference compiler's emitted output.
Closes #317.
What changed
legacyOptionalChainingoption added toTransformOptions(NAPI + Rust +index.d.ts) and threaded through ingest into bothComponentCompilationJobandHostBindingCompilationJob. The effective value isexplicit_override ?? version_default, where the version default is derived fromangularVersion:null)?.,undefined)optionalflag added to the resolved IR read/call nodes (ResolvedPropertyRead/ResolvedKeyedRead/ResolvedCall) and passed through reify so it renders as native?./?.[]/?.().expand_safe_readsrewritten to branch per node: legacy builds theSafeTernary(== null ? null); modern rewrites each safe access into the equivalent optional resolved read (no temporaries needed — native?.evaluates the receiver once).$safeNavigationMigration(...)escape hatch: a wrapped subtree is forced back to legacynullsemantics even on a modern target, and the wrapper is stripped. Detected post-resolve_names(where the receiver's names are already resolved), so no new IR node or extra phase was needed.Deviations from the issue text (both to match the reference compiler)
ctx.user?.name), not the== null ? undefinedternary the issue described. Both yieldundefined; native?.is what Angular actually emits.$safeNavigationMigration(...), not$null(...)— the referenced commit's message said$null, but the code (inv22.0.0-next.10) named it$safeNavigationMigration.Out of scope
Partial/linker output keeps legacy semantics for now; threading the
legacyOptionalChainingfacade field through partial emit is deferred (issue required-work #4). Wiring a user'stsconfigangularCompilerOptions.legacyOptionalChaining→ this NAPI option lives in the JS build-plugin layer.Verification
Output was diffed against Angular's own compliance goldens (
r3_view_compiler/safe_access) by running the exact fixture templates through OXC at v22:ctx.p?.a?.b?.c?.d,ctx.p?.["a"]?.["b"]..., and the mixed-optional/plain cases).$safeNavigationMigrationlegacy expansion reproduces the golden including the same temporary name (tmp_3_0) and chain structure.The only difference anywhere is OXC's emitter wrapping ternaries in extra parentheses — pre-existing style, semantically identical.
Test plan
cargo build --workspace(incl. NAPI crate) — cleancargo test -p oxc_angular_compiler— all green; existing safe-nav snapshots unchanged (version-unknown stays legacy)cargo fmt --check— clean$safeNavigationMigrationescape hatch🤖 Generated with Claude Code