@@ -1202,6 +1202,68 @@ TEST(contract_edge_imports_alias_resolves_real_file_issue767) {
12021202 PASS ();
12031203}
12041204
1205+ /* True if some CALLS edge's TARGET node carries `label` and a QN ending with
1206+ * `qn_suffix` — i.e. the call resolved to that specific definition. */
1207+ static int calls_edge_targets (cbm_store_t * store , const char * project , const char * label ,
1208+ const char * qn_suffix ) {
1209+ cbm_edge_t * edges = NULL ;
1210+ int n = 0 ;
1211+ if (cbm_store_find_edges_by_type (store , project , "CALLS" , & edges , & n ) != CBM_STORE_OK )
1212+ return 0 ;
1213+ int found = 0 ;
1214+ size_t sl = strlen (qn_suffix );
1215+ for (int i = 0 ; i < n && !found ; i ++ ) {
1216+ cbm_node_t tgt ;
1217+ if (cbm_store_find_node_by_id (store , edges [i ].target_id , & tgt ) != CBM_STORE_OK )
1218+ continue ;
1219+ const char * qn = tgt .qualified_name ;
1220+ if (qn && tgt .label && strcmp (tgt .label , label ) == 0 ) {
1221+ size_t ql = strlen (qn );
1222+ if (ql >= sl && strcmp (qn + ql - sl , qn_suffix ) == 0 )
1223+ found = 1 ;
1224+ }
1225+ cbm_node_free_fields (& tgt );
1226+ }
1227+ cbm_store_free_edges (edges , n );
1228+ return found ;
1229+ }
1230+
1231+ /* #871: a CommonJS require() binding shadowed call resolution. `const doThing
1232+ * = require("../bs/doThing")` emitted a Variable def for `doThing` in the
1233+ * importing module, so the call `doThing(...)` resolved to that same-module
1234+ * Variable (an import ALIAS, not a definition) instead of following the
1235+ * recorded import to the exported function — which stayed at zero inbound
1236+ * CALLS (the reporter's disconnected-node symptom). ESM `import` bindings
1237+ * never materialize as Variables; the identifier-bound require form must
1238+ * behave identically: no shadowing Variable, CALLS edge lands on the real
1239+ * Function. Fixture = the reporter's exact two-file repro. */
1240+ TEST (contract_edge_commonjs_require_call_resolves_issue871 ) {
1241+ LangProj lp ;
1242+ static const LangFile f [] = {
1243+ {"src/bs/doThing.js" ,
1244+ "module.exports = async function doThing({ id }) {\n return id;\n};\n" },
1245+ {"src/mutations/doThing.js" ,
1246+ "const doThing = require(\"../bs/doThing\");\n\n"
1247+ "module.exports = async (parent, args) => {\n return doThing({ id: args.id });\n};\n" }};
1248+ cbm_store_t * store = lang_index_files (& lp , f , 2 );
1249+ ASSERT_TRUE (store != NULL );
1250+ /* The call resolves THROUGH the require to the exported function. */
1251+ int resolved = calls_edge_targets (store , lp .project , "Function" , ".bs.doThing.doThing" );
1252+ /* The alias must not swallow the call: no CALLS edge may terminate on a
1253+ * Variable in the importing module (ESM parity: the binding is an import,
1254+ * not a definition). */
1255+ int shadowed = calls_edge_targets (store , lp .project , "Variable" , ".mutations.doThing.doThing" );
1256+ if (!resolved || shadowed ) {
1257+ fprintf (stderr , " [871] FAIL resolved=%d shadowed=%d (require binding must resolve to "
1258+ "the exported Function, not the local alias Variable)\n" ,
1259+ resolved , shadowed );
1260+ }
1261+ ASSERT_TRUE (resolved );
1262+ ASSERT_TRUE (!shadowed );
1263+ lang_cleanup (& lp , store );
1264+ PASS ();
1265+ }
1266+
12051267/* DEPENDS_ON — Helm Chart.yaml `dependencies:` -> per-dependency Chart node.
12061268 * Basename must be exactly "Chart.yaml"; pass_k8s runs in both pipeline paths. */
12071269TEST (contract_edge_depends_on ) {
@@ -1411,6 +1473,7 @@ SUITE(lang_contract) {
14111473 RUN_TEST (contract_edge_workspaces_imports_issue408 );
14121474 RUN_TEST (contract_edge_imports_alias_no_phantom_folder_edge_issue767 );
14131475 RUN_TEST (contract_edge_imports_alias_resolves_real_file_issue767 );
1476+ RUN_TEST (contract_edge_commonjs_require_call_resolves_issue871 );
14141477 RUN_TEST (contract_edge_depends_on );
14151478 RUN_TEST (contract_edge_parallel_service_edges );
14161479 RUN_TEST (contract_edge_file_changes_with );
0 commit comments