fix(js,ts): a function value imported from the project links only to what its import binds (#2673)

A function passed as a value whose name the file imports from its own
project still fell back to the project's one function of that name
whenever the import resolver bound the name to something that is not a
function: outline's `isCloudHosted` constant landed on the server's
`isCloudHosted()` check, its `Star` model class on the `<Star>` icon,
`import * as Slack` on the Slack settings component, and topcoder's
`import TCO21 from './themes/TCO21.scss'` on a route component.

matchFunctionRef's unique cross-file fallback now also declines when a
project import binds the name to something else (importBindsOther): a
namespace import, an asset import (stylesheet, image, GraphQL, JSON,
font, media; judged by the specifier's extension), or an import the
import resolver binds to a node that is not a function or method. A
wrapper declared beside the namesake (`export default
styled(Disclosure)`) and a lazy loader (`lazy(() => import(...))`)
still lead to it, and an import the resolver can't follow at all keeps
the fallback.

Follow-up to #2671.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 18:43:16 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent a50104c895
commit ab6ec218ff
3 changed files with 256 additions and 1 deletions
+30 -1
View File
@@ -1198,9 +1198,11 @@ export function matchFunctionRef(
// outside the repository, which no project function is: outline's
// `styled(Link)`, with `Link` from react-router-dom, is not an email
// template's `Link`, nor is cord-field's lodash `noop` a test's own stub.
// Nor a name a project import binds to something else (importBindsOther).
if (candidates.length === 1 && (named.length === 1 ||
(ref.language === 'python' && pythonFromImports(ref.filePath, context).has(ref.referenceName))) &&
!(JS_FAMILY.has(ref.language) && isOutOfRepoBinding(ref.referenceName, ref, context))) {
!(JS_FAMILY.has(ref.language) && isOutOfRepoBinding(ref.referenceName, ref, context)) &&
!(JS_FAMILY.has(ref.language) && importBindsOther(candidates[0]!, ref, context))) {
return {
original: ref,
targetNodeId: candidates[0]!.id,
@@ -14996,6 +14998,33 @@ function isOutOfRepoBinding(name: string, ref: UnresolvedRef, context: Resolutio
return !!binding && context.isOutOfRepoImport?.(binding.source, ref.filePath, ref.language) === true;
}
/** A module a bundler loads as data, not code: a stylesheet, an image, a GraphQL document, JSON, a font. */
const ASSET_MODULE = /\.(?:css|scss|sass|less|styl|stylus|pcss|graphql|gql|png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?|json5?|ya?ml|toml|md|mdx|txt|html?|xml|csv|woff2?|ttf|otf|eot|mp3|mp4|webm|ogg|wav|flac|pdf|wasm)$/i;
/**
* Does a project import bind `ref`'s name to something other than `candidate`,
* a function of that name in another file? A namespace import is the module
* object, and an asset import is what the bundler makes of the file: topcoder's
* `import TCO21 from './themes/TCO21.scss'` is no `TCO21()` route. An import
* the import resolver binds to a constant, class or component names that:
* outline's `isCloudHosted` constant is not the server's `isCloudHosted()`.
* Two bindings still lead to the namesake: a wrapper declared beside it
* (`export default styled(Disclosure)`), and a lazy loader (signoz's
* `Loadable(() => import('pages/Home'))`), which loads the page it is named
* after. An import the resolver can't follow at all says nothing.
*/
function importBindsOther(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (!/^[A-Za-z_$][\w$]*$/.test(ref.referenceName)) return false;
const binding = context.getImportMappings(ref.filePath, ref.language).find((m) => m.localName === ref.referenceName);
if (!binding) return false;
if (binding.isNamespace || ASSET_MODULE.test(binding.source.replace(/[?#].*$/, ''))) return true;
const bound = context.resolveImport?.(ref);
const node = bound ? context.getNodeById?.(bound.targetNodeId) : null;
if (!node || node.kind === 'function' || node.kind === 'method' || node.filePath === candidate.filePath) return false;
const lines = (context.getFileLines?.(node.filePath) ?? context.readFile(node.filePath)?.split(/\r?\n/)) ?? [];
return !/\bimport\s*\(/.test(lines.slice(node.startLine - 1, Math.min(node.endLine, node.startLine + 15)).join('\n'));
}
/** The directory of a Go file, which is its package: Go keeps one package per directory. */
function goPackageDir(filePath: string): string {
return path.posix.dirname(filePath.replace(/\\/g, '/'));