Skip to content

Angular inject()-bound class fields are not credited as typed instance bindings, so this.field.member chains lose member usage #244

@OmerGronich

Description

@OmerGronich

What happened?

In modern Angular (v14+), services are typically obtained via a class field initialiser:

private readonly inner = inject(InnerService);

Fallow does not recognise this as a typed binding, so any this.inner.member chain fails to credit member as used on InnerService. Every member of an inject()-acquired service that is only consumed via the field chain is reported as an unused class member.

The legacy constructor-parameter form (constructor(private readonly inner: InnerService) {}) does not have this problem.

Reproduction

mkdir -p /tmp/fallow-repro/src && cd /tmp/fallow-repro
// package.json — only the dep entry is needed (so fallow activates the Angular plugin); no install required
{
  "name": "fallow-repro",
  "version": "0.0.0",
  "type": "module",
  "dependencies": { "@angular/core": "^20.0.0" }
}
// src/inner.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class InnerService {
  readonly aaa = 1; // used via this.inner.aaa  → expected NOT reported
  readonly bbb = 2; // used via this.inner.bbb  → expected NOT reported
  readonly ccc = 3; // genuinely unused          → expected reported
}
// src/outer.service.ts
import { computed, inject, Injectable } from '@angular/core';
import { InnerService } from './inner.service';

@Injectable({ providedIn: 'root' })
export class OuterService {
  private readonly inner = inject(InnerService);

  readonly forwardedAaa = this.inner.aaa;
  readonly doubledBbb = computed(() => this.inner.bbb * 2);
}
// src/main.ts
import { OuterService } from './outer.service';
void OuterService;
// .fallowrc.json
{ "entry": ["src/main.ts"] }
fallow dead-code --format json 2>/dev/null \
  | jq '.unused_class_members[] | {parent_name, member_name, line}'

Expected behavior

Only InnerService.ccc is flagged. aaa and bbb are reachable via the inject()-bound inner field on OuterService.

{ "parent_name": "InnerService", "member_name": "ccc", "line": 7 }

(OuterService.forwardedAaa and OuterService.doubledBbb are also flagged, but that is unrelated and expected — void OuterService only references the class identity, not its instance members.)

Actual

aaa, bbb, AND ccc are all reported as unused on InnerService:

{ "parent_name": "InnerService", "member_name": "aaa", "line": 5 }
{ "parent_name": "InnerService", "member_name": "bbb", "line": 6 }
{ "parent_name": "InnerService", "member_name": "ccc", "line": 7 }

Switching OuterService to the legacy form clears the false positives — only ccc is then reported:

constructor(private readonly inner: InnerService) {}

Fallow version

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