feat(resolution): resolve this.<field>.method() on the field's declared type (TS/JS)#920
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 ( |
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.