Skip to content

fix(directive): chain query instructions on Angular ≥ 21.0.4, align _c<N> ordering#323

Merged
brandonroberts merged 2 commits into
voidzero-dev:mainfrom
brandonroberts:fix/query-signal-chained-calls
May 30, 2026
Merged

fix(directive): chain query instructions on Angular ≥ 21.0.4, align _c<N> ordering#323
brandonroberts merged 2 commits into
voidzero-dev:mainfrom
brandonroberts:fix/query-signal-chained-calls

Conversation

@brandonroberts
Copy link
Copy Markdown
Collaborator

@brandonroberts brandonroberts commented May 29, 2026

Summary

  • Chain consecutive same-kind query instructions (ɵɵviewQuery(p1)(p2), ɵɵcontentQuerySignal(...)(...)) when the target Angular runtime supports it — matches upstream ngtsc's instructionChainAfter() emit pattern.
  • Gate that chained emit behind a new AngularVersion::supports_chained_queries() predicate so consumers explicitly targeting v19/v20/v21.0.0–v21.0.3 can opt out and keep the safe separate-statement form. (The runtime functions returned void until v21.0.4; chaining on those versions throws at runtime.)
  • Swap predicate-pooling order so content queries are pooled before view queries, aligning the _c<N> constant-table indices with upstream emit. Runtime-safe on every supported version.

Compatibility (v19+)

Behavior across the currently-supported version range:

Angular version ɵɵviewQuery / ɵɵcontentQuery / ɵɵ*Signal return Emit Runtime
v19, v20, v21.0.0 – v21.0.3 (explicit) void separate statements (unchanged from current behavior) safe
v21.0.4+ (cherry-pick) typeof <fn> chained safe
v21.1.0+ / v22+ typeof <fn> chained safe
version unknown (None) chained (assume latest) safe on v21.0.4+

The runtime contract changed in angular/angular@ae1c0dc4 ("perf(compiler): chain query creation instructions", cherry-picked into v21.0.4 as f901cc9e). The existing comments in this codebase saying "Angular 20's ɵɵviewQuery returns void, so chaining is not supported" were correct on v20 and remain correct on v19/v20/v21.0.0–3 — this PR keeps that fallback path available to consumers who pass an explicit angular_version.

The first revision of this PR silently changed emit for every supported version, which would have broken v19/v20/v21.0.0–3 at runtime. This revision adds the version gate so the older fallback is reachable.

None = assume latest

When angular_version is None the compiler emits the chained form, matching this crate's existing convention (supports_implicit_standalone, supports_service_decorator, etc. all use map_or(true, …) and the transform.rs comment reads angular_version: None // assume latest). Consumers targeting v19/v20/v21.0.0–3 already need to set angular_version explicitly to get the right emit for several other instructions (ɵɵconditionalCreate vs. ɵɵtemplate, ɵɵdomProperty vs. ɵɵhostProperty, etc.); the chained-query gate slots into the same opt-out pattern.

Why

The Angular runtime source at v22 / v21.0.4+ shows all four query instructions return typeof <fn>:

export function ɵɵcontentQuerySignal<T>(
  
): typeof ɵɵcontentQuerySignal {
  bindQueryToSignal(target, createContentQuery());
  return ɵɵcontentQuerySignal;  // ← chainable
}

Upstream ngtsc chains consecutive same-instruction calls on those versions via instructionChainAfter(). Every compliance golden under signal_queries/ and r3_compiler_compliance/components_and_directives/queries/ emits the chained form.

The pool-ordering swap is a separate, runtime-safe fix: predicate pooling was running view queries first, so _c0/_c1 ended up on view queries while Angular's goldens have them on content queries. One-line swap in compile_component_full puts content first, aligning const-table indices with every supported Angular version.

What changes per file

crates/oxc_angular_compiler/src/component/metadata.rs

  • Add AngularVersion::supports_chained_queries(). Matches the existing supports_* pattern. v22+ unconditionally, v21.1.0+ on the v21 line, v21.0.4+ on the v21.0 cherry-pick line. v19/v20 explicitly false.

crates/oxc_angular_compiler/src/directive/query.rs

  • create_view_queries_function and create_content_queries_function gain an Option<AngularVersion> parameter. Chained-emit guard: angular_version.map_or(true, |v| v.supports_chained_queries())None assumes latest (chained), explicit pre-v21.0.4 falls back to one expression statement per query.
  • Consecutive same-is_signal queries fold via call_fn(chain, params). The chain flushes when signal-ness flips (different runtime symbol — can't bridge the boundary) and at end-of-loop.
  • Three unit tests rewritten to pass v22 and assert the chained form.

crates/oxc_angular_compiler/src/directive/compiler.rs and definition.rs

  • compile_directive, compile_directive_from_metadata, build_base_directive_fields, generate_directive_definitions, and generate_dir_definition thread the new Option<AngularVersion> parameter through.

crates/oxc_angular_compiler/src/component/transform.rs

  • Threads options.angular_version to the query builders and to generate_directive_definitions.
  • Swaps the call order so content-query predicates are pooled before view-query predicates.

crates/oxc_angular_compiler/tests/integration_test.rs

  • Three integration tests rewritten to pass v22 and assert chained output / boundary behavior.
  • New test_query_chaining_obeys_angular_version_gate exercises both contracts in one place: explicit v19.2.0, v20.0.0, v21.0.3 each fall back to separate statements; None, v21.0.4, v22.0.0 all chain.

Linker (intentionally untouched)

The linker (linker/mod.rs) processes partial declarations into final form without knowing the target runtime version, so chained emit there would be unsafe when the linked library ends up running on the v19/v20/v21.0.0–3 path. Worth revisiting once build_queries and its callers gain version awareness. The earlier revision of this PR did patch the linker; it's reverted here.

Mixed signal + decorator behavior

Worth calling out: ɵɵviewQuery and ɵɵviewQuerySignal are different runtime symbols, so a chain can't cross that boundary. The implementation flushes the chain when is_signal flips between consecutive queries — same for content queries. test_mixed_signal_and_decorator_view_queries_break_chain_on_boundary covers this: two signal queries chain into one expression, then the single decorator query produces a separate root call.

Test plan

  • cargo test -p oxc_angular_compiler — all ~2500 tests pass (1047 lib + 398 integration + remainder across other test files).
  • cargo fmt --all -- --check passes (clean rustfmt).
  • NAPI rebuild (pnpm --filter ./napi/angular-compiler build) succeeds.
  • End-to-end verification: linked the local NAPI build into analogjs/analog's upstream-anchored parity suite (which compares output against Angular's compliance goldens) and passed angularVersion: { major: 22, minor: 0, patch: 0 } via the adapter. The signal_queries/query_in_component fixture — previously the one known OXC divergence — now matches Angular v22.1.0-next.0 byte-for-byte.
  • New regression test test_query_chaining_obeys_angular_version_gate asserts both contracts: explicit pre-v21.0.4 falls back, None/v21.0.4+/v22 chain.

