Skip to content

JavaScript: calls through a CommonJS destructured require are never resolved (express: 63 call edges from 11,733 call sites) #3778

Description

@BloodyBeard

Summary

For CommonJS code, graphify resolves the import of a function but not calls through the imported binding. The result is that JavaScript projects get almost no calls edges, while the same analysis on other languages produces one to two orders of magnitude more.

Measurement

Same command on eight well-known repos (graphify <path> --code-only, then cluster-only --no-label). Call sites counted by walking each file's tree-sitter AST for the language's own call node type; call edges counted from graph.json:

Repo Language Call sites calls edges ratio
JamesNK/Newtonsoft.Json C# 48,070 13,487 28%
google/gson Java 23,468 5,337 23%
spf13/cobra Go 4,430 934 21%
BurntSushi/ripgrep Rust 17,103 3,270 19%
psf/requests Python 2,687 453 17%
gin-gonic/gin Go 9,414 1,346 14%
pallets/flask Python 3,963 386 10%
expressjs/express JavaScript 11,733 63 1%

express's whole graph contains 11 calls and 52 indirect_call edges across 404 nodes and 119 callable nodes.

Concrete case

expressjs/express at current HEAD:

// lib/utils.js:61
exports.normalizeType = function(type){ ... };

// lib/response.js:27
var normalizeType = require('./utils').normalizeType;

// lib/response.js:587
this.set('Content-Type', normalizeType(key).value);

Everything here is literal: a literal require path, a literal property, and a plain identifier call.

In the resulting graph.json, the node lib_utils_normalizetype has exactly two incoming edges:

response.js --imports--> normalizeType()
lib/utils.js --contains--> normalizeType()

There is no calls edge from lib/response.js to normalizeType(), even though the import was resolved correctly — so the resolver clearly knows where the symbol lives.

Expected

A calls edge from the calling function in lib/response.js to normalizeType() in lib/utils.js.

Why it matters

The import edge being present while the call edge is missing means the information needed to resolve the call is already available; only the call-site binding is not being made. Any consumer doing reachability or call-graph work on JavaScript gets a graph that is essentially call-free — a "no path found" result on a JS project currently means very little.

The var x = require('./m').x destructuring form is extremely common in CommonJS, so this is likely not an edge case.

Environment

  • graphify 0.9.65 (PyPI graphifyy)
  • Python 3.14, Windows 10
  • expressjs/express default branch, shallow clone 2026-09-22

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions