Skip to content

fix(js,ts): a function value imported from a package no longer links a project function of that name - #2671

Merged
colbymchenry merged 7 commits into
mainfrom
claude/angry-wu-cf3be0
Oct 19, 2026
Merged

colbymchenry merged 7 commits into
mainfrom
claude/angry-wu-cf3be0

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

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:

  • outline: styled(Link) with Link from react-router-dom → server/emails/templates/components/Footer.tsx:Link (18 files: breadcrumbs, menus, document lists, sharing).
  • cord-field: lodash's / ts-essentials' noop → a test file's own const noop = () => {} stub in UserForm.test.tsx; MUI's Typography → Typography.stories.tsx.
  • signoz: antd's Card/Tooltip/Input as InputComponent, chart.js's Legend/Tooltip, @signozhq/icons' BarChart/Histogram → project chart and periscope components.

Calls and other value references already consult the out-of-repo guard (isOutOfRepoBinding in matchByExactName and 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 import context.isOutOfRepoImport calls outside the repository (a package a package.json up the tree declares, or a Node built-in) — the same predicate the call/value paths use, including #2437's handling of catch-all and node_modules path aliases. JS-family languages only, like the other call sites.

Unchanged, by design:

  • a same-file declaration still wins — the only one that can coexist with the import is a local that shadows it (legend-state's const onValue = (snap) => … passed to fns.onValue(ref, onValue, onError) beside import { onValue } from 'firebase/database' keeps its edge);
  • a project import still resolves through resolveViaImport first;
  • a specifier no package.json declares 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:

  • outline's shape (styled(Link)), a named import (noop), a package-path default import (lodash/debounce) and a Node built-in (node:util's format), each beside a unique project function of that name → no edge;
  • cord-field's shape under a catch-all alias ("*": ["./typings/*"]) → no edge, while the test file's own noop still links to itself;
  • controls that keep resolving: a relative project import, an undeclared $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.ts only, which function values never reach); tsc --noEmit, this PR's test, function-ref.test.ts and #2670's react-router-outlet-routes.test.ts re-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 except dist/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.

repo removed added the removed edges
outline 21 0 react-router-dom Link ×18 → server/emails/templates/components/Footer.tsx:Link; prosemirror-commands' newlineInCode → outline's own shared/editor/commands/codeFence.ts:newlineInCode; date-fns' tr locale (tr_TR: tr) ×2 → shared/test/editor.ts:tr
signoz 24 0 antd Card ×9 → periscope/components/Card/Card.tsx:Card; antd Tooltip and chart.js Legend ×2 / Tooltip (Chart.register(…)) → uPlotV2's Legend/Tooltip; antd Input as InputComponent → components/Input/index.tsx:InputComponent; @signozhq/icons BarChart ×8 / Histogram ×2 → the visualization chart components
cord-field 20 0 lodash noop ×9 and ts-essentials noop → UserForm.test.tsx:noop, a test's own stub; MUI Typography ×7 → Typography.stories.tsx:Typography; MUI icon Home → the Home scene; @editorjs/header default ×2 → the app's Header
legend-state 4 0 react-native Switch (in the reactive component maps) → legend-state's own src/react/Switch.tsx:Switch
bulletproof-react 3 0 lucide-react's Users icon (icon: Users) in all three apps → the Next.js app's users.tsx:Users page component
excalidraw 1 0 import * as MermaidToExcalidraw from '@excalidraw/mermaid-to-excalidraw' handed to a mock → the project's MermaidToExcalidraw component
topcoder 1 0 react-router-dom Switch in expect(routeSwitch.type).toBe(Switch) → the project's Switch toggle
bitwarden 1 0 rxjs identity ({ enter: identity, exit: identity }) → libs/common/src/tools/rx.ts:identity
15 others 0 0 kcd-jest-cypress, next-saas, react-hn, vanlife, takenote, jira-clone, sensors-africa, windscribe, mantis, dho-web, happa, mastodon, react.dev, ghostfolio, home-assistant frontend: byte-identical

Triage: all 75 removed edges are wrong. Each is a function-ref edge (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-line react-native import, topcoder's toBe(Switch), signoz's Chart.register(…) from chart.js and styled(InputComponent), excalidraw's namespace import, bitwarden's rxjs identity, 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. There const onValue = (snap) => … (line 363) shadows import { 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' → a CreateStory component, bitwarden's eslint specs' { errorMessage } from './no-bit-dialog-wrapper.mjs' → a web-app step's errorMessage, outline's Link from ../marks/Link (a class) → the email template's Link. Many edges in that bucket are right, though (outline's Input from ./Input → Input.tsx:Input), so it needs its own rule and its own A/B. It follows as a separate PR.

Coordination

🤖 Generated with Claude Code

colbymchenry and others added 7 commits October 9, 2026 21:11
… 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
colbymchenry merged commit b8f81e2 into main Oct 19, 2026
@colbymchenry
colbymchenry deleted the claude/angry-wu-cf3be0 branch October 19, 2026 18:11
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>
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.

1 participant