Skip to content

fix(queries): compile decorator queries: and match ngtsc's query options - #495

Open
ashley-hunter wants to merge 6 commits into
feat/validate-input-transformsfrom
feat/decorator-metadata-queries
Open

ashley-hunter wants to merge 6 commits into
feat/validate-input-transformsfrom
feat/decorator-metadata-queries

Conversation

@ashley-hunter

@ashley-hunter ashley-hunter commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Stack (3/5): #493 inputs/outputs → #494 transform validation → #495 queries: → #496 .d.ts transform types → #504 signal API import identity. This is #495.

Compiles queries: declared in @Directive / @Component metadata, and matches ngtsc on a few member-query options.

The bug

@Directive({ selector: '[d]', queries: { el: new ViewChild('el') } })
export class D { el!: ElementRef; }

compiled with no view query, so el was never set. The queries key was ignored like inputs: was.

The fix

queries: is parsed like ngtsc's extractQueriesFromDecorator: new ViewChild/ViewChildren/ContentChild/ContentChildren(...) imported from @angular/core (directly or through a namespace import), appended after the member queries. It raises ngtsc's diagnostics word for word:

  • not an object literal
  • not a query type
  • missing or extra arguments
  • non-object options
  • a non-boolean descendants, static or emitDistinctChangesOnly
  • predicates that can't be interpreted
  • a signal query for the same property (only Angular's own viewChild/contentChild..., imported from @angular/core directly, with an alias or through a namespace)

An option or predicate element imported from another file ({descendants: FLAG}, new ViewChild([...SELS])) gets the same message as in #493 (... depends on 'FLAG', which is imported from another module. OXC compiles one file at a time ...) instead of ngtsc's wrong-type error for a value oxc never read.

Member query decorators now also match ngtsc:

  • @ViewChild/@ViewChildren honour descendants and emitDistinctChangesOnly. They were hard-coded before, so this changes the emitted query flags for code that sets them.
  • A predicate referencing a same-file string const (@ViewChild(SELECTOR)) compiles to that selector instead of a type predicate.
  • forwardRef(function () { return X; }), and forwardRef behind parentheses or as, is unwrapped like the arrow form. ns.forwardRef(...) is only unwrapped when ns is a namespace import of @angular/core; util.forwardRef(() => X) is kept as the predicate. A bare forwardRef is unwrapped only when it's imported from @angular/core (aliases too), like ngtsc; the public extract_view_queries / extract_content_queries, which don't get the file's imports, still match it by name.

extract_class_queries returns a class's queries as they're compiled. The compiler and the NAPI extractComponentMetadataSync both use it, so the metadata API now reports the same queries.

Member decorators through a namespace (@core.Input(), @core.ViewChild(), ...) are now matched using the file's imports, passed in through StringConsts (extract_input_metadata_in, extract_output_metadata_in, and the query and host variants), instead of state set by the compiler. The compiler and extractComponentMetadataSync both use them, so the NAPI metadata now includes @core.Input() too, and still ignores a namespace that isn't @angular/core.

Member query decorators read their options through the same code as queries:, like ngtsc's extractDecoratorQueryMetadata: const flags, parentheses and as are read, and wrong-type or non-object options, uninterpretable predicates and wrong argument counts report ngtsc's errors. This also changes emitted flags for code that passes const options. The error check reads queries: with the source text, so a function-expression predicate no longer gets a false "cannot be interpreted" error.

Tests

Added to the ngtsc snapshot: Angular's queries specs (ngtsc_spec's decorator-queries errors and "should generate queries for components/directives", forwardRef, and the authoring_queries_spec collisions), plus probes of every error, the flags, the ordering and forwardRef through local, @angular/core and other namespaces. One probe is skipped: ngtsc also reports its separate check for signal query functions called outside a member initializer, which oxc doesn't implement. A separate test covers the imported-value messages. A new NAPI test covers extractComponentMetadataSync: queries (this failed before the fix), and namespaced member decorators through @angular/core versus another module.

ashley-hunter and others added 6 commits October 1, 2026 04:31
…tions

