Skip to content

Commit 1c42569

Browse files
authored
Merge pull request DeusData#1042 from DeusData/fix/871-commonjs-require
fix(extract): stop CommonJS require() bindings from shadowing call resolution
2 parents 8c9ac98 + 66f2413 commit 1c42569

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

‎internal/cbm/extract_defs.c‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4646,6 +4646,37 @@ static void extract_destructured_vars(CBMExtractCtx *ctx, TSNode pattern, TSNode
46464646
}
46474647
}
46484648

4649+
/* True for `require("...")` call_expressions with a string-literal path — the
4650+
* CommonJS import form that extract_imports records with local_name = the
4651+
* enclosing declarator's identifier. Same string-argument kinds as
4652+
* process_commonjs_require so the two stay in lockstep. */
4653+
static bool is_require_import_call(TSNode value, const char *source, CBMArena *a) {
4654+
if (strcmp(ts_node_type(value), "call_expression") != 0) {
4655+
return false;
4656+
}
4657+
TSNode fn = ts_node_child_by_field_name(value, TS_FIELD("function"));
4658+
if (ts_node_is_null(fn) || strcmp(ts_node_type(fn), "identifier") != 0) {
4659+
return false;
4660+
}
4661+
char *fn_name = cbm_node_text(a, fn, source);
4662+
if (!fn_name || strcmp(fn_name, "require") != 0) {
4663+
return false;
4664+
}
4665+
TSNode args = ts_node_child_by_field_name(value, TS_FIELD("arguments"));
4666+
if (ts_node_is_null(args)) {
4667+
return false;
4668+
}
4669+
uint32_t argc = ts_node_named_child_count(args);
4670+
for (uint32_t i = 0; i < argc; i++) {
4671+
const char *ak = ts_node_type(ts_node_named_child(args, i));
4672+
if (strcmp(ak, "string") == 0 || strcmp(ak, "string_literal") == 0 ||
4673+
strcmp(ak, "template_string") == 0) {
4674+
return true;
4675+
}
4676+
}
4677+
return false;
4678+
}
4679+
46494680
// JS/TS variable extraction: skip function-assigned declarators.
46504681
static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
46514682
uint32_t n = ts_node_named_child_count(node);
@@ -4654,13 +4685,15 @@ static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
46544685
if (strcmp(ts_node_type(child), "variable_declarator") != 0) {
46554686
continue;
46564687
}
4688+
bool is_require = false;
46574689
TSNode value = ts_node_child_by_field_name(child, TS_FIELD("value"));
46584690
if (!ts_node_is_null(value)) {
46594691
const char *vk = ts_node_type(value);
46604692
if (strcmp(vk, "arrow_function") == 0 || strcmp(vk, "function_expression") == 0 ||
46614693
strcmp(vk, "generator_function") == 0) {
46624694
continue;
46634695
}
4696+
is_require = is_require_import_call(value, ctx->source, a);
46644697
}
46654698
TSNode vname = ts_node_child_by_field_name(child, TS_FIELD("name"));
46664699
if (!ts_node_is_null(vname)) {
@@ -4670,6 +4703,15 @@ static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
46704703
if (strcmp(nk, "object_pattern") == 0 || strcmp(nk, "array_pattern") == 0) {
46714704
extract_destructured_vars(ctx, vname, child, a);
46724705
} else {
4706+
/* `const foo = require('./foo')` is an import binding, not a
4707+
* definition — extract_imports records local_name="foo". A
4708+
* Variable node here shadows call resolution onto the alias
4709+
* and orphans the imported callee (#871); ESM `import`
4710+
* bindings emit no Variable either. Destructured requires
4711+
* keep theirs: the import row only records the module. */
4712+
if (is_require) {
4713+
continue;
4714+
}
46734715
push_var_def(ctx, cbm_node_text(a, vname, ctx->source), child);
46744716
}
46754717
}

‎tests/test_lang_contract.c‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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. */
12071269
TEST(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

Comments
 (0)