Repository navigation
fix(js,ts): require('./x').default reaches an ES module's default export - #2433
Merged
Merged
Conversation
…xport
`const X = require('./x').default` and `const { default: X } = require('./x')`
map to a named import of `default`. That is right for a CommonJS module that
sets the property itself (`exports.default = fn`, or the dual
`module.exports = X; module.exports.default = X`), but a module written as an
ES module and compiled to CommonJS sets it to its default export, which is no
named export. References through the binding stayed unresolved (bitwarden's
`new OsBiometricsServiceMac(...)`) or fell to name matching: the local
variable holding the module, or a namesake method in another package.
findExportedSymbolWalk now falls back to the module's ESM default export when
the named lookup finds no `default`. Only for JS-family refs, only in the
module the require names (`export * from` forwards no default), and only when
that module has an ESM default export at all: a single-file component, or an
`export default` statement. A CommonJS module's `exports.x = function`
declarations are flagged exported, so without that gate the
first-exported-function guess would invent a default for it.
bitwarden +2 edges (the two failed `instantiates` refs). react-native +872
-617: all 617 re-resolved at the same site (541 from the local require
binding to the real function or class, 47 from a namesake method in another
package to the right module, 28 metadata only, 1 from a type signature to the
implementation), 870 of 872 added edges on the declared default or what it
holds. express, eslint, mocha, koa, body-parser, fastify, topcoder,
proshop_mern, next-saas-starter and create-t3-turbo byte-identical. Dead-code
claims unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Merged
Merged
colbymchenry
added a commit
that referenced
this pull request
Oct 10, 2026
…he first exported function (#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` (#2433) reads the same statement first, so it reaches the declared default too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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.
Summary
extractJSImportsmapsconst X = require('./x').default(andconst { default: X } = require('./x')) as{ exportedName: 'default', isDefault: false }, sofindExportedSymbollooks only for a named export calleddefault. That is right for a CommonJS module that sets the property itself. A module written as an ES module (export default class Foo) has no nameddefault, though, so every reference through the binding stayed unresolved or fell to name matching.bitwarden's desktop app is the reported case.
main-biometrics.service.ts:51-52andos-biometrics-mac.service.spec.ts:105loadexport default class OsBiometricsServiceMacwithrequire("./os-biometrics-mac.service").defaultandnewit, and bothinstantiatesrefs stayedfailed.#2412 fixed the ESM sibling (
import { default as X }) by mapping it as the default import. The require form can't be mapped that way, because.defaultthere is a property ofmodule.exports:exports.default = fn,module.exports.default = X);module.exports = X; module.exports.default = Xsets it to X.Change
The named property is still looked up first. When
findExportedSymbolWalkfinds no nameddefault, it falls back to the module's ESM default export, under three gates:ESM_IMPORT_LANGUAGES). Python'sfrom .mod import defaultis untouched.depth === 0).export * fromnever forwards a default, so the walk through a wildcard re-export doesn't take one.export defaultstatement. A CommonJS module's anonymousexports.x = function () {}is flagged exported, so without this gate the first-exported-function guess would invent a default for a module that has none. That is the expressnormalizeTypebug class from fix(js): resolve calls through CommonJS require bindings #2189.The ESM default lookup itself (component, then the
export default NAMEbinding, then the first exported function or class) is factored intoesmDefaultExportand shared with theisDefaultbranch, sorequire('./x').defaultandimport X from './x'land on the same declaration.layoutComponent(frameworks/react.ts) triescontext.resolveImportfirst. Arequire(…).defaultlayout now resolves there, to the declaration its module read (lazyRouteComponent) found before. ItsisDefault || exportedName === 'default'branch stays as the fallback for when import resolution finds nothing. After #2412, that branch's second half is reached only by the require forms.Other readers of
isDefaultare unchanged. jsx-render's zero-candidate fallback, expo-modules, tier-synthesizer and react-router-synthesizer read the mapping flag, notfindExportedSymbol. topcoder's tworequire('../shared').defaultsites feed only JSX tags (<Application />), which go through jsx-render by name, so that repo is byte-identical.Validation
Fresh
codegraph init -yindexes,scripts/dump-graph.mjsbefore/after, native kernel staged in both arms. The arms are main 19f91e2 and this branch's fix commit, and onlydist/resolution/import-resolver.jsdiffers between them. The main commits merged in since (#2413, #2416, #2417, #2419, #2425, #2426) don't touchimport-resolver.ts, and their resolver hunks are gated to Go, C++ and sync-time navigation.failedrefs nowinstantiatesOsBiometricsServiceMacviaimport{ default: fastify } = require('../../fastify'))defaultalready foundfastifyReact Native's
Librarieshave 238require(…).defaultloads:const X = require(…).defaultbinding to the real function or class;JSTimers.setInterval→ an rn-tester test's method,Animated.delay→IntersectionObserver.delay,Keyboard.dismiss→ a dev-server handler) onto the right module. That is the method itself forKeyboard.dismiss. ForJSTimersandAnimatedit is the exported value, whose members are chosen at run time;importmetadata;log(…)signature in a Flow type to thelogimplementation.export defaultstatement:ExceptionsManager.handleException) or on methods of the class anewinstance has (Keyboard.dismiss);BatchedBridge.registerCallableModule/registerLazyCallableModule) land on the right methods but in the deprecatedtypes_DEPRECATED/modules/BatchedBridge.d.tsdeclare class MessageQueuerather thanMessageQueue.js. That is the existing instance-member type lookup, whichimport BatchedBridge fromgets too.unresolved_refs.Dead-code reports (bitwarden, react-native): no claims lost or gained.
Known limitations, both shared with
import X fromand filed as follow-upsexport default class Foo/export default function foo. So a module that exports a helper above its default class resolves to the helper, for an ESM default import today and now forrequire(…).defaulttoo. Repro on main:export function helper()aboveexport default class Foo, andimport Foo from './x'; new Foo()instantiateshelper. A scan found the shape in 7 react-native, 4 topcoder and 1 bitwarden file. None of them is arequire(…).defaulttarget in this corpus, so no edge above depends on it. The fix (reading the declaration forms indefaultExportBinding) changes ESM default imports everywhere and needs its own validation.export default,codegraph syncleaves the waiting reffailedwhile a fresh index links it. The failed-ref retry keys on the names the changed files declare, and a default binding's local name (X) is not the declaration's (Foo).import Y from './x'behaves the same way on main.Test plan
__tests__/require-default.test.ts(new, 8 tests):.defaultand{ default: X }(bitwarden's shape), and a Svelte component, are red on main and green with the fix;exports.default = fnbeside an anonymousexports.pad = function(the first-export guess would pickpad), the dual export, a CommonJS module with nodefault,export * from, and Pythonfrom .mod import defaultare green on both.export *test; dropping theexport defaultgate fails the no-defaultCommonJS test and the require-binding summary. Dropping the language gate changes nothing, because the target-file check already excludes Python; the Python test pins the behavior.tsc --noEmit,npm run build, kernel rebuilt after merging mainEBUSYtemp-dir cleanups and 2 daemon assertions (mcp-daemon,mcp-writer-lock).function-refCaller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 "a module global never binds through a shadow". It hits its own 60 s in-file limit even alone, and passes with the limit raised (219 s on this box).defaultlookup, and none of their fixtures has arequire(…).default.🤖 Generated with Claude Code