Repository navigation
Description
In a CommonJS module that assigns functions to exports.x, an anonymous function expression is indexed as exported but a named one isn't. A caller that does const mod = require('./mod'); mod.anything() then links every member call to the one anonymous export, through the "first exported function" default fallback. Calls to the named exports get no edge.
// lib/utils.js
function createETagGenerator(options) { return function generateETag(body) { return body; }; }
exports.etag = createETagGenerator({ weak: false });
exports.normalizeType = function (type) { return type; }; // line 4, anonymous
exports.compileTrust = function compileTrust(val) { return val; }; // line 5, named
// test/utils.js
var utils = require('../lib/utils');
function t() { utils.etag('x'); utils.compileTrust(1); utils.missing(); }
module.exports = t;| call | edge on main |
expected |
|---|---|---|
utils.etag('x') |
normalizeType (resolvedBy: import) ✗ |
none (a value, not a declared function) |
utils.compileTrust(1) |
normalizeType ✗ |
compileTrust |
utils.missing() |
normalizeType ✗ |
none |
The indexed nodes show why:
function normalizeType lib/utils.js:4 isExported=1
function compileTrust lib/utils.js:5 isExported=0
With only the named form (exports.a = function a() {} for every export) there are no wrong edges, but utils.a() doesn't link to a either. const vs var makes no difference.
Reproduced on main at 2577601 with CODEGRAPH_KERNEL=0. Linux/WSL2, Node 22.23.2. Not fixed by #2754 or #2800 (I ran this fixture on both heads).
Where it shows up. express (1c6f1b0) writes lib/utils.js this way: exports.normalizeType = function(type){…} and exports.compileTrust = function(val){…} are anonymous, and exports.etag = createETagGenerator(…) is a value. In test/utils.js (var utils = require('../lib/utils')), utils.etag(…) links to normalizeType.
Two parts (inferred, not traced):
- Export status differs between an anonymous and a named function expression on the right of
exports.x =. The named one should count as exported too. Export status is answered in three places, two of them hand-mirrored regexes #1721 / fix: unify export-status detection and fix ambient export handling (#1721) #2896 consolidate this check. - Separately, the default fallback (the first exported function when the module has no
export default) shouldn't answer a member call through arequirebinding:mod.x()reads propertyx, not the default. Fixing 1 alone makes this worse, because the fallback then has more functions to pick from. fix: unify export-status detection and fix ambient export handling (#1721) #2896 shows the same thing formodule.exports = { … }modules.
Related: #2189 (calls through CommonJS require bindings), #2432 (ES default imports no longer take the first exported function), #1721, #2896.
Edited 2026-11-04: the call edges no longer reproduce on main at f72c9d2 (with CODEGRAPH_KERNEL=0 and with the native kernel, Node 22.22.2): utils.etag() and utils.missing() get no edge and utils.compileTrust() links to compileTrust. Most likely fixed by #3162. One leftover: a named function expression (exports.x = function x() {}) still isn't marked exported, unlike the anonymous form, but I found no user-visible effect, so I'm not refiling it. Closing.
Activity
inth3shadows commentedon Nov 4, 2026
The call edges no longer reproduce on
mainat f72c9d2 (withCODEGRAPH_KERNEL=0and with the native kernel). Most likely fixed by #3162. A named function expression onexports.xstill isn't marked exported, but I found no user-visible effect, so I'm not refiling it. Closing.