fix(extract): a require binding does not shadow the required class by lab1207 · Pull Request #4634 · Graphify-Labs/graphify · GitHub
Skip to content

fix(extract): a require binding does not shadow the required class - #4634

Open
lab1207 wants to merge 1 commit into
Graphify-Labs:v8from
lab1207:fix/require-binding-shadow
Open

lab1207 wants to merge 1 commit into
Graphify-Labs:v8from
lab1207:fix/require-binding-shadow

Conversation

@lab1207

@lab1207 lab1207 commented Oct 21, 2026

Copy link
Copy Markdown

What

A CommonJS const X = require('./m') binding no longer creates a node in the importing file.

The binding is an import, not a module-scope data binding, but the extractor treated it as one. The node it created shadowed the class the required file exports, so inside the importing file new X() and class A extends X bound to the local node instead of the required one, and the required class got no incoming edge:

src/use.js      const Logger = require('./logger')
                new Logger('w')        ──calls──▶  src/use.js :: Logger   # wrong
src/logger.js   module.exports = Logger

The ESM default import (import Logger from './logger') resolves to the class in ./logger, and so does a var binding — that declaration kind creates no node. const and let were the odd ones out.

Fix

graphify/extractors/engine.py: the module-scope data-binding branch skips a declarator whose value is a require(...) call (_find_require_call, the same helper the require-import path uses). The require's own imports_from edge is unaffected — the import is still recorded, it just no longer mints a second node for the same name.

tests/test_js_require_binding_shadow.py: a new regression test writes src/logger.js (a Logger class exported through module.exports) and src/use.js (const Logger = require('./logger') plus new Logger('w')), then asserts that

  • no node exists for src/use.js :: Logger, and
  • the call lands on src/logger.js :: Logger.

Verification

I could not collect the graphify test suite here: the environment has no tree_sitter, so import graphify.extract fails at collection. This is a pre-existing environment gap, not related to the change; CI is the judge.

What I could check here is the change itself: the branch that creates module-scope data nodes now skips a require(...) value, and the helper it uses (_find_require_call) is the one the require-import path already relies on, so a require('./m') in any position — const X = require('./m'), const x = require('./m').y, const { a } = require('./m') — is recognised the same way.

AI authorship

Prepared with MiniMax M3.1 Flash Free via the opencode agent harness (minimax-m3.1-flash-free).

Closes #4621.

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.
@lab1207
lab1207 requested a review from safishamsi as a code owner October 21, 2026 13:12
@github-actions

Copy link
Copy Markdown

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; formal verification proved 0 of 1 changed function(s) (1 not verified).

Formal verification. PR-changed functions: 0/1 verified (0 proven, 0 may-equivalent, 0 distinguished) · 1 not verified (1 vacuous).

Not verified on this run: \_js\_extra\_walk (vacuous: never exercised).


Graphify review — findings

Stops creating a module-scope data node for CommonJS const X = require('./m') bindings in the JS extractor, since they are imports rather than data. Previously that local node shadowed the class the required file exports, so new X() resolved to the importing file's own node and the real class got no incoming calls edge (#4621). A regression test covers the case, checking that no local Logger node is emitted and that Watcher links to the exported Logger class.

No blocking issues surfaced. 3 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 954 functions depend on the 356 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: _js_extra_walk() — 2 callers, 12 callees

Verification — 954 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 843 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

40 of 437 test file(s) selected (9%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_build.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_ejs_extractor.py — impact
  • tests/test_ext_tagged_id_clash.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_php_closures.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_indirect_call_block_scoped_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_java_member_calls.py — impact
  • tests/test_js_exported_scalar_bindings.py — impact
  • tests/test_js_member_collisions.py — impact
  • tests/test_js_require_binding_shadow.py — impact, changed-test
  • tests/test_languages.py — impact
  • tests/test_multilang.py — impact
  • tests/test_php_unparsed_casts.py — impact
  • tests/test_python_inherited_method_calls.py — impact
  • tests/test_python_property_reads.py — impact
  • tests/test_python_pytest_fixture_receivers.py — impact
  • tests/test_python_underscore_resolution.py — impact
  • tests/test_rationale.py — impact
  • tests/test_ruby_resolution.py — impact
  • tests/test_scala_context_bounds.py — impact
  • tests/test_scala_given_instances.py — impact
  • tests/test_scala_self_type.py — impact
  • tests/test_scala_top_level_binding.py — impact
  • tests/test_scala_type_definition.py — impact
  • tests/test_svelte_extraction.py — impact
  • tests/test_swift_computed_properties.py — impact
  • tests/test_swift_protocol_requirements.py — impact
  • tests/test_trailing_newline_not_a_syntax_error.py — impact
  • tests/test_ts_new_expression_calls.py — impact
  • tests/test_typescript_module_extensions.py — impact
  • tests/test_vue_extraction.py — impact

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_js\_extra\_walk.

The verifier did not have enough to check \_js\_extra\_walk, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 264 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

· 1 more finding(s) on lines outside this diff (see the check run).

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JS/TS: const/let X = require('./m') creates a local node X that shadows the required class (new X() and extends X bind to the import binding)

1 participant