From 66bb69114a6f951a11227cefb11714ef9f4123a3 Mon Sep 17 00:00:00 2001 From: lab 1207 Date: Wed, 21 Oct 2026 18:41:48 +0530 Subject: [PATCH] fix(extract): a require binding does not shadow the required class A \const X = require('./m')\ binding is an import, not a module-scope data binding. The node it produced in the importing file shadowed the class the required file exports, so \ ew X()\ bound to the local node and the required class got no incoming edge. --- graphify/extractors/engine.py | 21 +++++++---- tests/test_js_require_binding_shadow.py | 48 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 tests/test_js_require_binding_shadow.py diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 79c861b50..736778ea6 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -4408,12 +4408,21 @@ def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, function_bodies=function_bodies, ) arrow_found = True - elif value and ( - is_exported_scalar_binding - or value.type in ( - "object", "array", "as_expression", - "satisfies_expression", "call_expression", - "new_expression", + elif ( + value + # A `const X = require('./m')` binding is an import, not + # a module-scope data binding. A node for it in this file + # shadowed the class the required file exports, so + # `new X()` bound to the local node instead of the + # required one (#4621). `var` never produced one either. + and _find_require_call(value) is None + and ( + is_exported_scalar_binding + or value.type in ( + "object", "array", "as_expression", + "satisfies_expression", "call_expression", + "new_expression", + ) ) ): # Simple exported identifiers are part of the module API diff --git a/tests/test_js_require_binding_shadow.py b/tests/test_js_require_binding_shadow.py new file mode 100644 index 000000000..516fbf091 --- /dev/null +++ b/tests/test_js_require_binding_shadow.py @@ -0,0 +1,48 @@ +"""A CommonJS `const X = require('./m')` binding must not create a local node. + +The binding is an import, not a module-scope data binding. A node for it in +the importing file shadowed the class the required file exports, so `new X()` +bound to the local node and the required class got no incoming edge (#4621). +""" +from __future__ import annotations + +from pathlib import Path + +from graphify.extract import _make_id, extract + + +def _write(path: Path, text: str) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + +def test_require_binding_does_not_shadow_the_required_class(tmp_path: Path): + logger = _write( + tmp_path / "src/logger.js", + "function Logger(p) { this.p = p }\n" + "Logger.prototype.warn = function warn() {}\n" + "module.exports = Logger\n", + ) + use = _write( + tmp_path / "src/use.js", + "const Logger = require('./logger')\n" + "function Watcher() { return new Logger('w') }\n" + "module.exports = Watcher\n", + ) + + result = extract([logger, use], cache_root=tmp_path) + + node_ids = {node["id"] for node in result["nodes"]} + assert _make_id("src/use", "Logger") not in node_ids, ( + "the require binding must not create a node in the importing file" + ) + + calls = { + (edge["source"], edge["target"]) + for edge in result["edges"] + if edge["relation"] == "calls" + } + assert (_make_id("src/use", "Watcher"), _make_id("src/logger", "Logger")) in calls, ( + "the call must land on the class the required file exports" + )