Skip to content

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

@OmerGronich

Description

@OmerGronich

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:

  1. The four modern signal-based query factories — viewChild(), viewChildren(), contentChild(), contentChildren().
  2. 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

  1. 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);
  2. Run fallow dead-code from the project root.

  3. 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

default

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions