Skip to content

TS/JS: this.field.m() is not resolved when this.field = new X() is assigned outside the constructor #4473

Open

Description

@sub4biz

Summary

Since v0.9.91 (990ab1b7, addressing #4294) the receiver of this.field.m() is typed from the caller's own class and
its bases. The constructor is scanned for this.f = new X() and this.f = <typed parameter>, but the same
assignment in any other method, including a static one, is not recorded, so the call stays unresolved.

This is not a regression: v0.9.90 resolved none of the three shapes below, v0.9.91 resolves the constructor one.
The commit message scopes the constructor deliberately (precision measured as "0 wrong" on TypeORM/NestJS), so this is
a request to extend that scope, not a bug report. Python already resolves the same shape on v0.9.91:
def setup(self): self.repo = RepoA() followed by self.repo.save() in another method produces the calls edge.

Reproduction

Run from a clean checkout of the tag. Each case writes two files and checks for a calls edge .go() -> .save():

import pathlib, tempfile
from graphify.extract import extract

REPOS = "export class RepoA { save(): number { return 1; } }\n"
SVC = {
    "constructor": "constructor() { this.repo = new RepoA(); }\n  go() { return this.repo.save(); }",
    "method": "init() { this.repo = new RepoA(); }\n  go() { return this.repo.save(); }",
    "static": "static init() { this.repo = new RepoA(); }\n  static go() { return this.repo.save(); }",
}
for name, body in SVC.items():
    d = pathlib.Path(tempfile.mkdtemp())
    (d / "repos.ts").write_text(REPOS, encoding="utf-8")
    (d / "svc.ts").write_text(
        'import { RepoA } from "./repos";\nexport class Svc {\n  ' + body + "\n}\n", encoding="utf-8")
    r = extract([d / "repos.ts", d / "svc.ts"], cache_root=d, parallel=False)
    label = {n["id"]: n["label"] for n in r["nodes"]}
    hit = any(e["relation"] == "calls" and label[e["source"]] == ".go()" and label[e["target"]] == ".save()"
              for e in r["edges"])
    print(f"{name:12} go() -> RepoA.save(): {'resolved' if hit else 'NOT resolved'}")
case v0.9.90 v0.9.91
constructor() { this.repo = new RepoA(); } NOT resolved resolved
init() { this.repo = new RepoA(); } NOT resolved NOT resolved
static init() { this.repo = new RepoA(); } NOT resolved NOT resolved

Expected: the init() and static init() cases resolve like the constructor one.

Related

Suggested direction

Collect this.f = new X() from every method of the class into the table the constructor pass already fills
(static methods under the static key), keeping the current precedence: a declared field type, then the
constructor-derived one, then a method assignment. If one class assigns two different types to the same
field, record neither (an assignment in a nested plain function, where this is rebound, should not count).
With exactly this change on top of v0.9.91 the repro above resolves all three cases; a field assigned two different
classes in one class resolves nothing; an assignment in a nested function is ignored; a declared field type wins.

Activity

  1. github-actions commented on Oct 15, 2026

    @github-actions

    Thanks for opening this issue, @sub4biz. A maintainer will take a look soon.

    If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.

    To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.

  2. FolatheDuckofDuckingburg commented on Oct 15, 2026

    @FolatheDuckofDuckingburg
    Contributor

    Your wish is my command
    I'm opening a PR

  3. omsenjalia commented on Oct 18, 2026

    @omsenjalia

    This looks fixed on the v8 branch by 6948da8e (test(ts): cover method-assigned field receivers). Could a maintainer confirm and close?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions