Repository navigation
fix(js,ts): a default import is what export default declares, not the first exported function - #2432
Merged
Merged
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>
…n-156996 # Conflicts: # CHANGELOG.md # src/resolution/import-resolver.ts
…n-156996 # Conflicts: # CHANGELOG.md
bompus
added a commit
to bompus/codegraph
that referenced
this pull request
Oct 10, 2026
…eceivers (#430) * fix(context): call paths label synthesized hops like explore, not as events (colbymchenry#2428) Two surfaces label a synthesized (dynamic-dispatch) `calls` hop: codegraph_explore's Flow and dynamic-dispatch links plus the codegraph_node trail (`dynamic: interface → impl @file:line`), and ContextBuilder's "## Call paths", which `codegraph context` prints (`→[…]`). Each kept its own list. The call paths' list stopped at callback, react-render, jsx-render, vue-handler, http-client, queue-job and socket messages, so every other synthesizer fell through to `event ${m.event ?? ''}`: an interface-impl hop read `→[event @src/pdf-exporter.ts:5]`, and so did a C++ override, a redux thunk, a gin middleware chain or a closure collection. The wording now lives once, in src/graph/synthesized-hop.ts (`describeSynthesizedHop`: a summary, the detail only the call paths print, and the wiring site), as AGENTS.md asks of a derivation two surfaces render. ToolHandler renders `dynamic: <summary> @site`, the call paths `<summary><detail> @site`. Explore and the node trail print exactly what they printed before; the call paths keep their wording for the kinds they already labelled, including the queue name, a socket's direction and the HTTP clause, and now name the rest the way explore does. A bus event reads `bus event` there too, and the section's footnote no longer says hops are marked `[callback/event …]`. The helper also holds the wording colbymchenry#2414 gives a Go method a struct gets by embedding (`metadata.promotedInto`), so that arm moves here too. The unused long `label` form of ToolHandler.synthEdgeNote (last read by the removed codegraph_trace) is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): a route rendering a same-file lazy value follows its module, and a new component gets its JSX edges (colbymchenry#2452) A route that renders `const Docs = lazy(() => import('./pages/Docs'))` binds to the component that module exports, and to the declaration while the module is missing or exports none (colbymchenry#2400's declaredComponent). The reference had resolved, so no failed-ref retry saw it, and CG-33's rebind reached it only when the module's component shared the declaration's name: a sync that added the module, gave it its default export or moved that export left the route on its old answer. A data router's `lazy` route kept a moved default export the same way. FrameworkResolver.lazyModules names the modules a route's answer reads, including the files a barrel forwards it through (colbymchenry#2436); React implements it for lazy-import references and the same-file lazy values Pattern 1 reads. After the CG-33 rebind, sync puts back the route references whose modules it added or changed (reopenRouteModuleReaders), in the order their edges were written, for the orphan sweep. The jsx-render refresh read only each changed file's own content, so a new component without JSX of its own never got the edge from `<Team />` in an unchanged file. A sync whose definition delta holds a JSX-renderable node now refreshes synthesis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cpp): a range-based for loop's variable has the type the loop declares (colbymchenry#2439) C++ receiver inference read a declaration only when the declared name was followed by `;`, `=`, `,`, `)`, `[`, `{` or `(`, so a range-based for's `Type name :` was never read. In protocolbuffers/protobuf's conformance runner, `suite->SetVerbose(…)`, `suite->RunSuite(…)` and `suite->GetFailureListFlagName()` inside `for (ConformanceTestSuite *suite : suites)` fell through to Strategy 3's guess by the receiver's name, and in RocksDB `log->file()` on a `log::Writer` loop variable went to `BlobLogWriter::file`. A line the declarator regex doesn't match is now read as a range-based for when its `for (` header declares the receiver before a single `:` (not `::`), and the loop's variable is taken only when the call is inside the loop's body: past the header, up to the `}` closing a braced body or the end of a single-statement body, with comments and literals skipped. After the loop, or in a later function, the same name is another variable, and the scan goes on as before. A loop over `auto` elements has nothing to read and keeps the existing fallback. Bit-fields and the scope operator are never read as a loop's declaration. A loop's declaration is recorded like any other (colbymchenry#2413's noteCppDeclaration), so the std-type gate applies to `for (const std::string& s : …)` too, and a loop that doesn't enclose the call still marks the receiver as shadowed. With colbymchenry#2421, the 13 conformance runner calls reach ConformanceTestSuite's methods at 0.9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(js,ts): a default import is what `export default` declares, not the first exported function (colbymchenry#2432) 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 from comment-stripped code. 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. Every line-initial statement is read and the first one that names a node of the file wins, so template text that only reads like a statement names nothing. Strings are not masked: a masker loses step on a template whose `${…}` holds another template or a regex with a backtick, and on outline it blanked the real statement below one. Only an expression default (`memo(Card)`, `new Service()`), a binding the file doesn't declare, or no statement keeps the old guess. `require('./x').default` (colbymchenry#2433) reads the same statement first, so it reaches the declared default too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cpp): a `->` call on a smart pointer or optional reaches the type it holds (colbymchenry#2440) A C++ call made with `->` on a receiver the calling function or its class declares as a `std::unique_ptr<T>`, `std::shared_ptr<T>` or `std::optional<T>` now resolves on `T`: on the class C++ name lookup finds for it where the call is written, or on a class that class derives from. Before, receiver inference read the type as `unique_ptr`, found no method, and the call fell through to the guess by method name: rocksdb's `std::unique_ptr<Iterator> iter; iter->Valid()` reached `ArenaWrappedDBIter::Valid`. The held type goes through colbymchenry#2407's lookup (`cppClassWritten`, extracted from `matchCppSupertype`), not `resolveMethodOnType` on its last name: rocksdb has 22 classes named `Iterator`, and the one the calls mean inherits its methods from `IteratorBase`, so a last-name lookup reaches `MemTableRep::Iterator`. The method is taken from that class or its bases through the class's own extends edges (`getSupertypeNodes`). A template parameter, a library type or an alias of one gets no edge; a type the project doesn't declare, or an alias the lookup can't follow, leaves the call as it was. The namespace-macro helpers move from name-matcher.ts to cpp-namespaces.ts unchanged, so name-matcher can import cpp-supertypes without a cycle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): a barrel edited to forward another page moves the routes that lazily load it --------- Co-authored-by: Colby Mchenry <me@colbymchenry.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 10, 2026
colbymchenry
added a commit
that referenced
this pull request
Oct 10, 2026
…ed default (#2481) #2432 made a default import read the module's own `export default` statement, and its merge pointed #2433's `require('./x').default` fallback (esmDefaultExport) at the same reading. Nothing in the suite pinned that path with a guessable helper above the default: #2433's fixture keeps its helper unexported. Two cases: - React Native's AnimatedColor shape, an exported helper above `export default class AnimatedColor`, loaded with `require("./AnimatedColor").default` and `const { default: Color } = require("./AnimatedColor")`. With the first-exported guess both `new` calls instantiate the helper. - `export default abstract class Repository` below an exported `connect()`, extended by `class Users extends Repository`. With the guess the `extends` edge is missing. Both fail with e47cb25's resolver (before #2432). On main, putting the guess back on the require path fails only the first, and dropping `abstract` from the declaration pattern fails only the second. The CHANGELOG's default-import bullet now says `require('./x').default` is read the same way. 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 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.Merged with #2433, re-validated on main cd97bb3
#2433 (
require('./x').default) landed first and built its fallback on the old guess chain (esmDefaultExport→defaultExportBindingNode→ first exported function). The merge keeps #2433's gates (hasEsmDefaultExport, depth 0, JS family) and hasesmDefaultExportread theexport defaultstatement first, so arequire().defaultreaches the declared default too. The binding-only reader is gone.require-default,import-default-asand this PR's tests pass on both the kernel and wasm paths. The JS/TS suites that landed since (catch-all alias, import-binding sync, lazy barrels, JSX disambiguation, HOC, kernel parity and others) pass 569/569 with the kernel rebuilt.The A/B was re-run on main cd97bb3 against main + this PR, adding #2433's react-native and topcoder (that session's survey found this PR's shape in 7 react-native and 4 topcoder files):
createAnimatedComponent). 4 removed: function refs toexport default class AnimatedValue/AnimatedColorused as values, which the guess had sent toflushValue/getRgbaValueAndNativeColor.LivePreview(export default class ThriveArticleLivePreviewat L152) sent toisThriveLivePreview, andcreateCustomPlugin(...), an anonymousexport default ({ editor }) => {…}, sent toupdateEntityData, a function nested inside it.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