What happened?
Fallow's unused-class-members analyzer recognises a method call routed through Angular's single-instance decorator queries (@ViewChild, @ContentChild) but misses the equivalent calls routed through:
- The four modern signal-based query factories —
viewChild(), viewChildren(), contentChild(), contentChildren().
- The two plural decorator queries that produce a
QueryList<T> — @ViewChildren, @ContentChildren — when the methods are invoked through QueryList.forEach(c => c.method()).
All of these are first-class, idiomatic Angular APIs that produce the same runtime behaviour as @ViewChild: a property holding a child-component reference (or a list of references) whose methods are called from the parent.
Reproduction
-
Create the four files below in an empty directory.
package.json
{
"name": "fallow-repro-angular-queries",
"version": "0.0.0",
"private": true,
"dependencies": {
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
.fallowrc.json
{
"entry": ["src/main.ts"]
}
src/child.component.ts
import { Component } from '@angular/core';
@Component({ selector: 'app-child', template: '<ng-content />' })
export class ChildComponent {
refreshViewChild(): void {
console.log('called via viewChild()');
}
refreshViewChildren(): void {
console.log('called via viewChildren()');
}
refreshContentChild(): void {
console.log('called via contentChild()');
}
refreshContentChildren(): void {
console.log('called via contentChildren()');
}
refreshDecoratorViewChild(): void {
console.log('called via @ViewChild');
}
refreshDecoratorViewChildren(): void {
console.log('called via @ViewChildren');
}
refreshDecoratorContentChild(): void {
console.log('called via @ContentChild');
}
refreshDecoratorContentChildren(): void {
console.log('called via @ContentChildren');
}
}
src/parent.component.ts
import {
Component,
ContentChild,
ContentChildren,
QueryList,
ViewChild,
ViewChildren,
contentChild,
contentChildren,
viewChild,
viewChildren
} from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
imports: [ChildComponent],
template: `
<app-child #vc />
<app-child #vcs />
<app-child #dvc />
<app-child #dvcs />
<ng-content />
`
})
export class ParentComponent {
// Modern signal queries
readonly vc = viewChild<ChildComponent>('vc');
readonly vcs = viewChildren<ChildComponent>('vcs');
readonly cc = contentChild<ChildComponent>(ChildComponent);
readonly ccs = contentChildren<ChildComponent>(ChildComponent);
// Legacy decorator queries
@ViewChild('dvc') readonly dvc?: ChildComponent;
@ViewChildren('dvcs') readonly dvcs?: QueryList<ChildComponent>;
@ContentChild(ChildComponent) readonly dcc?: ChildComponent;
@ContentChildren(ChildComponent) readonly dccs?: QueryList<ChildComponent>;
triggerRefresh(): void {
// Signal-based call sites
this.vc()?.refreshViewChild();
this.vcs().forEach((c) => c.refreshViewChildren());
this.cc()?.refreshContentChild();
this.ccs().forEach((c) => c.refreshContentChildren());
// Decorator-based call sites
this.dvc?.refreshDecoratorViewChild();
this.dvcs?.forEach((c) => c.refreshDecoratorViewChildren());
this.dcc?.refreshDecoratorContentChild();
this.dccs?.forEach((c) => c.refreshDecoratorContentChildren());
}
}
src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { ParentComponent } from './parent.component';
bootstrapApplication(ParentComponent);
-
Run fallow dead-code from the project root.
-
Output (fallow 2.63.0, compact format):
unused-class-member:src/child.component.ts:5:ChildComponent.refreshViewChild
unused-class-member:src/child.component.ts:9:ChildComponent.refreshViewChildren
unused-class-member:src/child.component.ts:13:ChildComponent.refreshContentChild
unused-class-member:src/child.component.ts:17:ChildComponent.refreshContentChildren
unused-class-member:src/child.component.ts:25:ChildComponent.refreshDecoratorViewChildren
unused-class-member:src/child.component.ts:33:ChildComponent.refreshDecoratorContentChildren
unused-class-member:src/parent.component.ts:40:ParentComponent.triggerRefresh
Tabulating which of the eight call sites in triggerRefresh are traced:
| Pattern |
Method on ChildComponent |
Traced? |
viewChild() |
refreshViewChild |
❌ |
viewChildren() |
refreshViewChildren |
❌ |
contentChild() |
refreshContentChild |
❌ |
contentChildren() |
refreshContentChildren |
❌ |
@ViewChild |
refreshDecoratorViewChild |
✅ |
@ViewChildren |
refreshDecoratorViewChildren |
❌ |
@ContentChild |
refreshDecoratorContentChild |
✅ |
@ContentChildren |
refreshDecoratorContentChildren |
❌ |
ParentComponent.triggerRefresh is a separate noise issue — bootstrapped-component lifecycle/event-handler members have no static caller; not the focus of this report.
Expected behavior
The four signal-query factories (viewChild, viewChildren, contentChild, contentChildren) and the two plural decorator queries (@ViewChildren, @ContentChildren) should be treated equivalently to the already-supported @ViewChild / @ContentChild decorators. A method call reachable from any of the following expressions should count as a reference to that method on the parameterised component type:
this.<prop>()?.method() — single-instance signal query
this.<prop>().forEach(c => c.method()) and similar QueryList/Signal<readonly T[]> iteration helpers — plural signal query
this.<prop>?.forEach(c => c.method()) — QueryList<T> from @ViewChildren / @ContentChildren
Fallow version
fallow 2.63.0
Operating system
macOS
Configuration
What happened?
Fallow's
unused-class-membersanalyzer recognises a method call routed through Angular's single-instance decorator queries (@ViewChild,@ContentChild) but misses the equivalent calls routed through:viewChild(),viewChildren(),contentChild(),contentChildren().QueryList<T>—@ViewChildren,@ContentChildren— when the methods are invoked throughQueryList.forEach(c => c.method()).All of these are first-class, idiomatic Angular APIs that produce the same runtime behaviour as
@ViewChild: a property holding a child-component reference (or a list of references) whose methods are called from the parent.Reproduction
Create the four files below in an empty directory.
package.json{ "name": "fallow-repro-angular-queries", "version": "0.0.0", "private": true, "dependencies": { "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0" } }tsconfig.json{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "skipLibCheck": true }, "include": ["src/**/*.ts"] }.fallowrc.json{ "entry": ["src/main.ts"] }src/child.component.tssrc/parent.component.tssrc/main.tsRun
fallow dead-codefrom the project root.Output (
fallow 2.63.0, compact format):Tabulating which of the eight call sites in
triggerRefreshare traced:ChildComponentviewChild()refreshViewChildviewChildren()refreshViewChildrencontentChild()refreshContentChildcontentChildren()refreshContentChildren@ViewChildrefreshDecoratorViewChild@ViewChildrenrefreshDecoratorViewChildren@ContentChildrefreshDecoratorContentChild@ContentChildrenrefreshDecoratorContentChildrenParentComponent.triggerRefreshis a separate noise issue — bootstrapped-component lifecycle/event-handler members have no static caller; not the focus of this report.Expected behavior
The four signal-query factories (
viewChild,viewChildren,contentChild,contentChildren) and the two plural decorator queries (@ViewChildren,@ContentChildren) should be treated equivalently to the already-supported@ViewChild/@ContentChilddecorators. A method call reachable from any of the following expressions should count as a reference to that method on the parameterised component type:this.<prop>()?.method()— single-instance signal querythis.<prop>().forEach(c => c.method())and similarQueryList/Signal<readonly T[]>iteration helpers — plural signal querythis.<prop>?.forEach(c => c.method())—QueryList<T>from@ViewChildren/@ContentChildrenFallow version
fallow 2.63.0
Operating system
macOS
Configuration
default