fix(resolution): type an awaited receiver instead of guessing a same-named method (#1840) - #1855
Open
maxmilian wants to merge 1 commit into
Open
Conversation
…named method
`const listed = await listPaths()` is neither `= new X()` nor `: X`, so the
TS/JS declaration patterns left the receiver untyped. `listed.split('\0')`
then fell through to the bare-name strategy that resolves a method when the
project declares exactly one with that name — recording a persisted call edge
into an unrelated class's `split` at confidence 0.7.
Give the local-receiver inference a second chance for JS/TS: follow an awaited
bare callee to its declared return annotation and unwrap one `Promise<...>`
layer. The annotation is read from the callee's declaration line rather than
from node metadata, because the TS/JS extractors do not populate `returnType`
(only Java, Go, C#, Swift, Dart, ObjC and Pascal do) and teaching them to
would change extraction output for every TypeScript index.
A primitive then joins the builtin receivers that decline rather than guess
(colbymchenry#1566): a receiver typed `string` calls a built-in string method, never a
project method. `TS_PRIMITIVE_TYPES` stays separate from `JS_BUILT_INS` —
those are runtime globals a receiver can be constructed from, and the set is
also read elsewhere in resolution.
The second chance runs only after the declaration patterns miss, and its
result is memoized per (file, scope, receiver) like the declaration scan it
follows, so the hot path keeps paying one scan per key rather than one per ref.
Both directions are covered: the false edge is gone, and a receiver bound to
`await makeEngine()` now resolves `Engine::run`, which previously resolved to
nothing at all.
Fixes colbymchenry#1840
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1840
The path
const listed = await listPaths()is neither= new X()nor: X, so the TS/JS declaration patterns leave the receiver untyped.listed.split('\0')then reaches the bare-name strategy that resolves a method when the project declares exactly one with that name, and records a persisted edge intoPaneManager::splitat confidence 0.7 — theresolvedBy: "instance-method"/refName: "listed.split"in the report.The
#1566guard that already declines a builtin receiver cannot help here: it needs an inferred type to fire, and inference produced nothing.The change
Second chance for the local-receiver inference, JS/TS only, only after the declaration patterns miss. Follow an awaited bare callee to its declared return annotation and unwrap one
Promise<...>layer.The annotation is read from the callee's declaration line rather than from node metadata. The TS/JS extractors do not populate
returnType— only Java, Go, C#, Swift, Dart, ObjC and Pascal implementgetReturnType— and teaching them to would change extraction output for every TypeScript index and need a version bump. A bounded source scan is already the idiom next door (inferPhpAssignedPropertyType), so this stays inside resolution.Only a bare callee qualifies.
await svc.load()names a member whose owner would itself have to be inferred first, and guessing there is the mistake being fixed.A primitive then joins the builtin receivers that decline rather than guess. A receiver typed
stringcalls a built-in string method, never a project method.TS_PRIMITIVE_TYPESis deliberately separate fromJS_BUILT_INS: those are runtime globals a receiver can be constructed from, and that set is also read inresolution/index.ts, so widening it would change behavior beyond this path.Precision and recall both move the right way
The second test is the one worth reading. With a receiver whose name does not capitalize onto a class (so the capitalized-receiver strategy cannot carry it):
On
mainthat resolves to nothing —Engine::runis simply missing from the graph. With this change it resolves. So this is not only the removal of a false edge; the same inference recovers a true one that the guessing strategies never found.On the hot path (red line 3)
inferLocalReceiverTyperuns for everyreceiver.method()ref, which is why the declaration scan carriesINFER_SCAN_STATES. The new lookup follows the same discipline: it runs only after the declaration patterns miss, and its result — includingnull, which is the common case — is memoized per(file, scope, language, receiver)in a per-contextWeakMap, cleared alongside the others inclearNameMatcherMemos. One scan and one node lookup per key, not per ref.Relationship to #1844
They do not overlap, and I checked rather than assumed: applying the failing test below to #1844's head (
95dccf9) still fails, so that PR does not cover this case. #1844 rewrites nested-receiver evidence (holder.values.get(...),Map.get/set/has) and bumps extraction 26 → 27; this change touches neither the extractor nor the extraction version, and its edit tojs-builtins.tsis a new appended export. If #1844 lands first I am happy to rebase.Validation
Node 24.19.0, macOS.
__tests__/resolution.test.ts, written before the fix, both red onmain:maingives['PaneManager::split', 'listPaths'], expected['listPaths'];maingives['makeEngine'], expected['Engine::run', 'makeEngine'].__tests__/resolution.test.tsin full: 212 passed, including the#1566/#1790builtin-receiver tests this sits next to.tsc --noEmit: clean.cli-*suites shell out tonodeand need adistbuild, and this machine's default Node is 26 while the package requires<25. They are not touched by this change either way.🤖 Generated with Claude Code
https://claude.ai/code/session_01K5hLnmSWco1QjCAgMcWdcA