Skip to content

Commit 07cac7d

Browse files
authored
Merge pull request DeusData#1371 from Joseph-MingEn/fix/py-aliased-from-import-calls
fix(python): resolve aliased from-import CALLS to real def
2 parents 9de6831 + d613840 commit 07cac7d

5 files changed

Lines changed: 212 additions & 13 deletions

File tree

‎src/pipeline/pass_calls.c‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,32 +114,42 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path,
114114
*out_vals = NULL;
115115
*out_count = 0;
116116

117-
/* Fast path: build from cached extraction result (no JSON parsing) */
117+
/* Fast path: build from cached extraction via the same resolver used for
118+
* IMPORTS edges. Do NOT use cbm_pipeline_fqn_module(module_path) — Python
119+
* from-imports store "pkg.symbol" (and aliases) in module_path, which is
120+
* not a filesystem rel-path and misses the def node. */
118121
if (result && result->imports.count > 0) {
119122
const char **keys = calloc((size_t)result->imports.count, sizeof(const char *));
120123
const char **vals = calloc((size_t)result->imports.count, sizeof(const char *));
121124
int count = 0;
125+
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__");
122126

123127
for (int i = 0; i < result->imports.count; i++) {
124128
const CBMImport *imp = &result->imports.items[i];
125129
if (!imp->local_name || !imp->local_name[0] || !imp->module_path) {
126130
continue;
127131
}
128-
char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, imp->module_path);
129-
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
130-
free(target_qn);
132+
const cbm_gbuf_node_t *target =
133+
cbm_pipeline_resolve_import_node(ctx, rel_path, file_qn, imp, NULL);
131134
if (!target) {
132135
continue;
133136
}
134137
keys[count] = strdup(imp->local_name);
135138
vals[count] = target->qualified_name; /* borrowed from gbuf */
136139
count++;
137140
}
141+
free(file_qn);
138142

139-
*out_keys = keys;
140-
*out_vals = vals;
141-
*out_count = count;
142-
return 0;
143+
if (count > 0) {
144+
*out_keys = keys;
145+
*out_vals = vals;
146+
*out_count = count;
147+
return 0;
148+
}
149+
free((void *)keys);
150+
free((void *)vals);
151+
/* Fall through to IMPORTS-edge scan when extraction paths did not
152+
* resolve (should be rare once resolve_import_node is used). */
143153
}
144154

145155
/* Slow path: scan graph buffer IMPORTS edges + parse JSON properties */

‎src/pipeline/pass_pkgmap.c‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,6 +1790,30 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
17901790
const cbm_gbuf_node_t *target = target_qn ? cbm_gbuf_find_by_qn(ctx->gbuf, target_qn) : NULL;
17911791
free(target_qn);
17921792
if (target) {
1793+
/* Python/TS from-import of a member: module_path is often
1794+
* "pkg.mod.symbol" while resolve_module lands on the Module node
1795+
* "pkg.mod". Prefer the member Function/Class when it exists —
1796+
* required for `from M import f as g` so CALLS can import_map to f
1797+
* (local_name g ≠ f). */
1798+
if (target->label &&
1799+
(strcmp(target->label, "Module") == 0 || strcmp(target->label, "File") == 0)) {
1800+
char symbuf[256];
1801+
const char *sym = import_candidate_symbol(imp->module_path, symbuf, sizeof(symbuf));
1802+
if (sym && sym[0] && strcmp(sym, "*") != 0) {
1803+
const char *mod_tail =
1804+
import_last_segment(target->qualified_name ? target->qualified_name : "");
1805+
if (!mod_tail || strcmp(mod_tail, sym) != 0) {
1806+
char member_qn[CBM_SZ_512];
1807+
snprintf(member_qn, sizeof(member_qn), "%s.%s", target->qualified_name, sym);
1808+
const cbm_gbuf_node_t *member = cbm_gbuf_find_by_qn(ctx->gbuf, member_qn);
1809+
if (member && import_targetable_label(member->label) &&
1810+
strcmp(member->label, "Module") != 0 &&
1811+
strcmp(member->label, "File") != 0) {
1812+
return member;
1813+
}
1814+
}
1815+
}
1816+
}
17931817
return target;
17941818
}
17951819

