From cdc5a797f389879fabb1a5275249421754365c42 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sun, 21 Jun 2026 14:17:52 -0600 Subject: [PATCH] fix(native): mark CJS require bindings to fix receiver-edge parity (#1671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- .../src/domain/graph/builder/pipeline.rs | 13 ++++++++++ .../graph/builder/stages/import_edges.rs | 3 +++ .../src/extractors/javascript.rs | 25 +++++++++++++++++++ crates/codegraph-core/src/types.rs | 6 +++++ src/domain/wasm-worker-entry.ts | 4 ++- src/extractors/dart.ts | 4 +-- tests/parsers/dart.test.ts | 6 ++++- tests/parsers/objc.test.ts | 18 ++++++++++--- 8 files changed, 72 insertions(+), 7 deletions(-) diff --git a/crates/codegraph-core/src/domain/graph/builder/pipeline.rs b/crates/codegraph-core/src/domain/graph/builder/pipeline.rs index 196588d81..434770f6d 100644 --- a/crates/codegraph-core/src/domain/graph/builder/pipeline.rs +++ b/crates/codegraph-core/src/domain/graph/builder/pipeline.rs @@ -242,6 +242,9 @@ fn resolve_pipeline_imports( let abs_file = Path::new(root_dir).join(rel_path); let abs_str = abs_file.to_str().unwrap_or("").replace('\\', "/"); for imp in &symbols.imports { + // Skip CJS require bindings — they feed imported_names for receiver-edge + // resolution but must not produce DB import edges (#1678). + if imp.cjs_require.unwrap_or(false) { continue; } batch_inputs.push(ImportResolutionInput { from_file: abs_str.clone(), import_source: imp.source.clone(), @@ -1285,6 +1288,16 @@ fn collect_imported_names_for_file( let resolved_path = import_ctx.get_resolved(abs_str, &imp.source); for name in &imp.names { let clean_name = name.strip_prefix("* as ").unwrap_or(name).to_string(); + // CJS require bindings are included in imported_names so the receiver-edge + // resolver treats them as import artifacts (not locally-defined symbols). + // We use an empty target_file so the import-aware call-target lookup + // (`nodes_by_name_and_file.get(&(name, ""))`) always misses and falls + // through to the same-file shadow node — matching WASM call-resolution + // behaviour where CJS bindings are not in importedNamesMap (#1678). + if imp.cjs_require.unwrap_or(false) { + imported_names.push(ImportedName { name: clean_name, file: String::new() }); + continue; + } let mut target_file = resolved_path.clone(); if import_ctx.is_barrel_file(&resolved_path) { let mut visited = HashSet::new(); diff --git a/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs b/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs index 7471e8a16..713ecbaa9 100644 --- a/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs +++ b/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs @@ -432,6 +432,9 @@ pub fn build_import_edges(conn: &Connection, ctx: &ImportEdgeContext) -> Vec, #[napi(js_name = "bashSource")] pub bash_source: Option, + /// Marks a CJS destructured require binding (`const { X } = require('./m')`). + /// When true, this entry feeds imported_names for receiver-edge resolution + /// but must NOT produce a DB import edge (mirrors WASM cjsRequireBindings, #1678). + #[napi(js_name = "cjsRequire")] + pub cjs_require: Option, } impl Import { @@ -176,6 +181,7 @@ impl Import { swift_import: None, scala_import: None, bash_source: None, + cjs_require: None, } } } diff --git a/src/domain/wasm-worker-entry.ts b/src/domain/wasm-worker-entry.ts index a4a27dbd2..d55f5f7e6 100644 --- a/src/domain/wasm-worker-entry.ts +++ b/src/domain/wasm-worker-entry.ts @@ -834,7 +834,9 @@ function serializeExtractorOutput( ? { returnTypeMap: Array.from(symbols.returnTypeMap.entries()) } : {}), ...(symbols.callAssignments?.length ? { callAssignments: symbols.callAssignments } : {}), - ...(symbols.cjsRequireBindings?.length ? { cjsRequireBindings: symbols.cjsRequireBindings } : {}), + ...(symbols.cjsRequireBindings?.length + ? { cjsRequireBindings: symbols.cjsRequireBindings } + : {}), }; } diff --git a/src/extractors/dart.ts b/src/extractors/dart.ts index a7a6e06f9..fcee53f2a 100644 --- a/src/extractors/dart.ts +++ b/src/extractors/dart.ts @@ -264,7 +264,7 @@ function handleDartSelector(node: TreeSitterNode, ctx: ExtractorOutput): void { // the next holds argument_part (the call args) — method name is in the previous sibling const unconditional = findChild(node, 'unconditional_assignable_selector'); let methodName: string | null = null; - let receiverText: string | null = null; + let _receiverText: string | null = null; if (unconditional) { const id = findChild(unconditional, 'identifier'); @@ -283,7 +283,7 @@ function handleDartSelector(node: TreeSitterNode, ctx: ExtractorOutput): void { if (id2) methodName = id2.text; } } else { - receiverText = sibling?.text ?? null; + _receiverText = sibling?.text ?? null; } } } diff --git a/tests/parsers/dart.test.ts b/tests/parsers/dart.test.ts index 2d7557d0d..f0106a241 100644 --- a/tests/parsers/dart.test.ts +++ b/tests/parsers/dart.test.ts @@ -56,7 +56,11 @@ import 'package:flutter/material.dart';`); var r = Function.apply(callback, []); }`); expect(symbols.calls).toContainEqual( - expect.objectContaining({ name: '', dynamic: true, dynamicKind: 'unresolved-dynamic' }), + expect.objectContaining({ + name: '', + dynamic: true, + dynamicKind: 'unresolved-dynamic', + }), ); }); }); diff --git a/tests/parsers/objc.test.ts b/tests/parsers/objc.test.ts index dcd2b3cb4..221413268 100644 --- a/tests/parsers/objc.test.ts +++ b/tests/parsers/objc.test.ts @@ -96,7 +96,11 @@ describe('Objective-C parser', () => { [obj performSelector:@selector(greet)]; }`); expect(symbols.calls).toContainEqual( - expect.objectContaining({ name: '', dynamic: true, dynamicKind: 'unresolved-dynamic' }), + expect.objectContaining({ + name: '', + dynamic: true, + dynamicKind: 'unresolved-dynamic', + }), ); }); @@ -105,7 +109,11 @@ describe('Objective-C parser', () => { [obj performSelector:@selector(greet) withObject:arg]; }`); expect(symbols.calls).toContainEqual( - expect.objectContaining({ name: '', dynamic: true, dynamicKind: 'unresolved-dynamic' }), + expect.objectContaining({ + name: '', + dynamic: true, + dynamicKind: 'unresolved-dynamic', + }), ); }); @@ -114,7 +122,11 @@ describe('Objective-C parser', () => { objc_msgSend(obj, sel, arg); }`); expect(symbols.calls).toContainEqual( - expect.objectContaining({ name: '', dynamic: true, dynamicKind: 'unresolved-dynamic' }), + expect.objectContaining({ + name: '', + dynamic: true, + dynamicKind: 'unresolved-dynamic', + }), ); });