Skip to content

fix(native): mark CJS require bindings to fix receiver-edge parity#1679

Merged
carlos-alm merged 2 commits into
mainfrom
fix/issue-1671
Jun 21, 2026
Merged

fix(native): mark CJS require bindings to fix receiver-edge parity#1679
carlos-alm merged 2 commits into
mainfrom
fix/issue-1671

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • const { X } = require('./path') creates a kind='function' shadow node in the importing file, which caused the native engine's emit_receiver_edge to treat it as a locally-defined non-class symbol and block the cross-file class fallback lookup
  • CJS require bindings were not added to symbols.imports in the Rust extractor, so imported_names.contains_key(name) returned false for names like Calculator, making is_local_definition = true
  • This caused the native engine to miss emitting the correct receiver edge (from main → Calculator) for calc.compute() when calc was typed via const calc = new Calculator()

Root cause: The WASM engine fixed this via cjsRequireBindings (issue #1661), but the native engine had no equivalent.

Fix:

  • types.rs: add cjs_require: Option<bool> to Import struct
  • javascript.rs: set cjs_require = Some(true) on const { X } = require() imports so the receiver-edge resolver treats them as import artifacts
  • pipeline.rs: use empty target_file for CJS entries in collect_imported_names_for_file (import-aware call-target lookup misses → falls through to shadow nodes, matching WASM behavior) and skip CJS in resolve_pipeline_imports
  • import_edges.rs: skip CJS require imports in build_import_edges (no spurious DB import edges)

Test plan

  • npx vitest run tests/integration/build-parity.test.ts — all 8 tests pass (was failing before fix)
  • npx vitest run tests/ — 3283 tests pass, no regressions
  • cargo test — 422 Rust tests pass

Fixes #1671

…1671)

const { X } = require('./path') creates a kind='function' shadow node in
the importing file. The Rust emit_receiver_edge checks is_local_definition
via imported_names.contains_key(name) — but CJS require bindings were not
added to symbols.imports, so the name appeared to be a local non-class
symbol and blocked the cross-file class lookup.

Fix:
- types.rs: add cjs_require: Option<bool> to Import struct
- javascript.rs: set cjs_require=Some(true) on const { X } = require() imports
- pipeline.rs: use empty target_file for CJS entries in collect_imported_names_for_file
  (so import-aware call-target lookup misses and falls through to shadow nodes,
  matching WASM behaviour) and skip CJS in resolve_pipeline_imports
- import_edges.rs: skip CJS require imports in build_import_edges (no DB import edges)

Also fix pre-existing biome lint/format issues in dart.ts, wasm-worker-entry.ts,
dart.test.ts, and objc.test.ts.

Fixes #1671

Impact: 2 functions changed, 4 affected
@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a receiver-edge parity bug between the WASM and native engines: const { X } = require('./path') destructuring bindings were not added to symbols.imports in the Rust extractor, causing them to appear as locally-defined symbols and blocking the cross-file class fallback lookup in emit_receiver_edge. The fix marks these bindings with cjs_require: Option<bool> so the native pipeline skips them in DB import-edge generation while still including them in imported_names (with an empty target_file) to satisfy the contains_key check that gates is_local_definition.

  • CJS binding fix (pipeline.rs, javascript.rs, types.rs, import_edges.rs): CJS require names are now added to imported_names with file: \"\" so is_local_definition is forced to false, allowing the cross-file class lookup to proceed — matching the WASM cjsRequireBindings behavior from fix: build-parity test fails — native classifies main/square as dead-unresolved but WASM says leaf #1661.
  • Dispatch-table (RES-2) rollback (javascript.ts, types.ts, test/fixture files): The earlier inline object-literal dispatch-table implementation is removed; ADR-002 records it as a planned future deliverable to be re-implemented properly.
  • buildDataflowP4ForNative transaction safety (dataflow.ts): The DELETE-then-stitch sequence is now wrapped in a single db.transaction(), eliminating the prior window where a crash between the two operations could leave arg_in edges permanently deleted but never replaced.

Confidence Score: 5/5

Safe to merge; the CJS binding fix is mechanically correct and the dataflow transaction improvement closes a real crash-window gap.

The is_local_definition guard in emit_receiver_edge now correctly returns false for CJS require names because they appear in imported_names with an empty file — exactly matching the WASM cjsRequireBindings behavior. The empty target_file intentionally causes the import-aware call-target lookup to miss and fall through to the shadow node, which is the documented design. The transaction consolidation in buildDataflowP4ForNative is correct: runInterproceduralStitch does not open its own transaction, so nesting is not an issue. The dispatch-table removal is internally consistent (type, code, fixture, tests all cleaned up together).

No files require special attention; the pipeline.rs and build_edges.rs interaction is the heart of the fix and reads correctly.

Important Files Changed

