@@ -1207,6 +1207,41 @@ TEST(contract_edge_imports_alias_resolves_real_file_issue767) {
12071207 PASS();
12081208}
12091209
1210+ /* True if a CALLS edge exists whose source QN ends with `src_suffix` and
1211+ * target QN ends with `tgt_suffix`. */
1212+ static int calls_edge_between(cbm_store_t *store, const char *project, const char *src_suffix,
1213+ const char *tgt_suffix) {
1214+ cbm_edge_t *edges = NULL;
1215+ int n = 0;
1216+ if (cbm_store_find_edges_by_type(store, project, "CALLS", &edges, &n) != CBM_STORE_OK)
1217+ return 0;
1218+ int found = 0;
1219+ size_t ssl = strlen(src_suffix);
1220+ size_t tsl = strlen(tgt_suffix);
1221+ for (int i = 0; i < n && !found; i++) {
1222+ cbm_node_t s, t;
1223+ if (cbm_store_find_node_by_id(store, edges[i].source_id, &s) != CBM_STORE_OK)
1224+ continue;
1225+ if (cbm_store_find_node_by_id(store, edges[i].target_id, &t) != CBM_STORE_OK) {
1226+ cbm_node_free_fields(&s);
1227+ continue;
1228+ }
1229+ const char *sq = s.qualified_name;
1230+ const char *tq = t.qualified_name;
1231+ if (sq && tq) {
1232+ size_t sql = strlen(sq);
1233+ size_t tql = strlen(tq);
1234+ if (sql >= ssl && strcmp(sq + sql - ssl, src_suffix) == 0 && tql >= tsl &&
1235+ strcmp(tq + tql - tsl, tgt_suffix) == 0)
1236+ found = 1;
1237+ }
1238+ cbm_node_free_fields(&s);
1239+ cbm_node_free_fields(&t);
1240+ }
1241+ cbm_store_free_edges(edges, n);
1242+ return found;
1243+ }
1244+
12101245/* #999: URLs in CI/tooling configs (.pre-commit-config.yaml, .github/
12111246 * workflows) are repository references for TOOLING, not endpoints this
12121247 * service exposes. They must not mint __route__infra__ Route nodes — the
@@ -1327,6 +1362,55 @@ static int calls_edge_targets(cbm_store_t *store, const char *project, const cha
13271362 return found;
13281363}
13291364
1365+ /* #988: `from m import f as g; g()` produced NO CALLS edge — the Python LSP
1366+ * bound the alias as MODULE("m.f") (the from-style heuristic keys on the
1367+ * local name matching the module-path tail, which an alias never does), so
1368+ * the call missed, and the registry fallback did not cover the minimal
1369+ * two-file shape either. The alias must resolve exactly like the plain
1370+ * import. The other import forms are pinned alongside as invariance guards
1371+ * (all resolve on main today and must keep resolving). */
1372+ TEST(contract_edge_python_aliased_import_call_resolves_issue988) {
1373+ LangProj lp;
1374+ static const LangFile f[] = {
1375+ {"m.py", "def f(x):\n return x + 1\n"},
1376+ {"pkg/__init__.py", ""},
1377+ {"pkg/dm.py", "def h(x):\n return x\n"},
1378+ {"caller_alias.py", "from m import f as g\n"
1379+ "\n"
1380+ "def use_alias(x):\n"
1381+ " return g(x)\n"},
1382+ {"caller_plain.py", "from m import f\n"
1383+ "\n"
1384+ "def use_plain(x):\n"
1385+ " return f(x)\n"},
1386+ {"caller_modalias.py", "import m as mm\n"
1387+ "\n"
1388+ "def use_modalias(x):\n"
1389+ " return mm.f(x)\n"},
1390+ {"caller_dotalias.py", "import pkg.dm as dz\n"
1391+ "\n"
1392+ "def use_dotalias(x):\n"
1393+ " return dz.h(x)\n"}};
1394+ cbm_store_t *store = lang_index_files(&lp, f, 7);
1395+ ASSERT_TRUE(store != NULL);
1396+ int alias = calls_edge_between(store, lp.project, ".use_alias", ".m.f");
1397+ int plain = calls_edge_between(store, lp.project, ".use_plain", ".m.f");
1398+ int modalias = calls_edge_between(store, lp.project, ".use_modalias", ".m.f");
1399+ int dotalias = calls_edge_between(store, lp.project, ".use_dotalias", ".pkg.dm.h");
1400+ if (!alias || !plain || !modalias || !dotalias) {
1401+ fprintf(stderr,
1402+ " [988] FAIL alias=%d plain=%d modalias=%d dotalias=%d (all import forms "
1403+ "must produce the CALLS edge)\n",
1404+ alias, plain, modalias, dotalias);
1405+ }
1406+ ASSERT_TRUE(alias); /* the #988 bug: aliased from-import call lost */
1407+ ASSERT_TRUE(plain);
1408+ ASSERT_TRUE(modalias);
1409+ ASSERT_TRUE(dotalias);
1410+ lang_cleanup(&lp, store);
1411+ PASS();
1412+ }
1413+
13301414/* #871: a CommonJS require() binding shadowed call resolution. `const doThing
13311415 * = require("../bs/doThing")` emitted a Variable def for `doThing` in the
13321416 * importing module, so the call `doThing(...)` resolved to that same-module
@@ -1572,6 +1656,7 @@ SUITE(lang_contract) {
15721656 RUN_TEST(contract_edge_workspaces_imports_issue408);
15731657 RUN_TEST(contract_edge_imports_alias_no_phantom_folder_edge_issue767);
15741658 RUN_TEST(contract_edge_imports_alias_resolves_real_file_issue767);
1659+ RUN_TEST(contract_edge_python_aliased_import_call_resolves_issue988);
15751660 RUN_TEST(contract_edge_no_infra_routes_from_ci_configs_issue999);
15761661 RUN_TEST(contract_edge_infra_routes_from_deploy_configs_still_minted);
15771662 RUN_TEST(contract_edge_commonjs_require_call_resolves_issue871);
0 commit comments