Skip to content

fix(resolution): a receiver-less JS/TS call never binds to a method - #1735

Open
danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/1714-bare-call-no-method
Open

fix(resolution): a receiver-less JS/TS call never binds to a method#1735
danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/1714-bare-call-no-method

Conversation

@danusha2345

Copy link
Copy Markdown
Contributor

Fixes #1714. Standalone off main (b9ca4b7), one commit.

What

Two rules for a receiver-less JS/TS call, both read from the call site's own source since the extractor emits this.m() / super.m() under the bare method name:

  1. A bare call never binds to a method (the rule the issue proposes). The text at the ref's column is the call expression; when it starts with the name itself and nothing but whitespace, an operator or an opener precedes it, method nodes leave the candidate set before ranking, and matchFuzzy declines a lone method survivor. serialize(this.raw) inside Record.serialize resolves to the module-scope function serialize; this.serialize() (recursion) and other.serialize() are unchanged.

  2. A name the file binds itself has no cross-file candidate. Once methods stop competing, the function that was out-ranked steps in — on vite, resolve() inside new Promise((resolve, reject) => …) landed on config.ts's resolve. A parameter, a const/let/var (destructuring included), a function or class declared in the file shadows every other file's symbol of that name; none of these is a node the graph holds (a parameter, a const bound to a call result, const updateDiag = () => …), so this is the only way the matcher can know. const { x } = require('./m') / = await import('./m') binds an import, not a shadow, and is excluded; a parameter list has to consist of parameters, so a string argument containing the word does not match.

Both memoised per context and cleared with the other name-matcher memos.

Measured

vitejs/vite 8492422 and a 114-Kotlin / 42-Go / 96-JS Electron app, edge sets keyed with resolvedBy.

Standalone against main:

corpus LOST GAINED
vite 1,018 124
Electron app 185 3

vite's removals by target: import(…)module-runner/runner.ts::import 264, test(…) → a fixture's test 309, resolve(…)PluginContainer.resolve 158, transform(…)PluginContainer.transform 59, next(…)bin/vite.js::next 53, log(…) → a spec's log 46, and the rest of that shape. The Electron app's: const updateDiag = () => shell.updateDiag(…), const logLine = (s) => …, const w = (...frames) => … — every sampled row a local arrow the graph has no node for. The 124 gained on vite are 80 resolve bound by import { resolve } from 'node:path' (the bare-import case #1715 declines) and 41 log(…) onto playground/hmr-ssr/event.d.ts's declare global { let log }, which is the right target; 3 are minified-library noise.

Stacked on #1715 + #1718 + #1720 + #1732 (all four merged, this on top): vite LOST 522, GAINED 42 (the 41 log globals + 1), the app LOST 103, GAINED 3 (minified). The node:path promotions are gone, as expected.

Tests

__tests__/bare-call-no-method.test.ts, whole pipeline over source fixtures: the issue's Record.serialize → free serialize (self-edge gone, function edge present); this.serialize() keeps the self-edge; other.serialize() keeps the method; a Promise-executor resolve, a module-scope const transform = makeTransform() and a factory's const now = options.now || … produce no cross-file edge; a destructured require and a string argument containing the name still resolve across files. resolution.test.ts, frameworks-integration, pr19-improvements, ts-this-field-call: 255 unchanged. tsc clean.

🤖 Generated with Claude Code

`serialize(this.raw)` inside `Record.serialize`, with a module-scope
`function serialize` in the same file, resolved onto the method itself:
both were exact-name candidates, both same-file, and findBestMatch's
line-proximity term always prefers the enclosing method (colbymchenry#1714). In JS/TS a
call written without a receiver cannot reach a method at all — methods
need `this.`, an object, or a bound reference.

The extractor emits `this.m()` and `super.m()` under the bare method name,
so the receiver is read back from the call site's own line (the ref's
column is the start of the call expression): when the text there begins
with the name itself and nothing but whitespace, an operator or an opener
precedes it, the call is bare, and `method` nodes leave the candidate set
before ranking. matchFuzzy declines a lone `method` survivor for the same
ref. `this.serialize()` (recursion) and `other.serialize()` are unchanged.

Standalone on vite this removes 566 method-bound bare calls (`log(…)` onto a
spec file's `log` method, `import(…)` onto a runner method, `resolve(…)`
onto PluginContainer.resolve) and lets 274 previously out-ranked
candidates through — `resolve` bound by `import { resolve } from
'node:path'` and Promise-callback `resolve` parameters — which colbymchenry#1715 and a
local-binding rule are for; measured in the stack it is purely
subtractive.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A bare call resolves onto the enclosing method when a same-file function shares its name

1 participant