Skip to content

fix(unused-class-members): trace Angular signal queries and plural QueryList iteration (#274)#283

Merged
BartWaardenburg merged 2 commits intofallow-rs:mainfrom
ChrisJr404:fix/angular-signal-queries-274
May 5, 2026
Merged

fix(unused-class-members): trace Angular signal queries and plural QueryList iteration (#274)#283
BartWaardenburg merged 2 commits intofallow-rs:mainfrom
ChrisJr404:fix/angular-signal-queries-274

Conversation

@ChrisJr404
Copy link
Copy Markdown
Contributor

Closes #274

unused-class-members already credits methods called through @ViewChild / @ContentChild decorator queries because the property's TS type annotation feeds the bound-member-access pipeline. Six other first-class Angular query patterns were silently missed.

Truth table from the OP

Pattern Method on ChildComponent Before After
viewChild() refreshViewChild not traced traced
viewChildren() refreshViewChildren not traced traced
contentChild() refreshContentChild not traced traced
contentChildren() refreshContentChildren not traced traced
@ViewChild refreshDecoratorViewChild traced traced
@ViewChildren refreshDecoratorViewChildren not traced traced
@ContentChild refreshDecoratorContentChild traced traced
@ContentChildren refreshDecoratorContentChildren not traced traced

Why the existing path missed them

  • Signal-based factories (viewChild, viewChildren, contentChild, contentChildren) return Signal<T> / Signal<readonly T[]>. The property has no explicit TS type annotation, and the call site this.vc()?.method() puts a CallExpression between this.vc and method. static_member_object_name resolved only Identifier / ThisExpression / StaticMemberExpression and returned None for the call-result, so the member access was never emitted.
  • Plural decorator queries (@ViewChildren / @ContentChildren) declare the property as QueryList<T> | undefined. extract_type_annotation_name flattens that to the bare "QueryList" identifier, so the element type was never reachable.
  • forEach arrow callbacks had no resolved type for the iteration parameter c, so c.method() fell on the floor regardless of how the receiver was typed.

Approach

Smallest change that closes the gap, structured to extend the existing binding_target_names machinery rather than introduce a parallel pipeline:

  1. extract_angular_signal_query in helpers.rs recognizes the four signal-query factories and pulls T from either the explicit <T> type argument or the first identifier argument (the locator-class form, contentChild(ChildComponent)).
  2. extract_query_list_element_type peels QueryList<T> out of a TS type annotation, including the nullable QueryList<T> | null|undefined variants. has_angular_plural_query_decorator recognizes @ViewChildren / @ContentChildren.
  3. visit_property_definition registers the element type on ModuleInfoExtractor:
    • Singular signal queries → binding_target_names["this.<name>()"] = T
    • Plural signal queries → iterable_element_types["this.<name>()"] = T
    • Plural decorator queries → iterable_element_types["this.<name>"] = T
  4. static_member_object_name is extended to descend into a zero-argument CallExpression (yielding the synthetic key "this.vc()") and into a ChainExpression, so this.vc()?.method() and this.dvcs?.forEach(...) flow through the pipeline.
  5. visit_call_expression detects <receiver>.forEach (and the optional-chained form), looks the receiver up in iterable_element_types, and binds the arrow callback's first parameter to the element type via the existing flat binding_target_names map. The convention matches the scope-unaware comment already in visit_impl.rs.

Patterns now covered (six new)

  1. this.vc()?.method() where vc = viewChild<T>(...)
  2. this.cc()?.method() where cc = contentChild<T>(...)
  3. this.vcs().forEach(c => c.method()) where vcs = viewChildren<T>(...)
  4. this.ccs().forEach(c => c.method()) where ccs = contentChildren<T>(...)
  5. this.dvcs?.forEach(c => c.method()) where dvcs: QueryList<T> is @ViewChildren-decorated
  6. this.dccs?.forEach(c => c.method()) where dccs: QueryList<T> is @ContentChildren-decorated

Limitations

  • The plural-iteration path matches forEach only. .map, .filter, .find, etc. on a QueryList or viewChildren() result remain untraced. Easy to extend (single match arm) when the demand surfaces; held back here to keep the diff minimal.
  • The element type must be syntactically discoverable from a single TS type argument or argument identifier. Mapped types, wider unions, or generic-parameter-relayed T are not resolved (matches the existing constraints on extract_type_reference_name).
  • forEach callbacks bind only when the parameter is a plain BindingIdentifier. Destructuring patterns (forEach(({ inner }) => ...)) fall back to no binding.

Tests

cargo test -p fallow-extract — 1391 pass.

Adds angular_signal_and_plural_queries_trace_child_method_calls in crates/extract/src/visitor/tests.rs, which is the issue's eight-pattern reproducer verbatim and asserts all eight ChildComponent methods are emitted as MemberAccess { object: "ChildComponent", member: <name> }.

ChrisJr404 and others added 2 commits May 5, 2026 21:09
…tion

The `unused-class-members` analyzer already credited methods called through
`@ViewChild` / `@ContentChild` decorator queries because the property's TS
type annotation was wired into the bound-member-access pipeline. Six other
first-class Angular query patterns were missed:

- `viewChild<T>(...)`, `contentChild<T>(...)` — singular signal factories
  whose initializer return type is `Signal<T>`. The property has no
  explicit type annotation, and the call site `this.vc()?.method()` puts a
  `CallExpression` between `this.vc` and `method`, which the static-member
  resolver did not descend into.
- `viewChildren<T>(...)`, `contentChildren<T>(...)` — plural signal
  factories iterated as `this.vcs().forEach(c => c.method())`. Even with
  a known element type, the arrow's `c` parameter had no resolved type.
- `@ViewChildren` / `@ContentChildren` — plural decorator queries typed
  `QueryList<T> | undefined`. `extract_type_annotation_name` flattens the
  annotation to the bare `"QueryList"` identifier, so the element type
  was never reachable.

Approach (smallest change that closes the gap):

1. Add `extract_angular_signal_query` to recognize the four signal-query
   factories and pull out `T` from either the explicit type argument or
   the first identifier argument (the locator-class form).

2. Add `extract_query_list_element_type` to peel `QueryList<T>` (and the
   nullable `QueryList<T> | null|undefined` variants) out of a TS type
   annotation, and `has_angular_plural_query_decorator` to recognize the
   `@ViewChildren` / `@ContentChildren` decorator forms.

3. In `visit_property_definition`, register the discovered element type
   on `ModuleInfoExtractor`:
   - Singular signal queries → `binding_target_names["this.<name>()"] = T`
   - Plural signal queries  → `iterable_element_types["this.<name>()"] = T`
   - Plural decorator queries → `iterable_element_types["this.<name>"] = T`

4. Extend `static_member_object_name` to descend into a zero-argument
   `CallExpression` (yielding `"this.vc()"`) and into a `ChainExpression`
   so `this.vc()?.method` and `this.dvcs?.forEach(...)` resolve through
   the existing pipeline.

5. In `visit_call_expression`, when the callee is `<receiver>.forEach`
   (or the optional-chained form) and `<receiver>` is a registered
   iterable, bind the arrow callback's first parameter to the element
   type. The flat `binding_target_names` map then drives `c.method()`
   resolution at end-of-visit, matching the existing scope-unaware
   convention noted in the visitor module.

Adds the issue's full eight-pattern Angular reproducer as a single test
asserting that all eight `ChildComponent` methods are traced through
`MemberAccess { object: "ChildComponent", member: <name> }`.

Closes fallow-rs#274
@BartWaardenburg BartWaardenburg force-pushed the fix/angular-signal-queries-274 branch from 4ef1b62 to bd37f52 Compare May 5, 2026 19:16
@BartWaardenburg BartWaardenburg merged commit 2ea51fc into fallow-rs:main May 5, 2026
4 checks passed
@BartWaardenburg
Copy link
Copy Markdown
Collaborator

Merged in 2ea51fc. Thanks @ChrisJr404, this is a great catch. All eight Angular query patterns now feed the bound-member-access pipeline: signal factories viewChild / viewChildren / contentChild / contentChildren plus the @ViewChild / @ViewChildren / @ContentChild / @ContentChildren decorators with QueryList<T>. Methods called via this.vc()?.method() and this.vcs().forEach(c => c.method()) are now credited correctly. The included eight-pattern reproducer test fails on main without the fix, so we have proper regression coverage.

@BartWaardenburg
Copy link
Copy Markdown
Collaborator

Released in v2.65.0. Thanks @ChrisJr404.

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.

Angular signal queries (viewChild/viewChildren/contentChild/contentChildren) and plural decorator queries are not traced for unused-class-members

2 participants