Repository navigation
fix(js,ts): a function value imported from a package no longer links a project function of that name - #2671
Merged
Conversation
… project function of that name
A function passed as a value (`styled(Link)`, `createContext({ onClose:
noop })`, `Chart.register(Legend)`) whose name the file imports from a
package is that package's function. matchFunctionRef still bound it to
the one project function of that name whenever there was exactly one:
outline's react-router-dom `Link` landed on an email template's
`Footer.tsx:Link`, cord-field's lodash `noop` on a test's own stub.
The cross-file fallback now consults the same out-of-repo guard the
call and value paths use (isOutOfRepoBinding). A same-file declaration
still wins (a local that shadows the import is what the name means
there), a project import still resolves through its import, and a
specifier no package.json declares may be an alias and stays the
project's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inks a project namesake Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 19, 2026
…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 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.
What was wrong
A function passed as a value —
styled(Link),createContext({ onClose: noop }),Chart.register(Legend),{ icon: Users }— whose name the file imports from a package is that package's function.matchFunctionRef's cross-file fallback still bound it to the project's one function of that name whenever there was exactly one:styled(Link)withLinkfromreact-router-dom→server/emails/templates/components/Footer.tsx:Link(18 files: breadcrumbs, menus, document lists, sharing).noop→ a test file's ownconst noop = () => {}stub inUserForm.test.tsx; MUI'sTypography→Typography.stories.tsx.Card/Tooltip/Input as InputComponent, chart.js'sLegend/Tooltip,@signozhq/icons'BarChart/Histogram→ project chart and periscope components.Calls and other value references already consult the out-of-repo guard (
isOutOfRepoBindinginmatchByExactNameand the fuzzy fallback); function values did not.The fix
src/resolution/name-matcher.ts,matchFunctionRef: the unique cross-file fallback now declines when the file binds the name by an importcontext.isOutOfRepoImportcalls outside the repository (a package apackage.jsonup the tree declares, or a Node built-in) — the same predicate the call/value paths use, including #2437's handling of catch-all andnode_modulespath aliases. JS-family languages only, like the other call sites.Unchanged, by design:
const onValue = (snap) => …passed tofns.onValue(ref, onValue, onError)besideimport { onValue } from 'firebase/database'keeps its edge);resolveViaImportfirst;package.jsondeclares may be an alias the resolver doesn't read (SvelteKit's$lib, a nested app's@/), so it keeps the name fallback.A CommonJS
const { noop } = require('lodash')never reaches this path: the extraction gate keeps only ES-import names and same-file declarations as function-value candidates.Tests
__tests__/function-ref-package-imports.test.ts:styled(Link)), a named import (noop), a package-path default import (lodash/debounce) and a Node built-in (node:util'sformat), each beside a unique project function of that name → no edge;"*": ["./typings/*"]) → no edge, while the test file's ownnoopstill links to itself;$lib/...specifier, and a nested declaration that shadows the package import.Red on main 3695818 (the first two fail with exactly the edges above), green with the fix.
Full suite on this branch at main 3695818 (Windows 11, kernel built from the tree,
CODEGRAPH_KERNEL_EXPECT=1,--maxWorkers=4 --minWorkers=1): 613 files passed, 6 skipped (POSIX-only process-lifecycle suites); 7,480 tests passed, 0 failed, 80 skipped. Synced afterwards with main 1aebd3a (#2670, React Router<Outlet />,react.tsonly, which function values never reach);tsc --noEmit, this PR's test,function-ref.test.tsand #2670'sreact-router-outlet-routes.test.tsre-run green on the merged tree.A/B on real repos
23 repos (the #2437 catch-all kit's 18, plus signoz, bulletproof-react, mantis, vanlife and react.dev), each with a stubbed
node_modules(a folder per declared package) so resolution sees an installed checkout. Both arms are this branch on main 3695818 with the same kernel, identical exceptdist/resolution/name-matcher.js(main's compiled into the before arm). Every edge compared by source, target, kind, position and metadata. Node counts are identical in every repo.Link×18 →server/emails/templates/components/Footer.tsx:Link; prosemirror-commands'newlineInCode→ outline's ownshared/editor/commands/codeFence.ts:newlineInCode; date-fns'trlocale (tr_TR: tr) ×2 →shared/test/editor.ts:trCard×9 →periscope/components/Card/Card.tsx:Card; antdTooltipand chart.jsLegend×2 /Tooltip(Chart.register(…)) → uPlotV2'sLegend/Tooltip; antdInput as InputComponent→components/Input/index.tsx:InputComponent;@signozhq/iconsBarChart×8 /Histogram×2 → the visualization chart componentsnoop×9 and ts-essentialsnoop→UserForm.test.tsx:noop, a test's own stub; MUITypography×7 →Typography.stories.tsx:Typography; MUI iconHome→ the Home scene;@editorjs/headerdefault ×2 → the app'sHeaderSwitch(in the reactive component maps) → legend-state's ownsrc/react/Switch.tsx:SwitchUsersicon (icon: Users) in all three apps → the Next.js app'susers.tsx:Userspage componentimport * as MermaidToExcalidraw from '@excalidraw/mermaid-to-excalidraw'handed to a mock → the project'sMermaidToExcalidrawcomponentSwitchinexpect(routeSwitch.type).toBe(Switch)→ the project'sSwitchtoggleidentity({ enter: identity, exit: identity }) →libs/common/src/tools/rx.ts:identityTriage: all 75 removed edges are wrong. Each is a
function-refedge (confidence 0.8, the unique cross-file fallback) whose name the source file imports from a package, landing on an unrelated project function. Spot-checked in source: legend-state's multi-linereact-nativeimport, topcoder'stoBe(Switch), signoz'sChart.register(…)from chart.js andstyled(InputComponent), excalidraw's namespace import, bitwarden's rxjsidentity, outline's date-fns locale map and prosemirror import, cord-field's EditorJS tools map. The same 75 edges came out of an earlier run on main 2c807f5.Residual: after the fix, the only name-matched function value still bound through a package import in all 23 repos is legend-state's
src/sync-plugins/firebase.ts:375. Thereconst onValue = (snap) => …(line 363) shadowsimport { onValue } from 'firebase/database', so the same-file rule keeping that edge is right.Cost: none measurable. Index times match within run-to-run noise (outline 21 s / 21 s, signoz 26 s / 23 s, bitwarden 66 s / 68 s). The check runs only on the unique cross-file branch and reads the file's cached import mappings.
Not in this PR (follow-up)
A project import that the import resolver doesn't take as a function still falls through to the same unique-namesake fallback, and this PR leaves project imports as they are. The residual scan shows the shape: topcoder's
{ TCO21 } from './themes/TCO21.scss'→src/shared/routes/Communities/TCO21/Routes.jsx:TCO21, cord-field's{ CreateStory } from './CommunityStoryStep.graphql'→ aCreateStorycomponent, bitwarden's eslint specs'{ errorMessage } from './no-bit-dialog-wrapper.mjs'→ a web-app step'serrorMessage, outline'sLinkfrom../marks/Link(a class) → the email template'sLink. Many edges in that bucket are right, though (outline'sInputfrom./Input→Input.tsx:Input), so it needs its own rule and its own A/B. It follows as a separate PR.Coordination
jsxChild+ the Vue template resolver); this PR touches onlymatchFunctionRef.jsxChild, default-export reading) is orthogonal: this gate fires only for package bindings, which never reach a project module's default export.🤖 Generated with Claude Code