Skip to content

JavaScript: every mod.x() through a CommonJS require links to the module's one anonymous exports.y = function #2952

Open

Description

@inth3shadows

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):

  1. 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.
  2. Separately, the default fallback (the first exported function when the module has no export default) shouldn't answer a member call through a require binding: mod.x() reads property x, 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 for module.exports = { … } modules.

Related: #2189 (calls through CommonJS require bindings), #2432 (ES default imports no longer take the first exported function), #1721, #2896.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions