diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 075378b12..e79c4e7a8 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -1520,6 +1520,31 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t const cbm_gbuf_node_t *target = target_qn ? cbm_gbuf_find_by_qn(ctx->gbuf, target_qn) : NULL; free(target_qn); if (target) { + /* Python/TS from-import of a member: module_path is often + * "pkg.mod.symbol" while resolve_module lands on the Module node + * "pkg.mod". Prefer the member Function/Class when it exists — + * required for `from M import f as g` so CALLS can import_map to f + * (local_name g ≠ f). */ + if (target->label && + (strcmp(target->label, "Module") == 0 || strcmp(target->label, "File") == 0)) { + char symbuf[256]; + const char *sym = import_candidate_symbol(imp->module_path, symbuf, sizeof(symbuf)); + if (sym && sym[0] && strcmp(sym, "*") != 0) { + const char *mod_tail = import_last_segment(target->qualified_name + ? target->qualified_name + : ""); + if (!mod_tail || strcmp(mod_tail, sym) != 0) { + char member_qn[CBM_SZ_512]; + snprintf(member_qn, sizeof(member_qn), "%s.%s", target->qualified_name, sym); + const cbm_gbuf_node_t *member = cbm_gbuf_find_by_qn(ctx->gbuf, member_qn); + if (member && import_targetable_label(member->label) && + strcmp(member->label, "Module") != 0 && + strcmp(member->label, "File") != 0) { + return member; + } + } + } + } return target; } diff --git a/tests/test_lang_contract.c b/tests/test_lang_contract.c index 31204e7d1..4b8dac93d 100644 --- a/tests/test_lang_contract.c +++ b/tests/test_lang_contract.c @@ -456,8 +456,7 @@ 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. */ +/* Python: relative aliased from-import must CALLS the real def (execute). */ TEST(contract_python_aliased_from_import_calls) { static const LangFile f[] = { {"gate.py", "def execute(x):\n return x\n"}, @@ -504,6 +503,56 @@ TEST(contract_python_aliased_from_import_calls) { PASS(); } +/* Python: absolute aliased from-import must CALLS the real def (Yui router shape). */ +TEST(contract_python_absolute_aliased_from_import_calls) { + static const LangFile f[] = { + {"services/satori_bridge/__init__.py", ""}, + {"services/satori_bridge/gate.py", "def execute(x):\n return x\n"}, + {"services/yui_core/__init__.py", ""}, + {"services/yui_core/router.py", + "from services.satori_bridge.gate import execute as bridge_execute\n\n\n" + "def submit_task(y):\n return bridge_execute(y)\n"}, + {"services/__init__.py", ""}}; + LangProj lp; + cbm_store_t *store = lang_index_files(&lp, f, 5); + 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[] = { @@ -1674,6 +1723,7 @@ SUITE(lang_contract) { RUN_TEST(contract_kotlin_methods); RUN_TEST(contract_python_relative_import); RUN_TEST(contract_python_aliased_from_import_calls); + RUN_TEST(contract_python_absolute_aliased_from_import_calls); RUN_TEST(contract_typescript_relative_import); /* Graph-level breadth across all grammars (P4). */