fix(js,ts): a nested function takes calls to a name the file imports from a package

When a JavaScript or TypeScript file imports a name from outside the
repository, like import { resolve } from 'node:path', matchByExactName
returns no edge for a bare call of that name (the early return main added
for issue 2101). A call to a function declared inside the calling function
under that name, which shadows the import, lost its edge with it.

The candidates for such a call are now narrowed to the calling file's
declarations nested in a function or method (innerScopeShadows), instead
of giving up; a top-level one is the import's own binding. Lexical
reachability, applied later, keeps only the nested declarations visible
where the call is.

Our changes: tests for a sibling function's nested declaration and one
nested a level below the call, which neither may take the call (both hold
through the reachability rule), and for a call from a function nested in
the declaring one; the CHANGELOG bullet moved next to the JS/TS import
bullets, with credit.

Validation: the tests fail 2 of 6 on main and pass here. A/B against main
on 8 repos, kernel per arm: flask, gin, excalidraw, codegraph, pixijs,
pdf.js and three.js byte-identical; vite gains 1 right link, scanImports'
call to its nested scan() beside import { scan } from
'rolldown/experimental'. Index CPU
time on codegraph, test files full of vitest imports, within noise:
81.7 s to 81.9 s (median of 3, interleaved).

Original PR #1715 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
danusha2345
2026-10-23 18:58:14 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 0d46dba08f
commit 428b3c487d
3 changed files with 125 additions and 4 deletions
+25 -4
View File
@@ -11787,6 +11787,23 @@ function bindsAtSites(source: string, name: string, sites: LocalBindingSites): b
return false;
}
/**
* The calling file's declarations of an out-of-repo imported name that sit
* inside a function or method — only those can shadow the import. A top-level
* one is the import's own binding (`request = require('supertest')`), not a
* shadow of it.
*/
function innerScopeShadows(nodes: Node[], ref: UnresolvedRef, context: ResolutionContext): Node[] {
const local = nodes.filter((n) => n.filePath === ref.filePath);
if (local.length === 0) return local;
const scopes = context.getNodesInFile(ref.filePath).filter((n) => n.kind === 'function' || n.kind === 'method');
return local.filter((n) => scopes.some((s) =>
s.id !== n.id &&
s.startLine <= n.startLine &&
(s.endLine ?? s.startLine) >= (n.endLine ?? n.startLine) &&
(s.startLine < n.startLine || (s.endLine ?? s.startLine) > (n.endLine ?? n.startLine))));
}
/**
* Try to resolve a reference by exact name match
*/
@@ -11814,14 +11831,16 @@ export function matchByExactName(
/^[A-Za-z_$][\w$]*$/.test(ref.referenceName) && isOutOfRepoBinding(ref.referenceName, ref, context)) {
return null;
}
// `import { useQuery } from '@tanstack/react-query'`: the call means the
// package's, and no other file's same-named symbol. A declaration of the
// name in this file's inner scope still shadows the import.
let outOfRepoCall = false;
if (bareJs) {
const storeAction = matchJsStoreBindingCall(ref, context);
if (storeAction) return storeAction;
const returned = matchDestructuredCallResult(ref, context);
if (returned) return returned;
// `import { useQuery } from '@tanstack/react-query'`: the call means the
// package's, and no same-named project symbol.
if (isOutOfRepoBinding(ref.referenceName, ref, context)) return null;
outOfRepoCall = isOutOfRepoBinding(ref.referenceName, ref, context);
}
// Every rule below judges one candidate on its own, so they run as ONE pass,
// the kind/language checks before the ones that read source: a common name
@@ -11833,7 +11852,9 @@ export function matchByExactName(
// Read once, and only for a candidate in another file: it scans the file.
let bareTypeName: boolean | undefined;
const tsBareTypeName = (): boolean => (bareTypeName ??= isTsBareTypeName(ref, context));
const sameName = context.getNodesByName(ref.referenceName);
const sameName = outOfRepoCall
? innerScopeShadows(context.getNodesByName(ref.referenceName), ref, context)
: context.getNodesByName(ref.referenceName);
// `NAME(...)` where NAME is a function-like macro somewhere in the project is
// an expansion or a call to a same-named function — never the macro itself
// (#1839), and never a type that happens to share the name (#2070: expat's