@brandonroberts brandonroberts force-pushed the fix/query-signal-chained-calls branch from 8a6ecc4 to 9bf5c52 Compare May 29, 2026 19:26
@brandonroberts brandonroberts changed the title fix(directive): chain query instructions and align _c<N> ordering with upstream fix(directive): chain query instructions on Angular ≥ 21.0.4, align _c<N> ordering May 29, 2026
@brandonroberts brandonroberts force-pushed the fix/query-signal-chained-calls branch from 9bf5c52 to 63701a9 Compare May 29, 2026 19:37
@brandonroberts
Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@brandonroberts
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63701a9b1b

ℹ️ 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".

Comment thread crates/oxc_angular_compiler/src/directive/query.rs Outdated
brandonroberts and others added 2 commits May 29, 2026 14:58
`ɵɵviewQuery`, `ɵɵcontentQuery`, `ɵɵviewQuerySignal`, and
`ɵɵcontentQuerySignal` switched from returning `void` to returning
`typeof <fn>` in `angular/angular@ae1c0dc4` ("perf(compiler): chain
query creation instructions"), cherry-picked into v21.0.4 as
`f901cc9e`. From that release on, upstream ngtsc chains consecutive
same-instruction calls via `instructionChainAfter()` (every
compliance golden under `signal_queries/` and
`r3_compiler_compliance/.../queries/` shows the chained form). This
compiler emitted separate statements unconditionally — a known
runtime-safe form that disagrees with the upstream emit on supported
modern Angular.

This change adds a new `AngularVersion::supports_chained_queries()`
predicate (`major > 21 || (major == 21 && (minor > 0 || patch >= 4))`)
and gates chained emit in both `create_view_queries_function` and
`create_content_queries_function`. When the predicate returns `false`
— including when the consumer doesn't pass a version (`None`) — both
functions fall back to the existing safe form, one expression
statement per query. v19, v20, and v21.0.0–v21.0.3 stay on that
fallback path; chaining them would throw `TypeError: not a function`
at runtime.

Mixed signal + decorator queries on a single directive: the chain
breaks at the signal-ness boundary (different runtime symbols can't
chain), matching upstream behavior.

`compile_directive`, `compile_directive_from_metadata`,
`build_base_directive_fields`, `generate_directive_definitions`, and
`generate_dir_definition` gain an `Option<AngularVersion>` parameter
so the new version-aware emit can be threaded from `TransformOptions`.

Tests:
- Three unit tests in `directive/query` rewritten to pass v22 and
  assert the chained form.
- Three integration tests rewritten with v22 and the corresponding
  assertions.
- New `test_query_chaining_falls_back_to_separate_statements_pre_v21_0_4`
  exercises the safe fallback for `None`, v19.2.0, v20.0.0, and
  v21.0.3, plus a sanity check that v21.0.4 chains.

Linker (`linker/mod.rs`) is intentionally left unchanged. The linker
processes partial declarations into final form without knowing the
target runtime version, so emitting chained calls there would be
unsafe on pre-v21.0.4 runtimes. Worth revisiting once the linker
gains version awareness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cates

Upstream ngtsc's `compileComponent` emits the `contentQueries:` field
before `viewQuery:` in the component definition object literal, and
pools predicates in that order. The resulting `_c<N>` constant-table
indices end up content-first (`_c0`, `_c1` for content; `_c2`, `_c3`
for view) — the form pinned by every compliance golden under
`compiler-cli/test/compliance/test_cases/signal_queries/`.

This compiler had the order reversed (view queries pooled first), so
the same source produced `_c0`/`_c1` for view queries and `_c2`/`_c3`
for content — runtime-equivalent but breaks emit-anchored tooling and
diff-based comparisons against Angular's own goldens.

Fix: swap the call order in `compile_component_full` so content
queries are pooled and emitted before view queries. Runtime behavior
is unaffected on every supported Angular version — only the
const-table indices shift back into upstream alignment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@brandonroberts brandonroberts force-pushed the fix/query-signal-chained-calls branch from 63701a9 to 2da0379 Compare May 29, 2026 19:58
@brandonroberts brandonroberts enabled auto-merge (squash) May 29, 2026 20:41
@brandonroberts brandonroberts merged commit ce76592 into voidzero-dev:main May 30, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants