@@ -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