diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 53f57025a..a40f8947c 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -406,7 +406,7 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca if (call->second_arg_name != NULL && call->second_arg_name[0] != '\0') { cbm_resolution_t hres = cbm_registry_resolve_handler(ctx->registry, call->second_arg_name, module_qn, imp_keys, - imp_vals, imp_count, cbm_pipeline_get_pkgmap()); + imp_vals, imp_count, ctx->gbuf); if (hres.qualified_name != NULL && hres.qualified_name[0] != '\0') { const cbm_gbuf_node_t *handler = cbm_gbuf_find_by_qn(ctx->gbuf, hres.qualified_name); if (handler != NULL) { diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index dfaea7161..7514008ce 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2295,8 +2295,8 @@ static void emit_route_registration(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *sou esc_rp); cbm_gbuf_insert_edge(gbuf, source->id, rid, "CALLS", props); if (handler_ref && handler_ref[0] != '\0') { - cbm_resolution_t hres = cbm_registry_resolve_handler(registry, handler_ref, module_qn, ik, - iv, ic, cbm_pipeline_get_pkgmap()); + cbm_resolution_t hres = + cbm_registry_resolve_handler(registry, handler_ref, module_qn, ik, iv, ic, main_gbuf); if (hres.qualified_name && hres.qualified_name[0] != '\0') { const cbm_gbuf_node_t *h = cbm_gbuf_find_by_qn(main_gbuf, hres.qualified_name); if (h) { diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 9c4ace14b..71d3d3bec 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -2060,10 +2060,10 @@ typedef enum { /* The File node at exactly `rel_path`, or NULL. Looked up through the name * index (File nodes are named by basename) and matched on the full path, so * a same-named file in another directory never qualifies. */ -static const cbm_gbuf_node_t *psr4_file_node(const cbm_pipeline_ctx_t *ctx, const char *rel_path) { +static const cbm_gbuf_node_t *psr4_file_node(const cbm_gbuf_t *gbuf, const char *rel_path) { const cbm_gbuf_node_t **hits = NULL; int hit_count = 0; - if (cbm_gbuf_find_by_name(ctx->gbuf, path_leaf(rel_path), &hits, &hit_count) != 0 || !hits) { + if (cbm_gbuf_find_by_name(gbuf, path_leaf(rel_path), &hits, &hit_count) != 0 || !hits) { return NULL; } for (int i = 0; i < hit_count; i++) { @@ -2084,9 +2084,8 @@ static const cbm_gbuf_node_t *psr4_file_node(const cbm_pipeline_ctx_t *ctx, cons * to the namespace bucket, which binds it to whichever file of the namespace * came first. `use function` / `use const` name namespace members, not class * files, so they are not applicable. */ -static psr4_outcome_t resolve_php_psr4_class(const cbm_pipeline_ctx_t *ctx, - const char *source_file_qn, const CBMImport *imp, - const cbm_gbuf_node_t **out) { +static psr4_outcome_t resolve_php_psr4_class(const cbm_gbuf_t *gbuf, const char *source_file_qn, + const CBMImport *imp, const cbm_gbuf_node_t **out) { *out = NULL; CBMHashTable *pkgmap = cbm_pipeline_get_pkgmap(); if (!pkgmap || imp->kind != CBM_IMPORT_KIND_DEFAULT) { @@ -2124,7 +2123,7 @@ static psr4_outcome_t resolve_php_psr4_class(const cbm_pipeline_ctx_t *ctx, *c = '/'; } } - const cbm_gbuf_node_t *file = psr4_file_node(ctx, rel); + const cbm_gbuf_node_t *file = psr4_file_node(gbuf, rel); if (file && (!source_file_qn || !file->qualified_name || strcmp(file->qualified_name, source_file_qn) != 0)) { *out = file; @@ -2134,6 +2133,45 @@ static psr4_outcome_t resolve_php_psr4_class(const cbm_pipeline_ctx_t *ctx, return covered ? PSR4_UNRESOLVED : PSR4_NOT_APPLICABLE; } +static bool psr4_class_label(const char *label) { + return label && (strcmp(label, "Class") == 0 || strcmp(label, "Interface") == 0 || + strcmp(label, "Trait") == 0 || strcmp(label, "Enum") == 0); +} + +const char *cbm_pipeline_psr4_member_qn(const cbm_gbuf_t *gbuf, const char *class_fqn, + const char *member) { + if (!gbuf || !class_fqn || !member || !member[0]) { + return NULL; + } + const CBMImport imp = {.module_path = class_fqn, .kind = CBM_IMPORT_KIND_DEFAULT}; + const cbm_gbuf_node_t *file = NULL; + if (resolve_php_psr4_class(gbuf, NULL, &imp, &file) != PSR4_RESOLVED || !file->file_path) { + return NULL; + } + const char *cls = strrchr(class_fqn, '\\'); + cls = cls ? cls + SKIP_ONE : class_fqn; + const cbm_gbuf_node_t **hits = NULL; + int hit_count = 0; + if (cbm_gbuf_find_by_name(gbuf, cls, &hits, &hit_count) != 0 || !hits) { + return NULL; + } + for (int i = 0; i < hit_count; i++) { + const cbm_gbuf_node_t *c = hits[i]; + if (!c || !psr4_class_label(c->label) || !c->qualified_name || !c->file_path || + strcmp(c->file_path, file->file_path) != 0) { + continue; + } + char qn[CBM_SZ_1K]; + int w = snprintf(qn, sizeof(qn), "%s.%s", c->qualified_name, member); + if (w <= 0 || (size_t)w >= sizeof(qn)) { + return NULL; + } + const cbm_gbuf_node_t *m = cbm_gbuf_find_by_qn(gbuf, qn); + return m ? m->qualified_name : NULL; + } + return NULL; +} + const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t *ctx, const char *source_rel, const char *source_file_qn, @@ -2154,7 +2192,7 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t /* PHP class imports covered by a composer psr-4 prefix name exactly one * file; when it is absent the import stays unresolved (#1186). */ const cbm_gbuf_node_t *psr4_target = NULL; - switch (resolve_php_psr4_class(ctx, source_file_qn, imp, &psr4_target)) { + switch (resolve_php_psr4_class(ctx->gbuf, source_file_qn, imp, &psr4_target)) { case PSR4_RESOLVED: return psr4_target; case PSR4_UNRESOLVED: diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 451776807..650f8a08d 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -213,14 +213,24 @@ void cbm_pipeline_set_pkgmap(CBMHashTable *map); * Laravel `[Class::class, 'method']` and invokable `Class::class` handlers * (#1146), with the class already qualified through the file's `use` imports * and namespace — is placed on a method QN only by what places the class file: - * a composer.json PSR-4 root in psr4_roots (the package map), else the one - * candidate whose directories end with the class's namespace. Never by short - * name alone, so a vendor controller that is not in the repo gets no handler. - * A reference without "::" resolves exactly as cbm_registry_resolve does. */ + * the composer.json PSR-4 class file in gbuf (cbm_pipeline_psr4_member_qn), + * else the one candidate whose directories end with the class's namespace. + * Never by short name alone, so a vendor controller that is not in the repo + * gets no handler. A reference without "::" resolves exactly as + * cbm_registry_resolve does. */ cbm_resolution_t cbm_registry_resolve_handler(const cbm_registry_t *r, const char *handler_ref, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, - const CBMHashTable *psr4_roots); + const cbm_gbuf_t *gbuf); + +/* The QN of `member` on the PHP class `class_fqn` ("Ns\\Class", NUL- + * terminated), placed the way composer's PSR-4 autoloader places the class: + * the class file the import resolver picks for `use Ns\\Class;` (#1186), the + * class that file defines, then that class's member. NULL when no PSR-4 + * prefix covers the class, its file is absent, or the class has no such + * member. The returned string is owned by gbuf. Read-only on gbuf. */ +const char *cbm_pipeline_psr4_member_qn(const cbm_gbuf_t *gbuf, const char *class_fqn, + const char *member); /* Unified module resolver: relative → pkgmap → fqn_module fallback. * Handles bare specifiers via pkgmap lookup with prefix matching. diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 4159c19f4..3e564f816 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1502,42 +1502,21 @@ static int qn_class_member_tail(const char *qn, const char *cls, size_t cls_len, return (int)(c - SKIP_ONE - qn); } -/* PSR-4: the longest composer.json autoload root "Prefix\\" that prefixes the - * FQN maps the remaining namespace onto directories below the root's folder - * and the class onto .php — the file Composer's autoloader loads. - * Returns the registered method QN, or NULL (no root prefixes the FQN, or the - * autoloaded file holds no such method). */ -static const char *psr4_member_qn(const cbm_registry_t *r, const CBMHashTable *psr4_roots, - const char *fqn, size_t fqn_len, const char *cls, size_t cls_len, +/* PSR-4: the class file Composer's autoloader loads for the FQN, the class it + * defines, and that class's member — through the PSR-4 import resolver + * (cbm_pipeline_psr4_member_qn), so a root whose folder does not mirror its + * namespace (Acme\\Blog\\ -> packages/blog/src) places the class as well. + * Returns the member QN, or NULL (no root covers the FQN, its class file is + * absent, or the class holds no such member). */ +static const char *psr4_member_qn(const cbm_gbuf_t *gbuf, const char *fqn, size_t fqn_len, const char *member) { - if (!psr4_roots || fqn_len >= CBM_SZ_512) { + char cls_fqn[CBM_SZ_512]; + if (!gbuf || fqn_len >= sizeof(cls_fqn)) { return NULL; } - char key[CBM_SZ_512]; - for (size_t cut = fqn_len; cut > 0; cut--) { - if (fqn[cut - SKIP_ONE] != '\\') { - continue; - } - memcpy(key, fqn, cut); /* "App\\" / "App\\Http\\" — keeps the trailing '\\' */ - key[cut] = '\0'; - const char *root_qn = (const char *)cbm_ht_get(psr4_roots, key); - if (!root_qn) { - continue; - } - char qn[CBM_SZ_1K]; - int n = snprintf(qn, sizeof(qn), "%s.%.*s.%.*s.%s", root_qn, (int)(fqn_len - cut), - fqn + cut, (int)cls_len, cls, member); - if (n <= 0 || (size_t)n >= sizeof(qn)) { - return NULL; - } - for (char *p = qn + strlen(root_qn); *p; p++) { - if (*p == '\\') { - *p = '.'; - } - } - return cbm_ht_get_key(r->exact, qn); - } - return NULL; + memcpy(cls_fqn, fqn, fqn_len); + cls_fqn[fqn_len] = '\0'; + return cbm_pipeline_psr4_member_qn(gbuf, cls_fqn, member); } /* Do the directory segments of qn (between the project root segment and the @@ -1600,7 +1579,7 @@ static const char *aligned_member_qn(const cbm_registry_t *r, const char *fqn, s cbm_resolution_t cbm_registry_resolve_handler(const cbm_registry_t *r, const char *handler_ref, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, - const CBMHashTable *psr4_roots) { + const cbm_gbuf_t *gbuf) { const char *sep = handler_ref ? strstr(handler_ref, "::") : NULL; if (!r || !sep) { return cbm_registry_resolve(r, handler_ref, module_qn, import_map_keys, import_map_vals, @@ -1618,7 +1597,7 @@ cbm_resolution_t cbm_registry_resolve_handler(const cbm_registry_t *r, const cha if (cls_len == 0 || !member[0]) { return empty_result(); } - const char *qn = psr4_member_qn(r, psr4_roots, handler_ref, fqn_len, cls, cls_len, member); + const char *qn = psr4_member_qn(gbuf, handler_ref, fqn_len, member); if (qn) { return (cbm_resolution_t){qn, "php_psr4", CONF_PSR4_MEMBER, REG_RESOLVED}; }