@@ -1468,7 +1468,19 @@ static bool pxc_edge_string_prop(const char *json, const char *key, char *out, s
14681468 return true;
14691469}
14701470
1471- /* `visited` holds barrel file QNs already searched for `name`: barrels form
1471+ static void pxc_free_visit_key (const char * key , void * value , void * userdata ) {
1472+ (void )key ;
1473+ (void )userdata ;
1474+ free (value );
1475+ }
1476+
1477+ /* Free a `visited` table built by pxc_barrel_symbol_qn and its owned keys. */
1478+ static void pxc_barrel_visited_free (CBMHashTable * visited ) {
1479+ cbm_ht_foreach (visited , pxc_free_visit_key , NULL );
1480+ cbm_ht_free (visited );
1481+ }
1482+
1483+ /* `visited` holds (barrel file QN, name) pairs already searched: barrels form
14721484 * DAGs (and cycles) where one lib index is reachable through many paths, and
14731485 * re-walking it on each path is exponential in the depth cap. */
14741486static char * pxc_barrel_symbol_qn (const cbm_gbuf_t * gbuf , const char * project_name ,
@@ -1501,10 +1513,24 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na
15011513 free (file_qn );
15021514 }
15031515 }
1504- if (!file_node || cbm_ht_has (visited , file_node -> qualified_name )) {
1516+ if (!file_node || !file_node -> qualified_name ) {
1517+ return NULL ;
1518+ }
1519+ /* Key on file and name: a named re-export renames the symbol, so one
1520+ * file can be searched for two names in one walk (`export *` and
1521+ * `export { a as b }` from the same module). The key owns its bytes and
1522+ * is the stored value; pxc_barrel_visited_free releases it. */
1523+ size_t vk_len = strlen (file_node -> qualified_name ) + 1 + strlen (name ) + 1 ;
1524+ char * visit_key = (char * )malloc (vk_len );
1525+ if (!visit_key ) {
15051526 return NULL ;
15061527 }
1507- cbm_ht_set (visited , file_node -> qualified_name , (void * )file_node );
1528+ snprintf (visit_key , vk_len , "%s\x1f%s" , file_node -> qualified_name , name );
1529+ if (cbm_ht_has (visited , visit_key )) {
1530+ free (visit_key );
1531+ return NULL ;
1532+ }
1533+ cbm_ht_set (visited , visit_key , visit_key );
15081534 char * defined = pxc_defined_symbol_qn (gbuf , project_name , rel , name );
15091535 if (defined ) {
15101536 return defined ;
@@ -1563,6 +1589,114 @@ static bool pxc_import_map_has_local(const char *const *keys, int count, const c
15631589 return false;
15641590}
15651591
1592+ /* True when `local` is a JS/TS module-object binding: `import * as local`
1593+ * or `const local = require(...)`. Such an import has no member, is not a
1594+ * default import and is not a re-export. A local that also names a member
1595+ * import is not treated as one. */
1596+ static bool pxc_js_namespace_local (const CBMFileResult * result , const char * local ) {
1597+ if (!result || !local ) {
1598+ return false;
1599+ }
1600+ bool found = false;
1601+ for (int i = 0 ; i < result -> imports .count ; i ++ ) {
1602+ const CBMImport * imp = & result -> imports .items [i ];
1603+ if (!imp -> local_name || strcmp (imp -> local_name , local ) != 0 ) {
1604+ continue ;
1605+ }
1606+ if (imp -> member || imp -> is_default || imp -> reexport != CBM_REEXPORT_NONE ||
1607+ !cbm_import_binds_runtime_name (imp )) {
1608+ return false;
1609+ }
1610+ found = true;
1611+ }
1612+ return found ;
1613+ }
1614+
1615+ /* A JS identifier: the member part of `ns.member`. */
1616+ static bool pxc_js_identifier (const char * s ) {
1617+ if (!s || !s [0 ] || (s [0 ] >= '0' && s [0 ] <= '9' )) {
1618+ return false;
1619+ }
1620+ for (const char * p = s ; * p ; p ++ ) {
1621+ char c = * p ;
1622+ if (!((c >= 'a' && c <= 'z' ) || (c >= 'A' && c <= 'Z' ) || (c >= '0' && c <= '9' ) ||
1623+ c == '_' || c == '$' )) {
1624+ return false;
1625+ }
1626+ }
1627+ return true;
1628+ }
1629+
1630+ /* Grow the parallel key/value arrays of an import map to hold `need` slots.
1631+ * Namespace members add entries beyond the one-per-edge first estimate, so
1632+ * every write in cbm_pxc_build_import_map_ex reserves first. */
1633+ static bool pxc_import_map_reserve (const char * * * keys , const char * * * vals , size_t * capacity ,
1634+ size_t need ) {
1635+ if (need <= * capacity ) {
1636+ return true;
1637+ }
1638+ size_t grown = * capacity * 2 > need ? * capacity * 2 : need ;
1639+ const char * * nk = (const char * * )realloc ((void * )* keys , grown * sizeof (char * ));
1640+ if (!nk ) {
1641+ return false;
1642+ }
1643+ * keys = nk ;
1644+ const char * * nv = (const char * * )realloc ((void * )* vals , grown * sizeof (char * ));
1645+ if (!nv ) {
1646+ return false; /* keys grew, capacity did not: still consistent */
1647+ }
1648+ * vals = nv ;
1649+ * capacity = grown ;
1650+ return true;
1651+ }
1652+
1653+ /* Calls through a namespace import. For each call `local.member()` in the
1654+ * file, follow the imported module (and its re-export chain, as for a named
1655+ * import) to the one file that defines `member`, and add the key
1656+ * "local.member" with that definition's QN. The registry looks up the whole
1657+ * callee as a key before it splits it, so `ns.fn()` binds to fn's defining
1658+ * file even when the imported module only re-exports fn. Nothing is added
1659+ * when no definition, or more than one, is found. */
1660+ static void pxc_add_js_namespace_members (const cbm_gbuf_t * gbuf , const char * project_name ,
1661+ const CBMFileResult * result , const char * local ,
1662+ const cbm_gbuf_node_t * target , const char * * * keys ,
1663+ const char * * * vals , int * count , size_t * capacity ) {
1664+ if (!result || !target || !target -> label ||
1665+ (strcmp (target -> label , "Module" ) != 0 && strcmp (target -> label , "Folder" ) != 0 )) {
1666+ return ;
1667+ }
1668+ size_t local_len = strlen (local );
1669+ for (int i = 0 ; i < result -> calls .count ; i ++ ) {
1670+ const char * callee = result -> calls .items [i ].callee_name ;
1671+ if (!callee || strncmp (callee , local , local_len ) != 0 || callee [local_len ] != '.' ||
1672+ !pxc_js_identifier (callee + local_len + 1 ) ||
1673+ pxc_import_map_has_local (* keys , * count , callee )) {
1674+ continue ;
1675+ }
1676+ CBMHashTable * visited = cbm_ht_create (16 );
1677+ char * qn = visited ? pxc_barrel_symbol_qn (gbuf , project_name , target ,
1678+ callee + local_len + 1 , 0 , visited )
1679+ : NULL ;
1680+ pxc_barrel_visited_free (visited );
1681+ if (!qn ) {
1682+ continue ;
1683+ }
1684+ char * key = strdup (callee );
1685+ if (!key ) {
1686+ free (qn );
1687+ continue ;
1688+ }
1689+ if (!pxc_import_map_reserve (keys , vals , capacity , (size_t )* count + 1 )) {
1690+ free (key );
1691+ free (qn );
1692+ return ;
1693+ }
1694+ (* keys )[* count ] = key ;
1695+ (* vals )[* count ] = qn ;
1696+ (* count )++ ;
1697+ }
1698+ }
1699+
15661700/* Recover a Python import directly from extraction metadata when graph import
15671701 * materialization produced no edge. Prefer an already materialized exact node
15681702 * (including the shared project-prefix rule); otherwise accept only the
@@ -1710,17 +1844,26 @@ int cbm_pxc_build_import_map_ex(const cbm_gbuf_t *gbuf, const char *project_name
17101844 CBMHashTable * visited = direct ? NULL : cbm_ht_create (16 );
17111845 if (visited ) {
17121846 barrel = pxc_barrel_symbol_qn (gbuf , project_name , target , local , 0 , visited );
1713- cbm_ht_free (visited );
1847+ pxc_barrel_visited_free (visited );
17141848 }
17151849 free (direct );
17161850 if (barrel ) {
17171851 free (value );
17181852 value = barrel ;
17191853 }
17201854 }
1855+ if (!pxc_import_map_reserve (& keys , & vals , & capacity , (size_t )count + 1 )) {
1856+ free (local );
1857+ free (value );
1858+ continue ;
1859+ }
17211860 keys [count ] = local ;
17221861 vals [count ] = value ;
17231862 count ++ ;
1863+ if (pxc_is_js_family (lang ) && pxc_js_namespace_local (result , local )) {
1864+ pxc_add_js_namespace_members (gbuf , project_name , result , local , target , & keys , & vals ,
1865+ & count , & capacity );
1866+ }
17241867 }
17251868
17261869 /* IMPORTS edges are best-effort graph relationships. The cross-LSP still
@@ -1745,7 +1888,8 @@ int cbm_pxc_build_import_map_ex(const cbm_gbuf_t *gbuf, const char *project_name
17451888 if (!value )
17461889 continue ;
17471890 char * local = strdup (imp -> local_name );
1748- if (!local ) {
1891+ if (!local || !pxc_import_map_reserve (& keys , & vals , & capacity , (size_t )count + 1 )) {
1892+ free (local );
17491893 free (value );
17501894 continue ;
17511895 }
0 commit comments