Repository navigation
Conversation
…ed type (TS/JS) A method call on an injected or declared field, like this.userService.findAll(), used to degrade to the bare name findAll (the receiver this.userService is a member_expression, not an identifier, so the receiver was dropped). When several classes define a same-named method, which is the norm in NestJS where every service has findAll/create and every repository has findOne, the bare name bound to an arbitrary one: the real target was left with zero callers and a wrong edge pointed at an unrelated class. This is the misbinding colbymchenry#750 calls out, in the form that dominates TypeScript. Extraction: when a member call's receiver is this.<field>, re-encode it as <field>.method instead of dropping to the bare method name. This mirrors the existing Java this.userbo.toLogin2() unwrap. Resolution: a TS/JS branch in matchMethodCall recovers the field's declared type from the enclosing class, either the constructor parameter property (constructor(private readonly userService: UserService)) or a class-body field (private readonly repo: Repo), and resolves the method on that type via resolveMethodOnType, which validates the method exists on the type (and its supertypes), so a wrong inference yields no edge rather than a wrong one. When a per-app type repeats across a monorepo, the candidate is disambiguated by directory proximity to the call site. When the type cannot be recovered (untyped or external/generic field), nothing is forced and the ref falls through to the existing strategies unchanged. Validated graph-level (baseline build vs this build) on real open-source NestJS repos. Node count identical in every repo (the fix adds no nodes). Corrected call edges retargeted from a wrong/self binding to the injected Service/Repository/Reflector, verified against source; the edges that disappear are bare-name mis-binds to external-library methods (Mongoose Model, TypeORM Repository, RxJS Subject.next) and cross-tier frontend/backend collisions. An audit of the testing-nestjs drops found 7/7 were wrong edges, zero correct edges lost.
f505de3 to
af0d820
Compare
|
@colbymchenry when you have a moment, would appreciate your review on this one. It ports the Java/Kotlin field-injection resolution to TS/JS: Validated graph-level ( |
|
This is still live on Same defect, reported again since this PR opened. #1496 (2026-08-01) reports The RCA in #1496 lands exactly where this PR's extraction hunk does. @inth3shadows forced the calls to stay unresolved and read Not superseded. I checked It is also the same shape as work already in flight. #1599 resolves Rust I have not run this branch (it needs a rebase first), so this is a read of the diff and of current |
|
Thank you for this, @tqrcisio, and for the careful A/B across NestJS repos! Main fixed the same bug in #1792 (shipped in 1.6.1), with a native-kernel twin and later follow-ups for base classes and injected fields. Your PR came about three months earlier and measured the same problem independently. We checked your test's case and five more on current main, and all reach the right method, so we're closing this as covered. |
Problem
A method call on an injected or declared field, like
this.userService.findAll(), degrades to the bare namefindAllduring extraction: the receiverthis.userServiceis amember_expression, not an identifier, so the receiver is dropped. When several classes define a same-named method, which is the norm in NestJS where every service hasfindAll/createand every repository hasfindOne, the bare name binds to an arbitrary one. Two failure modes follow, both visible in the graph:This is the misbinding #750 describes, in the shape that dominates real-world TypeScript. Minimal repro (two services sharing
findAll):Fix
Mirrors the existing Java
this.userbo.toLogin2()handling, in two parts:Extraction (
tree-sitter.ts): when a member call's receiver isthis.<field>, re-encode the call as<field>.methodinstead of dropping to the bare method name. Deeper chains (this.a.b.method()) keep the bare name.Resolution (
name-matcher.ts): a TS/JS branch inmatchMethodCallrecovers the field's declared type from the enclosing class and resolves the method on it. Two declaration sites are covered, both ubiquitous in NestJS:constructor(private readonly userService: UserService), read from the constructor method's signature;private readonly repo: Repo;, read from thepropertynode's signature.Resolution goes through
resolveMethodOnType, which validates the method exists on the inferred type (and its supertypes), so a wrong inference yields no edge rather than a wrong one. When a per-app type repeats across a monorepo (apps/billingandapps/admineach with aUserService), the candidate is disambiguated by directory proximity to the call site, the same file-path proximity the bare-name path relied on before re-encoding.When the field's type cannot be recovered (untyped field, external or generic type), nothing is forced: the ref falls through to the existing strategies, so behavior there is unchanged.
Validation
Graph-level A/B, baseline build (
main) vs this build, indexing five open-source NestJS repos. Node count is identical in every repo (the fix adds no nodes):server/)packages/twenty-server/)About 4,100 call edges across the five repos are retargeted from a wrong/self binding to the correct injected dependency. Verified against source:
UserAdminService::getSessions:this.sessionRepository.getByUserId(id)was bound toApiKeyRepository::getByUserId, nowSessionRepository::getByUserId(fieldsessionRepository: SessionRepository).BillingGaugeService:this.twentyConfigService.get(...)was bound toApplicationConnectionsController::get(a controller), nowTwentyConfigService::get.RolesGuard::canActivate:this.reflector.get(...)was bound toConfigService::get, nowReflector::get.PortfolioController::getHoldings: was a self-loop, nowPortfolioService::getHoldings.The dropped edges are bare-name mis-binds, not lost-correct edges. Audited jmcdo29/testing-nestjs: 7/7 drops were calls on an external library type, Mongoose
Model<CatDoc>and TypeORMRepository<Cat>, whosecreate/update/findOne/removehad been bound to same-named controller methods. ghostfolio's drops are dominated by Angular frontend components whosethis.x.get()was bound to a backend service or a test mock (HttpClientMock::get); nest's are coincidental binds on common names (this.subject.next()toExceptionsHandler::next). The net edge-count decrease is wrong-edge removal plus correct retargeting; the raw counts understate the precision gain, because both a corrected edge and a removed wrong edge lower the total.Tests
__tests__/ts-di-method-resolution.test.ts(new): constructor-injected and class-body-field resolution, same-name disambiguation, and the type-validation path. The existingsame-name-disambiguation.test.ts(#764) keeps passing: its NestJS monorepo fixture is exactly this scenario. The only failures on my machine are the pre-existing timing-sensitive mcp-daemon / ppid-watchdog / sync tests, which pass in isolation on a clean checkout as well.