Filename Overview
crates/codegraph-core/src/domain/graph/builder/pipeline.rs Adds a 3-line guard to skip CJS require entries in resolve_pipeline_imports; the companion changes in collect_imported_names_for_file (empty target_file for CJS names) were committed in the squashed base commit cdc5a79 and are already in main.
src/extractors/javascript.ts Removes the RES-2 dispatch-table extraction path: extractCallInfoWithBindings, the inline object-literal detection block in extractSubscriptCallInfo, and the arrayElemBindings parameter from dispatchQueryMatch. The ArrayElemBinding type and Phase 8.3e array-spread infrastructure are unchanged and still in use elsewhere.
src/features/dataflow.ts Folds the separate buildInterproceduralStitch wrapper into buildDataflowP4ForNative, wrapping the DELETE and stitch in a single transaction to close a crash-window inconsistency; also updates the runInterproceduralStitch JSDoc to reflect all callers.
src/types.ts Removes the 'dispatch-table' variant from DynamicKind; no remaining references to this variant exist in the TS source after the other removals.
tests/unit/points-to.test.ts Removes the three buildPointsToMap — dispatch-table pts constraints (RES-2) test cases; no remaining coverage for this feature is expected until re-implementation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["const { X } = require('./path')"] --> B{Rust extractor}
    B --> C["extract_destructured_bindings\n→ shadow node in definitions"]
    B --> D["NEW: cjs_require = Some(true)\n→ Import added to symbols.imports"]

    D --> E{collect_imported_names_for_file}
    E --> F["CJS path: ImportedName\n{ name: 'X', file: '' }"]

    F --> G{emit_receiver_edge}
    G --> H["imported_names.contains_key('X') → true\nis_local_definition = false"]
    H --> I["Cross-file class lookup via\nnodes_by_name proceeds ✓"]

    D --> J{resolve_pipeline_imports}
    J --> K["cjs_require=true → skip\nNo DB import edge created"]

    D --> L{build_import_edges}
    L --> M["cjs_require=true → skip\nNo spurious import edge in DB"]

    style I fill:#22c55e,color:#fff
    style K fill:#3b82f6,color:#fff
    style M fill:#3b82f6,color:#fff
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["const { X } = require('./path')"] --> B{Rust extractor}
    B --> C["extract_destructured_bindings\n→ shadow node in definitions"]
    B --> D["NEW: cjs_require = Some(true)\n→ Import added to symbols.imports"]

    D --> E{collect_imported_names_for_file}
    E --> F["CJS path: ImportedName\n{ name: 'X', file: '' }"]

    F --> G{emit_receiver_edge}
    G --> H["imported_names.contains_key('X') → true\nis_local_definition = false"]
    H --> I["Cross-file class lookup via\nnodes_by_name proceeds ✓"]

    D --> J{resolve_pipeline_imports}
    J --> K["cjs_require=true → skip\nNo DB import edge created"]

    D --> L{build_import_edges}
    L --> M["cjs_require=true → skip\nNo spurious import edge in DB"]

    style I fill:#22c55e,color:#fff
    style K fill:#3b82f6,color:#fff
    style M fill:#3b82f6,color:#fff
Loading

Reviews (2): Last reviewed commit: "fix: resolve merge conflicts with main" | Re-trigger Greptile

Comment on lines +1356 to +1363
let names = collect_object_pattern_names(&name_n, source);
if !names.is_empty() {
let mut imp = Import::new(
mod_path, names, start_line(node),
);
imp.cjs_require = Some(true);
symbols.imports.push(imp);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Aliased CJS destructuring not covered by fix

For const { Calculator: CalcAlias } = require('./calc'), collect_object_pattern_names returns "Calculator" (the key/export name, via child_by_field_name("key")), but extract_destructured_bindings creates a shadow node named "CalcAlias" (the value/local alias, via child_by_field_name("value")). At receiver-edge resolution time, effective_receiver is "CalcAlias" (from the type map), so imported_names.contains_key("CalcAlias") returns false and is_local_definition is still set to true — the same bug as before the fix.

The non-aliased case (const { Calculator } = require('./calc')) is handled correctly since both functions extract the same shorthand name. Aliased CJS destructuring is uncommon, but the gap is worth noting.

Fix in Claude Code

docs check acknowledged

Impact: 1 functions changed, 2 affected
@github-actions

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

6 functions changed14 callers affected across 5 files

  • runPostNativePasses in src/domain/graph/builder/stages/native-orchestrator.ts:1932 (4 transitive callers)
  • dispatchQueryMatch in src/extractors/javascript.ts:290 (2 transitive callers)
  • extractSymbolsQuery in src/extractors/javascript.ts:353 (1 transitive callers)
  • handleCallExpr in src/extractors/javascript.ts:1306 (3 transitive callers)
  • extractSubscriptCallInfo in src/extractors/javascript.ts:3155 (8 transitive callers)
  • buildDataflowP4ForNative in src/features/dataflow.ts:865 (0 transitive callers)

@carlos-alm carlos-alm merged commit f62651f into main Jun 21, 2026
29 checks passed
@carlos-alm carlos-alm deleted the fix/issue-1671 branch June 21, 2026 21:08
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: build-parity edges diverge — native emits extra receiver edge from main to Calculator vs WASM

1 participant