@@ -457,28 +457,35 @@ static void extract_psr4(yyjson_val *root, const char *section, const char *dir,
457457 yyjson_obj_iter iter = yyjson_obj_iter_with (psr4 );
458458 while ((key = yyjson_obj_iter_next (& iter )) != NULL ) {
459459 yyjson_val * val = yyjson_obj_iter_get_val (key );
460- if (!yyjson_is_str (key ) || ! yyjson_is_str ( val ) ) {
460+ if (!yyjson_is_str (key )) {
461461 continue ;
462462 }
463463 const char * ns_prefix = yyjson_get_str (key );
464- const char * ns_dir = yyjson_get_str (val );
465- while (ns_dir [0 ] == '.' && ns_dir [SKIP_ONE ] == '/' ) {
466- ns_dir += PAIR_LEN ; /* "./src/" names src/ */
467- }
468- if (strcmp (ns_dir , "." ) == 0 ) {
469- ns_dir = "" ; /* "." names the manifest's own directory */
470- }
471- char ns_entry [PKGMAP_PATH_BUF ];
472- if (dir [0 ]) {
473- snprintf (ns_entry , sizeof (ns_entry ), "%s/%s" , dir , ns_dir );
474- } else {
475- snprintf (ns_entry , sizeof (ns_entry ), "%s" , ns_dir );
476- }
477- size_t nelen = strlen (ns_entry );
478- if (nelen > 0 && ns_entry [nelen - SKIP_ONE ] == '/' ) {
479- ns_entry [nelen - SKIP_ONE ] = '\0' ;
464+ size_t count = yyjson_is_arr (val ) ? yyjson_arr_size (val ) : 1 ;
465+ for (size_t i = 0 ; i < count ; i ++ ) {
466+ yyjson_val * path = yyjson_is_arr (val ) ? yyjson_arr_get (val , i ) : val ;
467+ if (!yyjson_is_str (path )) {
468+ continue ;
469+ }
470+ const char * ns_dir = yyjson_get_str (path );
471+ while (ns_dir [0 ] == '.' && ns_dir [SKIP_ONE ] == '/' ) {
472+ ns_dir += PAIR_LEN ; /* "./src/" names src/ */
473+ }
474+ if (strcmp (ns_dir , "." ) == 0 ) {
475+ ns_dir = "" ; /* "." names the manifest's own directory */
476+ }
477+ char ns_entry [PKGMAP_PATH_BUF ];
478+ if (dir [0 ]) {
479+ snprintf (ns_entry , sizeof (ns_entry ), "%s/%s" , dir , ns_dir );
480+ } else {
481+ snprintf (ns_entry , sizeof (ns_entry ), "%s" , ns_dir );
482+ }
483+ size_t nelen = strlen (ns_entry );
484+ if (nelen > 0 && ns_entry [nelen - SKIP_ONE ] == '/' ) {
485+ ns_entry [nelen - SKIP_ONE ] = '\0' ;
486+ }
487+ pkg_entries_push (entries , strdup (ns_prefix ), strdup (ns_entry ));
480488 }
481- pkg_entries_push (entries , strdup (ns_prefix ), strdup (ns_entry ));
482489 }
483490}
484491
@@ -1003,6 +1010,17 @@ static bool pkgmap_is_psr4_prefix(const char *pkg_name) {
10031010 return n > 0 && pkg_name [n - SKIP_ONE ] == '\\' ;
10041011}
10051012
1013+ /* PSR-4 values own an ordered directory list; other package values remain QNs. */
1014+ typedef struct psr4_dir {
1015+ struct psr4_dir * next ;
1016+ char path [];
1017+ } psr4_dir_t ;
1018+
1019+ static const char * pkgmap_first_path (CBMHashTable * map , const char * key ) {
1020+ void * value = cbm_ht_get (map , key );
1021+ return value && pkgmap_is_psr4_prefix (key ) ? ((psr4_dir_t * )value )-> path : value ;
1022+ }
1023+
10061024CBMHashTable * cbm_pkgmap_build (cbm_pkg_entries_t * worker_entries , int worker_count ,
10071025 const char * project_name ) {
10081026 /* Count total entries */
@@ -1020,28 +1038,43 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou
10201038 for (int w = 0 ; w < worker_count ; w ++ ) {
10211039 cbm_pkg_entries_t * we = & worker_entries [w ];
10221040 for (int i = 0 ; i < we -> count ; i ++ ) {
1023- /* Convert entry_rel to QN: project.dir.parts. A PSR-4 namespace
1024- * prefix (key ends in '\\') keeps its repo-relative DIRECTORY
1025- * instead: its readers map `Prefix\Sub\Class` onto a file path
1026- * (<dir>/Sub/Class.php), which a dotted QN cannot express (#1186). */
1027- const char * entry_rel = we -> items [i ].entry_rel ;
1028- char * qn = pkgmap_is_psr4_prefix (we -> items [i ].pkg_name )
1029- ? cbm_strndup (entry_rel , strlen (entry_rel ))
1030- : cbm_pipeline_fqn_module (project_name , entry_rel );
1031- if (!qn ) {
1041+ const bool psr4 = pkgmap_is_psr4_prefix (we -> items [i ].pkg_name );
1042+ psr4_dir_t * dirs = psr4 ? cbm_ht_get (map , we -> items [i ].pkg_name ) : NULL ;
1043+ /* Other package mappings retain their existing first-wins rule. */
1044+ if (!psr4 && cbm_ht_has (map , we -> items [i ].pkg_name )) {
10321045 continue ;
10331046 }
1034-
1035- /* Check for duplicate — first wins */
1036- if (cbm_ht_has (map , we -> items [i ].pkg_name )) {
1037- free (qn );
1047+ char * key = dirs ? NULL : strdup (we -> items [i ].pkg_name );
1048+ if (!dirs && !key ) {
10381049 continue ;
10391050 }
1040-
1041- /* Transfer ownership: key = strdup'd pkg_name, value = qn */
1042- char * key = strdup (we -> items [i ].pkg_name );
1043- cbm_ht_set (map , key , qn );
1044- merged ++ ;
1051+ const char * entry_rel = we -> items [i ].entry_rel ;
1052+ void * value = NULL ;
1053+ if (psr4 ) {
1054+ size_t len = strlen (entry_rel );
1055+ psr4_dir_t * node = cbm_alloc (CBM_MEM_CLASS_SEMANTIC , sizeof (* node ) + len + 1 );
1056+ if (node ) {
1057+ node -> next = NULL ;
1058+ memcpy (node -> path , entry_rel , len + 1 );
1059+ }
1060+ value = node ;
1061+ } else {
1062+ value = cbm_pipeline_fqn_module (project_name , entry_rel );
1063+ }
1064+ if (!value ) {
1065+ free (key );
1066+ continue ;
1067+ }
1068+ if (dirs ) {
1069+ /* ponytail: linear append for small lists; cache a tail if large lists matter. */
1070+ while (dirs -> next ) {
1071+ dirs = dirs -> next ;
1072+ }
1073+ dirs -> next = value ;
1074+ } else {
1075+ cbm_ht_set (map , key , value );
1076+ merged ++ ;
1077+ }
10451078 }
10461079 }
10471080
@@ -1296,8 +1329,17 @@ CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_i
12961329
12971330static void pkgmap_free_entry (const char * key , void * value , void * userdata ) {
12981331 (void )userdata ;
1332+ if (pkgmap_is_psr4_prefix (key )) {
1333+ psr4_dir_t * dir = value ;
1334+ while (dir ) {
1335+ psr4_dir_t * next = dir -> next ;
1336+ cbm_free (CBM_MEM_CLASS_SEMANTIC , dir );
1337+ dir = next ;
1338+ }
1339+ } else {
1340+ free (value );
1341+ }
12991342 free ((void * )key );
1300- free (value );
13011343}
13021344
13031345void cbm_pkgmap_free (CBMHashTable * pkgmap ) {
@@ -1322,7 +1364,7 @@ static char *resolve_slash_prefix(CBMHashTable *map, const char *module_path) {
13221364 continue ;
13231365 }
13241366 * slash = '\0' ;
1325- const char * base_qn = ( const char * ) cbm_ht_get (map , buf );
1367+ const char * base_qn = pkgmap_first_path (map , buf );
13261368 if (!base_qn ) {
13271369 continue ;
13281370 }
@@ -1355,7 +1397,7 @@ static char *resolve_dot_prefix(CBMHashTable *map, const char *module_path,
13551397 continue ;
13561398 }
13571399 * dot = '\0' ;
1358- const char * base_qn = ( const char * ) cbm_ht_get (map , buf );
1400+ const char * base_qn = pkgmap_first_path (map , buf );
13591401 if (!base_qn ) {
13601402 continue ;
13611403 }
@@ -1377,8 +1419,8 @@ static char *resolve_dot_prefix(CBMHashTable *map, const char *module_path,
13771419}
13781420
13791421/* Try backslash-based prefix matching (PHP PSR-4: App\\Controllers\\Foo).
1380- * The map value of a PSR-4 prefix is its repo-relative directory (see
1381- * cbm_pkgmap_build) . Returns heap QN or NULL. */
1422+ * Generic module resolution uses the first mapped directory; class imports
1423+ * search every directory through resolve_php_psr4_class . Returns heap QN or NULL. */
13821424static char * resolve_backslash_prefix (CBMHashTable * map , const char * module_path ,
13831425 const char * project_name ) {
13841426 char * buf = strdup (module_path );
@@ -1392,7 +1434,7 @@ static char *resolve_backslash_prefix(CBMHashTable *map, const char *module_path
13921434 * bs = '\0' ;
13931435 char prefix [PKGMAP_PATH_BUF ];
13941436 snprintf (prefix , sizeof (prefix ), "%s\\" , buf );
1395- const char * base_dir = ( const char * ) cbm_ht_get (map , prefix );
1437+ const char * base_dir = pkgmap_first_path (map , prefix );
13961438 if (!base_dir ) {
13971439 continue ;
13981440 }
@@ -1452,7 +1494,7 @@ char *cbm_pipeline_resolve_module(const cbm_pipeline_ctx_t *ctx, const char *sou
14521494 }
14531495
14541496 /* 3. Exact lookup */
1455- const char * mapped_qn = ( const char * ) cbm_ht_get (pkgmap , module_path );
1497+ const char * mapped_qn = pkgmap_first_path (pkgmap , module_path );
14561498 if (mapped_qn ) {
14571499 return strdup (mapped_qn );
14581500 }
@@ -2111,26 +2153,29 @@ static psr4_outcome_t resolve_php_psr4_class(const cbm_pipeline_ctx_t *ctx,
21112153 }
21122154 memcpy (key , name , cut );
21132155 key [cut ] = '\0' ;
2114- const char * dir = ( const char * ) cbm_ht_get (pkgmap , key );
2115- if (!dir ) {
2156+ const psr4_dir_t * dirs = cbm_ht_get (pkgmap , key );
2157+ if (!dirs ) {
21162158 continue ;
21172159 }
21182160 covered = true;
2119- char rel [PKGMAP_PATH_BUF ];
2120- int w = snprintf (rel , sizeof (rel ), "%s%s%s.php" , dir , dir [0 ] ? "/" : "" , name + cut );
2121- if (w <= 0 || (size_t )w >= sizeof (rel )) {
2122- continue ;
2123- }
2124- for (char * c = rel ; * c ; c ++ ) {
2125- if (* c == '\\' ) {
2126- * c = '/' ;
2161+ for (const psr4_dir_t * dir = dirs ; dir ; dir = dir -> next ) {
2162+ char rel [PKGMAP_PATH_BUF ];
2163+ int w = snprintf (rel , sizeof (rel ), "%s%s%s.php" , dir -> path , dir -> path [0 ] ? "/" : "" ,
2164+ name + cut );
2165+ if (w <= 0 || (size_t )w >= sizeof (rel )) {
2166+ continue ;
2167+ }
2168+ for (char * c = rel ; * c ; c ++ ) {
2169+ if (* c == '\\' ) {
2170+ * c = '/' ;
2171+ }
2172+ }
2173+ const cbm_gbuf_node_t * file = psr4_file_node (ctx , rel );
2174+ if (file && (!source_file_qn || !file -> qualified_name ||
2175+ strcmp (file -> qualified_name , source_file_qn ) != 0 )) {
2176+ * out = file ;
2177+ return PSR4_RESOLVED ;
21272178 }
2128- }
2129- const cbm_gbuf_node_t * file = psr4_file_node (ctx , rel );
2130- if (file && (!source_file_qn || !file -> qualified_name ||
2131- strcmp (file -> qualified_name , source_file_qn ) != 0 )) {
2132- * out = file ;
2133- return PSR4_RESOLVED ;
21342179 }
21352180 }
21362181 return covered ? PSR4_UNRESOLVED : PSR4_NOT_APPLICABLE ;
0 commit comments