`@Directive({ queries: { el: new ViewChild('el') } })` (and `@Component`) was
ignored, so those queries never ran. It's now parsed like ngtsc's
`extractQueriesFromDecorator`: `new ViewChild/ViewChildren/ContentChild/
ContentChildren(...)` imported from `@angular/core` (directly or through a
namespace import), appended after the member queries, with ngtsc's diagnostics
word for word (not an object literal, not a query type, missing/extra
arguments, non-object options, non-boolean `descendants`/`static`/
`emitDistinctChangesOnly`, uninterpretable predicates, and a signal query
declared for the same property).

Member query decorators now match ngtsc too:

- `descendants` and `emitDistinctChangesOnly` are honoured on `@ViewChild`/
  `@ViewChildren` (they were hard-coded), which changes the emitted flags
- a predicate referencing a same-file string const (`@ViewChild(SELECTOR)`)
  compiles to that selector rather than to a type predicate
- `forwardRef(function () { return X; })`, and `forwardRef` behind parentheses
  or `as`, is unwrapped like the arrow form

`extract_class_queries` returns a class's queries as they're compiled; the
compiler and the NAPI `extractComponentMetadataSync` both use it, so the
metadata API reports the same queries.
- `ns.forwardRef(...)` is only unwrapped when `ns` is a namespace import of
  `@angular/core`, like ngtsc; `util.forwardRef(() => X)` on any other
  object is kept as the predicate. A bare `forwardRef` is still matched by
  name.
- A `queries:` option or predicate element imported from another file
  (`{descendants: FLAG}`, `new ViewChild([...SELS])`) now says OXC can't
  read other files, as `inputs:`/`outputs:` do, instead of ngtsc's
  wrong-type error for a value it never read.
- Member decorators through a namespace (`@core.Input()`) are matched with
  the file's imports passed in, not a thread-local set by the compiler, so
  `extractComponentMetadataSync` reads them too (and still ignores a
  namespace that isn't `@angular/core`).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y Angular's signal queries

Checked against @angular/compiler-cli 22.1.7 (probes are in the fixture):

- A bare `forwardRef(() => X)` in a query predicate was unwrapped by
  name. Like ngtsc's `tryUnwrapForwardRef`, it's now unwrapped only when
  it's imported from `@angular/core`, under any alias
  (`forwardRef as fr`); a local `forwardRef`, one from another module or
  an undeclared one is kept as written. Without the file's imports (the
  public `extract_view_queries` / `extract_content_queries`), it's still
  matched by name.
- "Query is declared multiple times" fired for any member calling a
  function named `viewChild()`, `contentChild()`, ... It now fires only
  for Angular's, imported from `@angular/core` by name (under any alias)
  or through a namespace import, like the input/output checks. Signal
  query members are still compiled by name, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s:` with the source text

Member query decorators (`@ViewChild`, `@ContentChildren`, ...) now go
through the same code as `queries:` (ngtsc uses one function for both,
`extractDecoratorQueryMetadata`):

- `static`, `descendants` and `emitDistinctChangesOnly` are evaluated, so
  `{emitDistinctChangesOnly: FLAG}` with `const FLAG = false` gives flag 1
  like ngtsc (it was 5), and parenthesized or `as` options are read.
- A non-boolean option, options that aren't an object literal
  (`@ViewChild('x', OPTS)`), a predicate that can't be interpreted, and
  missing or extra arguments report ngtsc's errors, at the node ngtsc
  points to. An imported option reports the other-module error.

The public `extract_*` functions without the file's imports keep reading
literals only.

The diagnostic pass read `queries:` without the source text, so a
function-expression predicate (`new ViewChild(function Token() {})`) was
compiled but also reported as "predicate cannot be interpreted". It now
reads it the way the class is compiled.

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

`queries: {q: new ViewChild('x' || NAME)}`, `@ViewChild('x', {static: true || FLAG})`
and `{descendants: false && FLAG}` now compile like ngtsc (the evaluator fix
below this PR); these snapshot probes pin it.

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

Query selectors, `read` and `static` options read through `typeof NS.X` or
`import S = NS.X` (in `queries:` and on members) now evaluate like ngtsc,
and `NS.C` in a value position stays the expression it is. Add those cases to
the ngtsc snapshot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn
Brooooooklyn force-pushed the feat/decorator-metadata-queries branch from df67666 to 023efb2 Compare October 1, 2026 02:43
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