‎src/pipeline/registry.c‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -662,11 +662,12 @@ static cbm_resolution_t resolve_import_map(const cbm_registry_t *r, const char *
662662
* resolved.requireAdmin — not just resolved, which would point at the
663663
* module node and miss the function entirely. */
664664
/* Direct hit ONLY for suffix-less callees (an aliased direct-symbol
665-
* import called bare: `from m import f as g; g()` — #875/#979). With a
666-
* suffix present (`imported.method()`), returning the bare base here
667-
* would swallow the suffix and bind the call to the imported symbol's
668-
* own node (a Variable/Class/module) instead of base.method — exactly
669-
* the mis-resolution the comment above warns about. That regressed
665+
* import called bare: `from m import f as g; g()` — #875/#979; Yui
666+
* `import execute as bridge_execute`). With a suffix present
667+
* (`imported.method()`), returning the bare base here would swallow
668+
* the suffix and bind the call to the imported symbol's own node
669+
* (a Variable/Class/module) instead of base.method — exactly the
670+
* mis-resolution the comment above warns about. That regressed
670671
* django-scale graphs by ~11K CALLS/TESTS edges (Signal.send calls
671672
* degraded to edges onto the signal variables themselves). #1000 */
672673
if (!suffix || !suffix[0]) {

‎tests/test_lang_contract.c‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,144 @@ TEST(contract_python_relative_import) {
460460
PASS();
461461
}
462462

463+
/* Python: relative aliased from-import must CALLS the real def (execute). */
464+
TEST(contract_python_aliased_from_import_calls) {
465+
static const LangFile f[] = {
466+
{"gate.py", "def execute(x):\n return x\n"},
467+
{"router.py",
468+
"from .gate import execute as bridge_execute\n\n\n"
469+
"def submit_task(y):\n return bridge_execute(y)\n"}};
470+
LangProj lp;
471+
cbm_store_t *store = lang_index_files(&lp, f, 2);
472+
ASSERT_TRUE(store != NULL);
473+
cbm_node_t *nodes = NULL;
474+
int ncount = 0;
475+
ASSERT_EQ(cbm_store_find_nodes_by_name(store, lp.project, "submit_task", &nodes, &ncount),
476+
CBM_STORE_OK);
477+
ASSERT_TRUE(ncount >= 1);
478+
char **callers = NULL;
479+
char **callees = NULL;
480+
int n_callers = 0;
481+
int n_callees = 0;
482+
ASSERT_EQ(cbm_store_node_neighbor_names(store, nodes[0].id, 32, &callers, &n_callers, &callees,
483+
&n_callees),
484+
0);
485+
int saw_execute = 0;
486+
int saw_alias_ghost = 0;
487+
for (int i = 0; i < n_callees; i++) {
488+
if (callees[i] && strcmp(callees[i], "execute") == 0) {
489+
saw_execute = 1;
490+
}
491+
if (callees[i] && strcmp(callees[i], "bridge_execute") == 0) {
492+
saw_alias_ghost = 1;
493+
}
494+
}
495+
for (int i = 0; i < n_callers; i++) {
496+
free(callers[i]);
497+
}
498+
for (int i = 0; i < n_callees; i++) {
499+
free(callees[i]);
500+
}
501+
free(callers);
502+
free(callees);
503+
cbm_store_free_nodes(nodes, ncount);
504+
lang_cleanup(&lp, store);
505+
ASSERT_TRUE(saw_execute);
506+
ASSERT_FALSE(saw_alias_ghost);
507+
PASS();
508+
}
509+
510+
/* Python: absolute aliased from-import must CALLS the real def (Yui router shape). */
511+
TEST(contract_python_absolute_aliased_from_import_calls) {
512+
static const LangFile f[] = {
513+
{"services/satori_bridge/__init__.py", ""},
514+
{"services/satori_bridge/gate.py", "def execute(x):\n return x\n"},
515+
{"services/yui_core/__init__.py", ""},
516+
{"services/yui_core/router.py",
517+
"from services.satori_bridge.gate import execute as bridge_execute\n\n\n"
518+
"def submit_task(y):\n return bridge_execute(y)\n"},
519+
{"services/__init__.py", ""}};
520+
LangProj lp;
521+
cbm_store_t *store = lang_index_files(&lp, f, 5);
522+
ASSERT_TRUE(store != NULL);
523+
cbm_node_t *nodes = NULL;
524+
int ncount = 0;
525+
ASSERT_EQ(cbm_store_find_nodes_by_name(store, lp.project, "submit_task", &nodes, &ncount),
526+
CBM_STORE_OK);
527+
ASSERT_TRUE(ncount >= 1);
528+
char **callers = NULL;
529+
char **callees = NULL;
530+
int n_callers = 0;
531+
int n_callees = 0;
532+
ASSERT_EQ(cbm_store_node_neighbor_names(store, nodes[0].id, 32, &callers, &n_callers, &callees,
533+
&n_callees),
534+
0);
535+
int saw_execute = 0;
536+
int saw_alias_ghost = 0;
537+
for (int i = 0; i < n_callees; i++) {
538+
if (callees[i] && strcmp(callees[i], "execute") == 0) {
539+
saw_execute = 1;
540+
}
541+
if (callees[i] && strcmp(callees[i], "bridge_execute") == 0) {
542+
saw_alias_ghost = 1;
543+
}
544+
}
545+
for (int i = 0; i < n_callers; i++) {
546+
free(callers[i]);
547+
}
548+
for (int i = 0; i < n_callees; i++) {
549+
free(callees[i]);
550+
}
551+
free(callers);
552+
free(callees);
553+
cbm_store_free_nodes(nodes, ncount);
554+
lang_cleanup(&lp, store);
555+
ASSERT_TRUE(saw_execute);
556+
ASSERT_FALSE(saw_alias_ghost);
557+
PASS();
558+
}
559+
560+
/* Python: unresolvable module + alias must emit no CALLS (no invented edge). */
561+
TEST(contract_python_ghost_module_aliased_from_import_no_calls) {
562+
static const LangFile f[] = {
563+
{"router.py",
564+
"from ghost_module import nope as alias\n\n\n"
565+
"def submit_task(y):\n return alias(y)\n"}};
566+
LangProj lp;
567+
cbm_store_t *store = lang_index_files(&lp, f, 1);
568+
ASSERT_TRUE(store != NULL);
569+
cbm_node_t *nodes = NULL;
570+
int ncount = 0;
571+
ASSERT_EQ(cbm_store_find_nodes_by_name(store, lp.project, "submit_task", &nodes, &ncount),
572+
CBM_STORE_OK);
573+
ASSERT_TRUE(ncount >= 1);
574+
char **callers = NULL;
575+
char **callees = NULL;
576+
int n_callers = 0;
577+
int n_callees = 0;
578+
ASSERT_EQ(cbm_store_node_neighbor_names(store, nodes[0].id, 32, &callers, &n_callers, &callees,
579+
&n_callees),
580+
0);
581+
int saw_any_callee = 0;
582+
for (int i = 0; i < n_callees; i++) {
583+
if (callees[i] && callees[i][0]) {
584+
saw_any_callee = 1;
585+
}
586+
}
587+
for (int i = 0; i < n_callers; i++) {
588+
free(callers[i]);
589+
}
590+
for (int i = 0; i < n_callees; i++) {
591+
free(callees[i]);
592+
}
593+
free(callers);
594+
free(callees);
595+
cbm_store_free_nodes(nodes, ncount);
596+
lang_cleanup(&lp, store);
597+
ASSERT_FALSE(saw_any_callee);
598+
PASS();
599+
}
600+
463601
/* TypeScript: a relative import resolves to a sibling module → IMPORTS edge. */
464602
TEST(contract_typescript_relative_import) {
465603
static const LangFile f[] = {
@@ -1648,6 +1786,9 @@ SUITE(lang_contract) {
16481786
RUN_TEST(contract_java_methods);
16491787
RUN_TEST(contract_kotlin_methods);
16501788
RUN_TEST(contract_python_relative_import);
1789+
RUN_TEST(contract_python_aliased_from_import_calls);
1790+
RUN_TEST(contract_python_absolute_aliased_from_import_calls);
1791+
RUN_TEST(contract_python_ghost_module_aliased_from_import_no_calls);
16511792
RUN_TEST(contract_typescript_relative_import);
16521793

16531794
/* Graph-level breadth across all grammars (P4). */

‎tests/test_registry.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,28 @@ TEST(resolve_import_map_bare_alias) {
386386
PASS();
387387
}
388388

389+
/* Adversarial pin: when import_map already stores the def QN under the alias
390+
* key, bare alias call must CALLS→def (not invent …M.bridge_execute). Behavior
391+
* already on main via #875/#979; this locks the Yui G1 shape. */
392+
TEST(resolve_import_map_aliased_from_import) {
393+
cbm_registry_t *r = cbm_registry_new();
394+
cbm_registry_add(r, "execute", "proj.services.satori_bridge.gate.execute", "Function");
395+
/* Alias ghost must not win if somehow registered. */
396+
cbm_registry_add(r, "bridge_execute", "proj.services.satori_bridge.gate.bridge_execute",
397+
"Function");
398+
399+
const char *keys[] = {"bridge_execute"};
400+
const char *vals[] = {"proj.services.satori_bridge.gate.execute"};
401+
402+
cbm_resolution_t res =
403+
cbm_registry_resolve(r, "bridge_execute", "proj.services.yui_core.router", keys, vals, 1);
404+
ASSERT_STR_EQ(res.qualified_name, "proj.services.satori_bridge.gate.execute");
405+
ASSERT_STR_EQ(res.strategy, "import_map");
406+
407+
cbm_registry_free(r);
408+
PASS();
409+
}
410+
389411
TEST(resolve_unique_name) {
390412
cbm_registry_t *r = cbm_registry_new();
391413
cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function");
@@ -888,6 +910,7 @@ SUITE(registry) {
888910
RUN_TEST(resolve_import_map);
889911
RUN_TEST(resolve_import_map_bare_function);
890912
RUN_TEST(resolve_import_map_bare_alias);
913+
RUN_TEST(resolve_import_map_aliased_from_import);
891914
RUN_TEST(resolve_import_map_alias_with_suffix_hits_method);
892915
RUN_TEST(resolve_unique_name);
893916
RUN_TEST(resolve_unresolved);

0 commit comments

Comments
 (0)