fix(unused-class-members): trace Angular signal queries and plural QueryList iteration (#274)#283
Merged
BartWaardenburg merged 2 commits intofallow-rs:mainfrom May 5, 2026
Conversation
…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
4ef1b62 to
bd37f52
Compare
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 |
Collaborator
|
Released in v2.65.0. Thanks @ChrisJr404. |
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.
Closes #274
unused-class-membersalready credits methods called through@ViewChild/@ContentChilddecorator 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
ChildComponentviewChild()refreshViewChildviewChildren()refreshViewChildrencontentChild()refreshContentChildcontentChildren()refreshContentChildren@ViewChildrefreshDecoratorViewChild@ViewChildrenrefreshDecoratorViewChildren@ContentChildrefreshDecoratorContentChild@ContentChildrenrefreshDecoratorContentChildrenWhy the existing path missed them
viewChild,viewChildren,contentChild,contentChildren) returnSignal<T>/Signal<readonly T[]>. The property has no explicit TS type annotation, and the call sitethis.vc()?.method()puts aCallExpressionbetweenthis.vcandmethod.static_member_object_nameresolved onlyIdentifier/ThisExpression/StaticMemberExpressionand returnedNonefor the call-result, so the member access was never emitted.@ViewChildren/@ContentChildren) declare the property asQueryList<T> | undefined.extract_type_annotation_nameflattens that to the bare"QueryList"identifier, so the element type was never reachable.forEacharrow callbacks had no resolved type for the iteration parameterc, soc.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_namesmachinery rather than introduce a parallel pipeline:extract_angular_signal_queryinhelpers.rsrecognizes the four signal-query factories and pullsTfrom either the explicit<T>type argument or the first identifier argument (the locator-class form,contentChild(ChildComponent)).extract_query_list_element_typepeelsQueryList<T>out of a TS type annotation, including the nullableQueryList<T> | null|undefinedvariants.has_angular_plural_query_decoratorrecognizes@ViewChildren/@ContentChildren.visit_property_definitionregisters the element type onModuleInfoExtractor:binding_target_names["this.<name>()"] = Titerable_element_types["this.<name>()"] = Titerable_element_types["this.<name>"] = Tstatic_member_object_nameis extended to descend into a zero-argumentCallExpression(yielding the synthetic key"this.vc()") and into aChainExpression, sothis.vc()?.method()andthis.dvcs?.forEach(...)flow through the pipeline.visit_call_expressiondetects<receiver>.forEach(and the optional-chained form), looks the receiver up initerable_element_types, and binds the arrow callback's first parameter to the element type via the existing flatbinding_target_namesmap. The convention matches the scope-unaware comment already invisit_impl.rs.Patterns now covered (six new)
this.vc()?.method()wherevc = viewChild<T>(...)this.cc()?.method()wherecc = contentChild<T>(...)this.vcs().forEach(c => c.method())wherevcs = viewChildren<T>(...)this.ccs().forEach(c => c.method())whereccs = contentChildren<T>(...)this.dvcs?.forEach(c => c.method())wheredvcs: QueryList<T>is@ViewChildren-decoratedthis.dccs?.forEach(c => c.method())wheredccs: QueryList<T>is@ContentChildren-decoratedLimitations
forEachonly..map,.filter,.find, etc. on aQueryListorviewChildren()result remain untraced. Easy to extend (single match arm) when the demand surfaces; held back here to keep the diff minimal.Tare not resolved (matches the existing constraints onextract_type_reference_name).forEachcallbacks bind only when the parameter is a plainBindingIdentifier. 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_callsincrates/extract/src/visitor/tests.rs, which is the issue's eight-pattern reproducer verbatim and asserts all eightChildComponentmethods are emitted asMemberAccess { object: "ChildComponent", member: <name> }.