diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 6db2a73f7..b2b56291f 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -114,20 +114,23 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, *out_vals = NULL; *out_count = 0; - /* Fast path: build from cached extraction result (no JSON parsing) */ + /* Fast path: build from cached extraction via the same resolver used for + * IMPORTS edges. Do NOT use cbm_pipeline_fqn_module(module_path) — Python + * from-imports store "pkg.symbol" (and aliases) in module_path, which is + * not a filesystem rel-path and misses the def node. */ if (result && result->imports.count > 0) { const char **keys = calloc((size_t)result->imports.count, sizeof(const char *)); const char **vals = calloc((size_t)result->imports.count, sizeof(const char *)); int count = 0; + char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__"); for (int i = 0; i < result->imports.count; i++) { const CBMImport *imp = &result->imports.items[i]; if (!imp->local_name || !imp->local_name[0] || !imp->module_path) { continue; } - char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, imp->module_path); - const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn); - free(target_qn); + const cbm_gbuf_node_t *target = + cbm_pipeline_resolve_import_node(ctx, rel_path, file_qn, imp, NULL); if (!target) { continue; } @@ -135,11 +138,18 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, vals[count] = target->qualified_name; /* borrowed from gbuf */ count++; } + free(file_qn); - *out_keys = keys; - *out_vals = vals; - *out_count = count; - return 0; + if (count > 0) { + *out_keys = keys; + *out_vals = vals; + *out_count = count; + return 0; + } + free((void *)keys); + free((void *)vals); + /* Fall through to IMPORTS-edge scan when extraction paths did not + * resolve (should be rare once resolve_import_node is used). */ } /* Slow path: scan graph buffer IMPORTS edges + parse JSON properties */ diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index cf1b3f008..e0d609eb8 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -619,11 +619,12 @@ static cbm_resolution_t resolve_import_map(const cbm_registry_t *r, const char * * resolved.requireAdmin — not just resolved, which would point at the * module node and miss the function entirely. */ /* Direct hit ONLY for suffix-less callees (an aliased direct-symbol - * import called bare: `from m import f as g; g()` — #875/#979). With a - * suffix present (`imported.method()`), returning the bare base here - * would swallow the suffix and bind the call to the imported symbol's - * own node (a Variable/Class/module) instead of base.method — exactly - * the mis-resolution the comment above warns about. That regressed + * import called bare: `from m import f as g; g()` — #875/#979; Yui + * `import execute as bridge_execute`). With a suffix present + * (`imported.method()`), returning the bare base here would swallow + * the suffix and bind the call to the imported symbol's own node + * (a Variable/Class/module) instead of base.method — exactly the + * mis-resolution the comment above warns about. That regressed * django-scale graphs by ~11K CALLS/TESTS edges (Signal.send calls * degraded to edges onto the signal variables themselves). #1000 */ if (!suffix || !suffix[0]) { diff --git a/tests/test_lang_contract.c b/tests/test_lang_contract.c index 3ce639ded..31204e7d1 100644 --- a/tests/test_lang_contract.c +++ b/tests/test_lang_contract.c @@ -456,6 +456,54 @@ TEST(contract_python_relative_import) { PASS(); } +/* Python: aliased from-import must CALLS the real def (execute), not a ghost + * named after the local alias (bridge_execute). Mirrors Yui G1. */ +TEST(contract_python_aliased_from_import_calls) { + static const LangFile f[] = { + {"gate.py", "def execute(x):\n return x\n"}, + {"router.py", + "from .gate import execute as bridge_execute\n\n\n" + "def submit_task(y):\n return bridge_execute(y)\n"}}; + LangProj lp; + cbm_store_t *store = lang_index_files(&lp, f, 2); + ASSERT_TRUE(store != NULL); + cbm_node_t *nodes = NULL; + int ncount = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(store, lp.project, "submit_task", &nodes, &ncount), + CBM_STORE_OK); + ASSERT_TRUE(ncount >= 1); + char **callers = NULL; + char **callees = NULL; + int n_callers = 0; + int n_callees = 0; + ASSERT_EQ(cbm_store_node_neighbor_names(store, nodes[0].id, 32, &callers, &n_callers, &callees, + &n_callees), + 0); + int saw_execute = 0; + int saw_alias_ghost = 0; + for (int i = 0; i < n_callees; i++) { + if (callees[i] && strcmp(callees[i], "execute") == 0) { + saw_execute = 1; + } + if (callees[i] && strcmp(callees[i], "bridge_execute") == 0) { + saw_alias_ghost = 1; + } + } + for (int i = 0; i < n_callers; i++) { + free(callers[i]); + } + for (int i = 0; i < n_callees; i++) { + free(callees[i]); + } + free(callers); + free(callees); + cbm_store_free_nodes(nodes, ncount); + lang_cleanup(&lp, store); + ASSERT_TRUE(saw_execute); + ASSERT_FALSE(saw_alias_ghost); + PASS(); +} + /* TypeScript: a relative import resolves to a sibling module → IMPORTS edge. */ TEST(contract_typescript_relative_import) { static const LangFile f[] = { @@ -1625,6 +1673,7 @@ SUITE(lang_contract) { RUN_TEST(contract_java_methods); RUN_TEST(contract_kotlin_methods); RUN_TEST(contract_python_relative_import); + RUN_TEST(contract_python_aliased_from_import_calls); RUN_TEST(contract_typescript_relative_import); /* Graph-level breadth across all grammars (P4). */ diff --git a/tests/test_registry.c b/tests/test_registry.c index 725a6f935..42c6dae60 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -386,6 +386,28 @@ TEST(resolve_import_map_bare_alias) { PASS(); } +/* from M import execute as bridge_execute + bridge_execute() — IMPORTS target is + * already the def QN. Must CALLS→execute, not invent …M.bridge_execute. + * Yui G1 shape (alongside resolve_import_map_bare_alias). */ +TEST(resolve_import_map_aliased_from_import) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "execute", "proj.services.satori_bridge.gate.execute", "Function"); + /* Alias ghost must not win if somehow registered. */ + cbm_registry_add(r, "bridge_execute", "proj.services.satori_bridge.gate.bridge_execute", + "Function"); + + const char *keys[] = {"bridge_execute"}; + const char *vals[] = {"proj.services.satori_bridge.gate.execute"}; + + cbm_resolution_t res = + cbm_registry_resolve(r, "bridge_execute", "proj.services.yui_core.router", keys, vals, 1); + ASSERT_STR_EQ(res.qualified_name, "proj.services.satori_bridge.gate.execute"); + ASSERT_STR_EQ(res.strategy, "import_map"); + + cbm_registry_free(r); + PASS(); +} + TEST(resolve_unique_name) { cbm_registry_t *r = cbm_registry_new(); cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function"); @@ -860,6 +882,7 @@ SUITE(registry) { RUN_TEST(resolve_import_map); RUN_TEST(resolve_import_map_bare_function); RUN_TEST(resolve_import_map_bare_alias); + RUN_TEST(resolve_import_map_aliased_from_import); RUN_TEST(resolve_import_map_alias_with_suffix_hits_method); RUN_TEST(resolve_unique_name); RUN_TEST(resolve_unresolved);