Repository navigation
fix(js,ts): a default import is what export default declares, not the first exported function - #2432
Open
colbymchenry wants to merge 4 commits into
Open
colbymchenry wants to merge 4 commits into
colbymchenry wants to merge 4 commits into
Conversation
…he first exported function
A default import resolved to the module's first exported function or
component whenever the `export default` statement wrote a declaration
(`export default function Vans()`, `export default class Store`): only a
separate `export default NAME` statement was read. A React Router 6.4+
data-router page exports its `loader` or `action` above the page
component, so `import Vans, { loader as vansLoader } from './Vans'` bound
the route `/vans` (and every call through `Vans`) to `loader`.
The resolver now reads the file's own `export default` statement, with
comments and strings masked: a declaration it writes is found by
position, a binding it names by name, and an anonymous function, class or
arrow function is no node at all (the guess took an exported function
beside it, or one nested inside it). The statement also beats an
exported styled/memo component above it. Only an expression default
(`memo(Card)`, `new Service()`), a binding the file doesn't declare, or
no statement keeps the old guess.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ode only
Masking strings too (blankStringContents) loses step on a template whose
`${…}` holds another template or a regex with a backtick, and blanks the
code up to the next backtick. On outline it blanked `export default
EditorContainer` at the end of a 2,900-line styled-components file and
`export default function headingToSlug` below such a regex, and the guess
that followed bound `Styles` to an unrelated exported `pulse`. Comments are
still stripped; a statement must name a node of the file, so template text
that only reads like one names nothing.
Adds the headingToSlug shape to the test, and the CHANGELOG entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 7, 2026
…ode is the module's A template can hold statement-shaped text: react.dev's SandpackWithHTMLOutput.tsx writes two sandbox files' `export default function` lines above its own `export default memo(function ...)`, and reading only the first line left the module's default import with nothing. Every line-initial statement outside a block comment is now read, the first that names a node wins, and a function or class a statement declares binds only the node that starts there, so a template's `export default function formatHTML` can't reach the file's real formatHTML. The block-comment check reads the file once for all of its statements. Carries #2432's default-export tests unchanged (all pass), and adapts its multi-statement and declaration-position rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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 JS/TS default import resolved to the wrong symbol whenever the module exported a function above its
export default function X()/export default class X.findExportedSymbolWalk's default branch tookdefaultComponent ?? defaultExportBindingNode(...) ?? defaultFnClass:defaultExportBindingNodeonly read a separateexport default NAMEstatement, anddefaultFnClassis the file's first exported function or class (ORDER BY start_line).A React Router 6.4+ data-router page exports its
loader/actionabove the page component, so in the reported repro (BlazejBatko/react-router-v6-learning-project)import Vans, { loader as vansLoader } from "./pages/Vans/Vans"boundVanstoloader. On main/vans,/vans/:id,/host,/host/vansand the threeHostVanDetaillayout edges all pointed atloader, and/loginataction.Two related guesses failed the same way:
defaultComponent(the first exportedcomponentnode) beat the statement, so a.tsxfile exporting astyled/memocomponent lost its default function or binding to that component.export default function () {…},export default class {…},export default () => …) took whatever exported function came first, including one nested inside the default itself, because the extractor'sisExportedwalks the whole parent chain.The fix
statedDefaultExport(insrc/resolution/import-resolver.ts) reads the file's ownexport defaultstatement and replaces the old binding-only reader:export default [async] function[*] NAME,export default [abstract] class NAME): the node namedNAMEthat starts between the statement's line and the name's line. That is found by position, so a nested same-name function can't stand in, andexport default\nfunction Xworks.export default NAME): unchanged semantics. The first function/class/component/constant/variable of that name.memo(Card),connect(...)(Card),new Service(),{ … }, a binding the file doesn't declare, no statement):'unstated', which keeps the olddefaultComponent ?? defaultFnClassguess. Svelte/Vue SFCs never reach the reader, so their component still wins (Re-export barrels unresolved after #130: default re-exports (export { default as X } from './x.svelte'), bare './' index imports, and package-subpath barrels - callers/callees/impact miss the edge (false 0 callers) #629).Comments are stripped before scanning, but strings are deliberately not masked.
blankStringContentsgets out of step on a template whose${…}holds another template or a regex containing a backtick. In validation it blanked the real statement in 6 files across outline/excalidraw/folo/dokploy: outline'sheadingToSlug.tsand its 2,900-line styled-componentsStyles.ts, where the fallback guess then produced a wrongStyles → pulseedge. A statement now has to name a node of the file to count, so template text that merely saysexport default function Xnames nothing. Of several statements, one that names a node wins.Kernel
There is no parallel path to keep in step.
codegraph-kernelonly extracts; import resolution is TS-only. The change reads the nodes both extractors emit, so I checked that they emit the same ones for every shape involved: 31 default-export shapes (named/async/generator/class/abstract/decorated/anonymous/arrow/multiline/overloads/in-comment/in-template/styled-first, …) are 31/31 byte-parity underscripts/kernel-parity.mjs. The new test passes with the kernel (CODEGRAPH_KERNEL_EXPECT=1) and on wasm (CODEGRAPH_KERNEL=0).Tests
__tests__/default-export-declaration.test.ts(new), red on main (4 of 5 failing) → green:/vans,/host/vans(anexport const loader = async () =>arrow),/login(export async function action) bind to the page components.export default async function handler(Next.js API route),export default class Store,export default function* streamwritten across a line break, andheadingToSlugbelow a regex-with-backtick template (fails if strings are masked).export default Bannerbinding beat an exportedstyledcomponent.helper/inner/useThing/handle.export default new Service()still reachesService.run(passes on main too).On the branch merged with main, the default-export and resolution suites pass with the kernel staged: namespace-object-resolution, esm-unexported-binding, store-exported-later, react-router, react-router-nested-lazy, expo-router, nextjs, extraction, resolution and kernel-tsjs-parity, 1,238 tests.
tsc --noEmitis clean.The full parallel suite ran on a saturated machine (100% CPU, 43 node processes from other sessions; 25 minutes). It reported 734 failures in 181 files. 722 were timeouts, 6 were
EBUSY/EPERMteardown errors, and 6 were timing assertions in daemon, watchdog, writer-lock and MCP-lifecycle tests (e.g.expected 100.36 to be less than 100ms). A serial rerun of those 181 files (--no-file-parallelism --testTimeout=120000, 94 minutes at the same load) left 7 failures, all timing or teardown:function-ref's #1820 case (its own 60 s limit),orphaned-refs-sweep(its own 15 s limit),mcp-subproject-adoption(a 5 s handshake timer),mcp-writer-lock(a liveness check readunverified),mpeg-ts-not-typescript(2359 < 2000ms),ui-server-api(156.8 < 100ms) anddaemon-pid-reuse(EBUSYremoving its temp dir). Run alone, all 7 pass; the two in-file limits were raised for that run only.Validation (before/after
scripts/dump-graph.mjsdiffs)Arms: main 2f2afea vs main + this fix, same kernel,
codegraph init -yper repo. After merging main up to ed199e6 (#2413 and #2417, C++/Go only), vanlife, epic-stack and hyperdx re-index byte-identical to the pre-merge after arm.examples/@6.29.0notes)examples/Every one of the 341 re-resolutions was checked against an independent oracle (a raw-source regex for the target file's
export defaultname, no resolver code): 316 land on the file's declared default, 2 on members of a default class (outlineToggleBlock.isEmpty/isHeadEmpty, previouslytoggleStorageKey), and 23 keep the same target with new metadata. Of those 23, 15 are upgrades: outline/hyperdx function refs that had fallen back to name matching at 0.8, and 4client.userInfo()calls in outline's OAuth test at 0.7, now resolve through the import at 0.9, because astyled/memocomponent or a wrong guess no longer intercepts the default. The other 8 are binner downgrades (below). None were unexplained.Highlights: vanlife's 5 routes + 3 layouts and rr-examples'
notesroute + 2 layouts reach the page (Vans,Login,VanDetail,Dashboard,HostVans,HostVanDetail,Root) instead ofloader/action. epic-stack's testimport { default as UsernameRoute, loader }(a default import since #2412) reachesProfileRoute. hyperdx'suseSidePanelStack×30,useRowWhere×21,CheckAlertTask×9 andProvisionDashboardsTask×7, and outline'sRelation×56,pagination×45,lazyWithRetry×36 anddocumentCreator×18, previously all went to the file's first exported helper. On main, outline'sPOST documents.createroute had no edge todocumentCreator: its call was guessed asauthorizeDocumentCreateand merged into the route's own direct call to that function.Every removed edge (25), each a wrong edge the guess made:
injectReducer(...)/injectSaga(...)(×2 copies, app + internals template) are anonymous HOC factories,export default ({ key, reducer }) => WrappedComponent => { class ReducerInjector … }. The calls were recorded asinstantiatesof the class declared inside the factory. They are now unresolved.rateLimiter(...)(export default (config?: Partial<Options>) => rateLimit(...)) went torateLimiterKeyGenerator. Now unresolved.createExpoConfig(...)(export default ({ config }: ConfigContext): ExpoConfig => …) went toresolveRuntimeVersion, which the same test imports separately by name. Now unresolved.[CodeFence, Image, ToggleBlock]plusDeliverWebhookTask. They went to an unrelated exported helper (expandCodeBlockAt,downloadImageNode,toggleStorageKey,isExpectedNetworkError). The import now resolves to the class, which a TS function ref deliberately doesn't target (bareClassOkis Python-only), so they are unresolved. The other 2: calls tomarkRule,export default function (options) {…}, went toemphasisPlugin.Added edges (35, all outline): 7
extendsthat failed on main (OAuthClient×5 including the test's subclass,CodeFence,MarkdownAPIImportTask; the guess returned a function, whichextendsrejects), the 11interface-imploverride calls thoseextendsedges enable, 16 function refs toButton/ListItemthat resolve through the import now, anddocuments.create → documentCreator.binner:
export default (props) => <Search {...props} params={useParams()} … />is an anonymous wrapper around the file's ownexport function Search. Main's first-exported guess happened to land onSearch. Now the route is bound by name to the same node (exact-match0.5 instead ofimport0.9). A survey of ~35 local React repos found this forwarding-wrapper shape only in binner's 6 page files, so I left the anonymous rule as specified rather than add a look-through here.Overlap with open PRs
findExportedSymbolWalkwith a broader reading, so the two conflict and should converge on one.memo(Card),connect(m)(Bar),observer(function X…)),new X()as the class,export { X as default }, and an imported binding re-exported as the default. Once a JS file has a statement it never guesses. That covers both follow-ups under "Not in this PR".export defaultstatement, keeps the first one that names a node, and finds declarations by position. fix(react): a JSX tag renders what the file imports under that name #2451 reads only the first statement, and on reactjs/react.dev that loses a default.src/components/MDX/SandpackWithHTMLOutput.tsxhas sandbox template text at L16 (export default function ShowRenderedHTML) above the realexport default memo(function SandpackWithHTMLOutput(…))at L82. On fix(react): a JSX tag renders what the file imports under that name #2451's build,MDXComponents.tsx's 2 function refs to it drop from import 0.9 to name-match 0.8.export defaultstatement is read, the first that names a node wins, and a declared function or class is found by position. It also includes this PR's test file unchanged.resolvedBy/confidence on fix(react): a JSX tag renders what the file imports under that name #2451: vanlife 8, rr-examples 3, epic-stack 1, react-boilerplate 26, binner 8, folo 1, hyperdx 76, outline 275. That is 398 of 398 sites, with no differences, and the three controls are byte-identical on both PRs.Not in this PR
export default memo(Card),connect(mapState)(Card),withRouter(Card),new Service()) keep the first-exported guess, which is right only when the wrapped binding is the first exported function. Looking through a wrapper to its argument, as expo-router'sdefaultExportNamedoes, would be the next step; fix(react): a JSX tag renders what the file imports under that name #2451 does it.import X from './X'; export default Xin a barrel: the binding names an import, not a local declaration, so it still falls to the guess. Following the import like a re-export would close it; fix(react): a JSX tag renders what the file imports under that name #2451 does that too.export default NAMEreader rather than adding another reader.🤖 Generated with Claude Code