mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-05 08:23:53 +08:00
fix(routes): resolve PSR-4 handler classes through the class-file resolver
Why: since 830f0ccf (#2509) the package map stores a composer.json PSR-4
prefix ("Acme\Blog\") as its repo-relative directory ("packages/blog/src"),
not as a dotted QN root. The route-handler resolver still formatted that
value as a QN root and built "packages/blog/src.Http.PostsController.
PostsController.index", which names no node. The PSR-4 step therefore
resolved nothing. Handlers whose folders mirror their namespace
(App\Http\Controllers -> app/Http/Controllers) still got their edge through
the namespace/folder fallback, but a package class under a root whose folder
does not mirror its namespace got no HANDLES edge. That is the case of
packages/blog/src in the test fixture and of krayin's
Webkul\Admin\ -> packages/Webkul/Admin/src.
Change: resolve the handler class the way the import pass resolves
`use Ns\Class;` for PHP.
- pass_pkgmap.c: psr4_file_node and resolve_php_psr4_class take the graph
buffer instead of the pipeline ctx, since they only read ctx->gbuf. The
new cbm_pipeline_psr4_member_qn runs resolve_php_psr4_class for the class
FQN, takes the Class/Interface/Trait/Enum node of that name defined in the
resolved file, and returns the QN of <class QN>.<member> when that node
exists.
- registry.c: psr4_member_qn delegates to it and no longer builds a QN from
the package-map value. cbm_registry_resolve_handler takes the graph buffer
in place of the package map. The namespace/folder fallback is unchanged.
- pass_calls.c and pass_parallel.c pass the graph buffer they already use
for the handler lookup: ctx->gbuf, and main_gbuf, which is read-only
during the resolve phase.
Proof:
- handles_laravel_class_handlers_issue1146 and its _parallel twin
(edge_types_probe): RED 3/3 before the change ("missing
PostsController.index -> /blog", expected=4 actual=3; suite 75 passed,
2 failed). GREEN 3/3 after (77 passed). RED again with the production
change reverted. GREEN again once it was restored.
- Real input: no Laravel app with a non-mirroring PSR-4 root is available
locally. koel/koel v9.11.1 (App\ -> app/) was indexed with the binary
before and after, each under a private runtime and cache. The results
are identical: 16,564 nodes, 60,177 edges, 15 Routes and 1 HANDLES
(/demo/new-session -> NewSessionController.__invoke), with the same
handler rows.
- Suites: edge_types_probe 77, edge_imports 68, pipeline 317 and
route_canon 11 all pass (473). make lint-ci passes.
Refs #1146
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
+14
-35
@@ -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 <Class>.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};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user