mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
fix(pipeline): resolve PSR-4 imports to exact class files
Preserve import kinds and resolve class imports through Composer prefixes to the existing target class file, including autoload-dev mappings. Refs #1186 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -304,10 +304,20 @@ typedef struct {
|
||||
// guard's unique-name exemption. Default false.
|
||||
} CBMCall;
|
||||
|
||||
// What an import statement names, when the syntax says so. Only PHP's
|
||||
// `use function` / `use const` set a non-default kind today: they name a
|
||||
// namespace MEMBER, so they must never be mapped to a class file (#1186).
|
||||
typedef enum {
|
||||
CBM_IMPORT_KIND_DEFAULT = 0, // module / class / namespace (language default)
|
||||
CBM_IMPORT_KIND_FUNCTION, // PHP `use function A\b`
|
||||
CBM_IMPORT_KIND_CONST, // PHP `use const A\B`
|
||||
} CBMImportKind;
|
||||
|
||||
typedef struct {
|
||||
const char *local_name; // local alias or name
|
||||
const char *module_path; // resolved module path / QN
|
||||
bool is_default; // ES default import (`import X from "Y"`), JS/TS only (#1916)
|
||||
CBMImportKind kind; // CBM_IMPORT_KIND_DEFAULT unless the syntax names a member kind
|
||||
} CBMImport;
|
||||
|
||||
typedef enum {
|
||||
|
||||
@@ -1205,7 +1205,29 @@ static void parse_wolfram_imports(CBMExtractCtx *ctx) {
|
||||
// require/include forms remain `expression_statement`s and are still handled by
|
||||
// the text fallback. Take the first qualified_name/name descendant of each
|
||||
// clause as the module path.
|
||||
static void emit_php_use_clause(CBMExtractCtx *ctx, TSNode clause, const char *group_prefix) {
|
||||
// `use function` / `use const` carry the keyword as an anonymous direct child
|
||||
// of the declaration (flat form) or of the clause (grouped form
|
||||
// `use A\{function f, const C, K}`). DEFAULT when neither is present.
|
||||
static CBMImportKind php_use_kind(TSNode node) {
|
||||
uint32_t n = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
TSNode c = ts_node_child(node, i);
|
||||
if (ts_node_is_named(c)) {
|
||||
continue;
|
||||
}
|
||||
const char *k = ts_node_type(c);
|
||||
if (strcmp(k, "function") == 0) {
|
||||
return CBM_IMPORT_KIND_FUNCTION;
|
||||
}
|
||||
if (strcmp(k, "const") == 0) {
|
||||
return CBM_IMPORT_KIND_CONST;
|
||||
}
|
||||
}
|
||||
return CBM_IMPORT_KIND_DEFAULT;
|
||||
}
|
||||
|
||||
static void emit_php_use_clause(CBMExtractCtx *ctx, TSNode clause, const char *group_prefix,
|
||||
CBMImportKind decl_kind) {
|
||||
CBMArena *a = ctx->arena;
|
||||
// The path node is the qualified_name / namespace_name / name child.
|
||||
TSNode path_node = clause;
|
||||
@@ -1230,12 +1252,14 @@ static void emit_php_use_clause(CBMExtractCtx *ctx, TSNode clause, const char *g
|
||||
TSNode alias = ts_node_child_by_field_name(clause, TS_FIELD("alias"));
|
||||
const char *local =
|
||||
!ts_node_is_null(alias) ? cbm_node_text(a, alias, ctx->source) : path_last(a, path);
|
||||
CBMImport imp = {.local_name = local, .module_path = path};
|
||||
CBMImportKind kind = decl_kind != CBM_IMPORT_KIND_DEFAULT ? decl_kind : php_use_kind(clause);
|
||||
CBMImport imp = {.local_name = local, .module_path = path, .kind = kind};
|
||||
cbm_imports_push(&ctx->result->imports, a, imp);
|
||||
}
|
||||
|
||||
static void emit_php_use_decl(CBMExtractCtx *ctx, TSNode decl) {
|
||||
CBMArena *a = ctx->arena;
|
||||
const CBMImportKind decl_kind = php_use_kind(decl);
|
||||
// Grouped form: namespace_use_group with a leading prefix qualified_name.
|
||||
TSNode group = decl;
|
||||
if (find_first_descendant_of(decl, "namespace_use_group", &group)) {
|
||||
@@ -1257,7 +1281,7 @@ static void emit_php_use_decl(CBMExtractCtx *ctx, TSNode decl) {
|
||||
const char *ck = ts_node_type(clause);
|
||||
if (strcmp(ck, "namespace_use_group_clause") == 0 ||
|
||||
strcmp(ck, "namespace_use_clause") == 0) {
|
||||
emit_php_use_clause(ctx, clause, prefix);
|
||||
emit_php_use_clause(ctx, clause, prefix, decl_kind);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -1268,13 +1292,13 @@ static void emit_php_use_decl(CBMExtractCtx *ctx, TSNode decl) {
|
||||
for (uint32_t i = 0; i < dc; i++) {
|
||||
TSNode clause = ts_node_named_child(decl, i);
|
||||
if (strcmp(ts_node_type(clause), "namespace_use_clause") == 0) {
|
||||
emit_php_use_clause(ctx, clause, NULL);
|
||||
emit_php_use_clause(ctx, clause, NULL, decl_kind);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (!any) {
|
||||
// Some grammar versions inline the path directly under the declaration.
|
||||
emit_php_use_clause(ctx, decl, NULL);
|
||||
emit_php_use_clause(ctx, decl, NULL, decl_kind);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+129
-8
@@ -440,9 +440,12 @@ static void parse_pyproject_toml(const char *source, int source_len, const char
|
||||
}
|
||||
}
|
||||
|
||||
/* Extract PSR-4 autoload entries from composer.json root. */
|
||||
static void extract_psr4(yyjson_val *root, const char *dir, cbm_pkg_entries_t *entries) {
|
||||
yyjson_val *autoload = yyjson_obj_get(root, "autoload");
|
||||
/* Extract PSR-4 entries from one composer.json autoload section ("autoload"
|
||||
* or "autoload-dev": composer registers both, so test namespaces such as
|
||||
* Tests\ map to their directory as well). */
|
||||
static void extract_psr4(yyjson_val *root, const char *section, const char *dir,
|
||||
cbm_pkg_entries_t *entries) {
|
||||
yyjson_val *autoload = yyjson_obj_get(root, section);
|
||||
if (!yyjson_is_obj(autoload)) {
|
||||
return;
|
||||
}
|
||||
@@ -459,6 +462,12 @@ static void extract_psr4(yyjson_val *root, const char *dir, cbm_pkg_entries_t *e
|
||||
}
|
||||
const char *ns_prefix = yyjson_get_str(key);
|
||||
const char *ns_dir = yyjson_get_str(val);
|
||||
while (ns_dir[0] == '.' && ns_dir[SKIP_ONE] == '/') {
|
||||
ns_dir += PAIR_LEN; /* "./src/" names src/ */
|
||||
}
|
||||
if (strcmp(ns_dir, ".") == 0) {
|
||||
ns_dir = ""; /* "." names the manifest's own directory */
|
||||
}
|
||||
char ns_entry[PKGMAP_PATH_BUF];
|
||||
if (dir[0]) {
|
||||
snprintf(ns_entry, sizeof(ns_entry), "%s/%s", dir, ns_dir);
|
||||
@@ -497,7 +506,8 @@ static void parse_composer_json(const char *source, int source_len, const char *
|
||||
}
|
||||
}
|
||||
|
||||
extract_psr4(root, dir, entries);
|
||||
extract_psr4(root, "autoload", dir, entries);
|
||||
extract_psr4(root, "autoload-dev", dir, entries);
|
||||
|
||||
free(dir);
|
||||
yyjson_doc_free(doc);
|
||||
@@ -987,6 +997,12 @@ bool cbm_pkgmap_try_parse(const char *basename, const char *rel_path, const char
|
||||
|
||||
/* ── Merge: per-worker entries → hash table ────────────────────── */
|
||||
|
||||
/* A composer.json `autoload.psr-4` key: a namespace prefix ending in '\\'. */
|
||||
static bool pkgmap_is_psr4_prefix(const char *pkg_name) {
|
||||
size_t n = pkg_name ? strlen(pkg_name) : 0;
|
||||
return n > 0 && pkg_name[n - SKIP_ONE] == '\\';
|
||||
}
|
||||
|
||||
CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_count,
|
||||
const char *project_name) {
|
||||
/* Count total entries */
|
||||
@@ -1004,8 +1020,14 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou
|
||||
for (int w = 0; w < worker_count; w++) {
|
||||
cbm_pkg_entries_t *we = &worker_entries[w];
|
||||
for (int i = 0; i < we->count; i++) {
|
||||
/* Convert entry_rel to QN: project.dir.parts */
|
||||
char *qn = cbm_pipeline_fqn_module(project_name, we->items[i].entry_rel);
|
||||
/* Convert entry_rel to QN: project.dir.parts. A PSR-4 namespace
|
||||
* prefix (key ends in '\\') keeps its repo-relative DIRECTORY
|
||||
* instead: its readers map `Prefix\Sub\Class` onto a file path
|
||||
* (<dir>/Sub/Class.php), which a dotted QN cannot express (#1186). */
|
||||
const char *entry_rel = we->items[i].entry_rel;
|
||||
char *qn = pkgmap_is_psr4_prefix(we->items[i].pkg_name)
|
||||
? cbm_strndup(entry_rel, strlen(entry_rel))
|
||||
: cbm_pipeline_fqn_module(project_name, entry_rel);
|
||||
if (!qn) {
|
||||
continue;
|
||||
}
|
||||
@@ -1355,7 +1377,8 @@ static char *resolve_dot_prefix(CBMHashTable *map, const char *module_path,
|
||||
}
|
||||
|
||||
/* Try backslash-based prefix matching (PHP PSR-4: App\\Controllers\\Foo).
|
||||
* Returns heap QN or NULL. */
|
||||
* The map value of a PSR-4 prefix is its repo-relative directory (see
|
||||
* cbm_pkgmap_build). Returns heap QN or NULL. */
|
||||
static char *resolve_backslash_prefix(CBMHashTable *map, const char *module_path,
|
||||
const char *project_name) {
|
||||
char *buf = strdup(module_path);
|
||||
@@ -1375,7 +1398,8 @@ static char *resolve_backslash_prefix(CBMHashTable *map, const char *module_path
|
||||
}
|
||||
const char *subpath = module_path + (size_t)(bs - buf) + SKIP_ONE;
|
||||
char path_result[PKGMAP_PATH_BUF];
|
||||
snprintf(path_result, sizeof(path_result), "%s/%s", base_dir, subpath);
|
||||
snprintf(path_result, sizeof(path_result), "%s%s%s", base_dir, base_dir[0] ? "/" : "",
|
||||
subpath);
|
||||
for (char *c = path_result; *c; c++) {
|
||||
if (*c == '\\') {
|
||||
*c = '/';
|
||||
@@ -2025,6 +2049,91 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx
|
||||
return found;
|
||||
}
|
||||
|
||||
/* ── PHP PSR-4 class imports (#1186) ──────────────────────────────── */
|
||||
|
||||
typedef enum {
|
||||
PSR4_NOT_APPLICABLE = 0, /* no composer psr-4 prefix covers the import */
|
||||
PSR4_RESOLVED, /* the class file exists: *out is its File node */
|
||||
PSR4_UNRESOLVED, /* a prefix covers it but no class file exists */
|
||||
} psr4_outcome_t;
|
||||
|
||||
/* 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) {
|
||||
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) {
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < hit_count; i++) {
|
||||
const cbm_gbuf_node_t *n = hits[i];
|
||||
if (n && n->label && strcmp(n->label, "File") == 0 && n->file_path &&
|
||||
strcmp(n->file_path, rel_path) == 0) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Resolve a PHP class import `use A\B\C;` the way composer's PSR-4 autoloader
|
||||
* does: for every autoload.psr-4 prefix covering the name, longest first, the
|
||||
* class lives in exactly <prefix-dir>/<rest>.php with sub-namespaces as
|
||||
* subdirectories; the first existing file wins. When a prefix covers the name
|
||||
* but no such file exists the import is UNRESOLVED: it must never fall through
|
||||
* 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) {
|
||||
*out = NULL;
|
||||
CBMHashTable *pkgmap = cbm_pipeline_get_pkgmap();
|
||||
if (!pkgmap || imp->kind != CBM_IMPORT_KIND_DEFAULT) {
|
||||
return PSR4_NOT_APPLICABLE;
|
||||
}
|
||||
const char *name = imp->module_path;
|
||||
if (name[0] == '\\') {
|
||||
name++; /* fully qualified `use \App\X;` */
|
||||
}
|
||||
char key[PKGMAP_PATH_BUF];
|
||||
size_t name_len = strlen(name);
|
||||
if (name_len == 0 || name_len >= sizeof(key) || !strchr(name, '\\')) {
|
||||
return PSR4_NOT_APPLICABLE;
|
||||
}
|
||||
bool covered = false;
|
||||
/* Right to left: each '\' ends a candidate prefix, longest first. */
|
||||
for (size_t cut = name_len - SKIP_ONE; cut > 0; cut--) {
|
||||
if (name[cut - SKIP_ONE] != '\\') {
|
||||
continue;
|
||||
}
|
||||
memcpy(key, name, cut);
|
||||
key[cut] = '\0';
|
||||
const char *dir = (const char *)cbm_ht_get(pkgmap, key);
|
||||
if (!dir) {
|
||||
continue;
|
||||
}
|
||||
covered = true;
|
||||
char rel[PKGMAP_PATH_BUF];
|
||||
int w = snprintf(rel, sizeof(rel), "%s%s%s.php", dir, dir[0] ? "/" : "", name + cut);
|
||||
if (w <= 0 || (size_t)w >= sizeof(rel)) {
|
||||
continue;
|
||||
}
|
||||
for (char *c = rel; *c; c++) {
|
||||
if (*c == '\\') {
|
||||
*c = '/';
|
||||
}
|
||||
}
|
||||
const cbm_gbuf_node_t *file = psr4_file_node(ctx, rel);
|
||||
if (file && (!source_file_qn || !file->qualified_name ||
|
||||
strcmp(file->qualified_name, source_file_qn) != 0)) {
|
||||
*out = file;
|
||||
return PSR4_RESOLVED;
|
||||
}
|
||||
}
|
||||
return covered ? PSR4_UNRESOLVED : PSR4_NOT_APPLICABLE;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -2042,6 +2151,18 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
|
||||
return header_target;
|
||||
}
|
||||
|
||||
/* 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)) {
|
||||
case PSR4_RESOLVED:
|
||||
return psr4_target;
|
||||
case PSR4_UNRESOLVED:
|
||||
return NULL;
|
||||
case PSR4_NOT_APPLICABLE:
|
||||
break;
|
||||
}
|
||||
|
||||
/* Strategy 1: module-path resolution → existing node (Python/TS/Go).
|
||||
* No label filter here: directory-module languages (Go/Java packages)
|
||||
* legitimately resolve straight to a Folder node -- that's the intended,
|
||||
|
||||
@@ -1243,6 +1243,194 @@ TEST(ei_php_interface_use) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* PHP PSR-4 — #1186
|
||||
*
|
||||
* With composer.json `autoload.psr-4`, `use App\Models\Agency;` names exactly
|
||||
* one file: <mapped-dir>/Models/Agency.php. The resolver instead fell through
|
||||
* to the namespace bucket and bound every class import of App\Models to the
|
||||
* FIRST file declaring that namespace (User.php), fabricating a hub. These
|
||||
* tests pin the exact target file per local name; a missing class file must
|
||||
* leave the import unresolved rather than land on a sibling.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
typedef struct {
|
||||
const char *local_name; /* IMPORTS edge local_name */
|
||||
const char *want_path; /* expected target file_path; NULL = no edge */
|
||||
} EIPhpImportExpect;
|
||||
|
||||
/* Find the IMPORTS edge of `importer` whose local_name is `local` and write
|
||||
* its target's file_path into `out` ("" when there is no such edge). */
|
||||
static void ei_import_target_path(cbm_store_t *store, const char *project, const char *importer,
|
||||
const char *local, char *out, size_t outsz) {
|
||||
out[0] = '\0';
|
||||
int64_t src_id = ei_node_id_for_file_label(store, project, importer, "File");
|
||||
cbm_edge_t *edges = NULL;
|
||||
int n = 0;
|
||||
if (src_id <= 0 ||
|
||||
cbm_store_find_edges_by_source_type(store, src_id, "IMPORTS", &edges, &n) != CBM_STORE_OK) {
|
||||
return;
|
||||
}
|
||||
char needle[256];
|
||||
snprintf(needle, sizeof(needle), "\"local_name\":\"%s\"", local);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (!edges[i].properties_json || !strstr(edges[i].properties_json, needle)) {
|
||||
continue;
|
||||
}
|
||||
cbm_node_t target;
|
||||
memset(&target, 0, sizeof(target));
|
||||
if (cbm_store_find_node_by_id(store, edges[i].target_id, &target) == CBM_STORE_OK) {
|
||||
snprintf(out, outsz, "%s", target.file_path ? target.file_path : "?");
|
||||
cbm_node_free_fields(&target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
cbm_store_free_edges(edges, n);
|
||||
}
|
||||
|
||||
/* Index `files` and check every expectation for `importer`. Returns 1 when
|
||||
* all hold; prints each mismatch so a RED names the fabricated target. */
|
||||
static int ei_php_imports_match(const EILangFile *files, int nfiles, const char *importer,
|
||||
const EIPhpImportExpect *want, int nwant) {
|
||||
EILangProj lp;
|
||||
cbm_store_t *store = ei_index_files(&lp, files, nfiles);
|
||||
if (!store) {
|
||||
ei_cleanup(&lp, store);
|
||||
return 0;
|
||||
}
|
||||
int ok = 1;
|
||||
for (int i = 0; i < nwant; i++) {
|
||||
char got[512];
|
||||
ei_import_target_path(store, lp.project, importer, want[i].local_name, got, sizeof(got));
|
||||
const char *expect = want[i].want_path ? want[i].want_path : "";
|
||||
if (strcmp(got, expect) != 0) {
|
||||
fprintf(stderr, " [IMPORTS %s] %s -> got \"%s\", want \"%s\"\n", importer,
|
||||
want[i].local_name, got, expect);
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
ei_cleanup(&lp, store);
|
||||
return ok;
|
||||
}
|
||||
|
||||
#define EI_PHP_CLASS(ns, cls) "<?php\nnamespace " ns ";\n\nclass " cls " {\n}\n"
|
||||
|
||||
/* Several classes in one namespace directory: each import binds its own file,
|
||||
* not the first file of App\Models. */
|
||||
TEST(ei_php_psr4_class_per_file_issue1186) {
|
||||
static const EILangFile f[] = {
|
||||
{"composer.json",
|
||||
"{\"name\":\"acme/app\",\"autoload\":{\"psr-4\":{\"App\\\\\":\"app/\"}}}\n"},
|
||||
{"app/Models/Agency.php", EI_PHP_CLASS("App\\Models", "Agency")},
|
||||
{"app/Models/Client.php", EI_PHP_CLASS("App\\Models", "Client")},
|
||||
{"app/Models/Property.php", EI_PHP_CLASS("App\\Models", "Property")},
|
||||
{"app/Models/User.php", EI_PHP_CLASS("App\\Models", "User")},
|
||||
{"app/Http/Controller.php", "<?php\nnamespace App\\Http;\n\n"
|
||||
"use App\\Models\\Property;\nuse App\\Models\\User;\n"
|
||||
"use App\\Models\\Client as C;\n\n"
|
||||
"class Controller {\n}\n"}};
|
||||
static const EIPhpImportExpect want[] = {
|
||||
{"Property", "app/Models/Property.php"},
|
||||
{"User", "app/Models/User.php"},
|
||||
{"C", "app/Models/Client.php"},
|
||||
};
|
||||
ASSERT_TRUE(ei_php_imports_match(f, 6, "app/Http/Controller.php", want, 3));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Sub-namespaces map to subdirectories of the PSR-4 root, at any depth. */
|
||||
TEST(ei_php_psr4_nested_subnamespace_issue1186) {
|
||||
static const EILangFile f[] = {
|
||||
{"composer.json", "{\"autoload\":{\"psr-4\":{\"App\\\\\":\"app/\"}}}\n"},
|
||||
{"app/Models/Billing/Account.php", EI_PHP_CLASS("App\\Models\\Billing", "Account")},
|
||||
{"app/Models/Billing/Invoice.php", EI_PHP_CLASS("App\\Models\\Billing", "Invoice")},
|
||||
{"app/Models/Billing/Tax/Exempt.php", EI_PHP_CLASS("App\\Models\\Billing\\Tax", "Exempt")},
|
||||
{"app/Models/Billing/Tax/Rate.php", EI_PHP_CLASS("App\\Models\\Billing\\Tax", "Rate")},
|
||||
{"app/Jobs/Bill.php", "<?php\nnamespace App\\Jobs;\n\n"
|
||||
"use App\\Models\\Billing\\Invoice;\n"
|
||||
"use App\\Models\\Billing\\Tax\\Rate;\n\n"
|
||||
"class Bill {\n}\n"}};
|
||||
static const EIPhpImportExpect want[] = {
|
||||
{"Invoice", "app/Models/Billing/Invoice.php"},
|
||||
{"Rate", "app/Models/Billing/Tax/Rate.php"},
|
||||
};
|
||||
ASSERT_TRUE(ei_php_imports_match(f, 6, "app/Jobs/Bill.php", want, 2));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Several psr-4 roots, across two composer.json files and both autoload
|
||||
* sections: the longest matching prefix wins (App\Domain\ -> src/Domain/ over
|
||||
* App\ -> app/), a package's own root maps its namespace, and autoload-dev
|
||||
* maps Tests\. The decoy app/Domain/Order.php declares the same namespace and
|
||||
* sorts first. */
|
||||
TEST(ei_php_psr4_multiple_roots_longest_prefix_issue1186) {
|
||||
static const EILangFile f[] = {
|
||||
{"composer.json", "{\"autoload\":{\"psr-4\":{\"App\\\\\":\"app/\","
|
||||
"\"App\\\\Domain\\\\\":\"src/Domain/\"}},"
|
||||
"\"autoload-dev\":{\"psr-4\":{\"Tests\\\\\":\"tests/\"}}}\n"},
|
||||
{"tests/Support/Assert.php", EI_PHP_CLASS("Tests\\Support", "Assert")},
|
||||
{"tests/Support/Factory.php", EI_PHP_CLASS("Tests\\Support", "Factory")},
|
||||
{"packages/billing/composer.json",
|
||||
"{\"name\":\"acme/billing\",\"autoload\":{\"psr-4\":{\"Billing\\\\\":\"src/\"}}}\n"},
|
||||
{"app/Domain/Order.php", EI_PHP_CLASS("App\\Domain", "Order")},
|
||||
{"src/Domain/Customer.php", EI_PHP_CLASS("App\\Domain", "Customer")},
|
||||
{"src/Domain/Order.php", EI_PHP_CLASS("App\\Domain", "Order")},
|
||||
{"packages/billing/src/Account.php", EI_PHP_CLASS("Billing", "Account")},
|
||||
{"packages/billing/src/Ledger.php", EI_PHP_CLASS("Billing", "Ledger")},
|
||||
{"app/Http/Checkout.php", "<?php\nnamespace App\\Http;\n\n"
|
||||
"use App\\Domain\\Order;\nuse Billing\\Ledger;\n"
|
||||
"use Tests\\Support\\Assert;\n\n"
|
||||
"class Checkout {\n}\n"}};
|
||||
static const EIPhpImportExpect want[] = {
|
||||
{"Order", "src/Domain/Order.php"},
|
||||
{"Ledger", "packages/billing/src/Ledger.php"},
|
||||
{"Assert", "tests/Support/Assert.php"},
|
||||
};
|
||||
ASSERT_TRUE(ei_php_imports_match(f, 10, "app/Http/Checkout.php", want, 3));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Control: `use function` / `use const` name a namespace member, not a class
|
||||
* file, so they must not go through PSR-4 class-file mapping (there is no
|
||||
* app/Helpers/format_money.php); they keep resolving to the declaring file. */
|
||||
TEST(ei_php_psr4_use_function_not_class_mapped_issue1186) {
|
||||
static const EILangFile f[] = {
|
||||
{"composer.json", "{\"autoload\":{\"psr-4\":{\"App\\\\\":\"app/\"}}}\n"},
|
||||
{"app/Helpers/money.php", "<?php\nnamespace App\\Helpers;\n\n"
|
||||
"const CURRENCY = 'EUR';\n\n"
|
||||
"function format_money($x) { return $x; }\n"},
|
||||
{"app/Http/Shop.php", "<?php\nnamespace App\\Http;\n\n"
|
||||
"use function App\\Helpers\\format_money;\n"
|
||||
"use const App\\Helpers\\CURRENCY;\n\n"
|
||||
"class Shop {\n"
|
||||
" public function show() { return format_money(CURRENCY); }\n"
|
||||
"}\n"}};
|
||||
static const EIPhpImportExpect want[] = {
|
||||
{"format_money", "app/Helpers/money.php"},
|
||||
{"CURRENCY", "app/Helpers/money.php"},
|
||||
};
|
||||
ASSERT_TRUE(ei_php_imports_match(f, 3, "app/Http/Shop.php", want, 2));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Control: a PSR-4 class whose file does not exist stays unresolved — it must
|
||||
* never fall back to the first file of the namespace directory. The present
|
||||
* sibling import still resolves. */
|
||||
TEST(ei_php_psr4_missing_class_file_unresolved_issue1186) {
|
||||
static const EILangFile f[] = {
|
||||
{"composer.json", "{\"autoload\":{\"psr-4\":{\"App\\\\\":\"app/\"}}}\n"},
|
||||
{"app/Models/User.php", EI_PHP_CLASS("App\\Models", "User")},
|
||||
{"app/Http/Guard.php", "<?php\nnamespace App\\Http;\n\n"
|
||||
"use App\\Models\\Ghost;\nuse App\\Models\\User;\n\n"
|
||||
"class Guard {\n}\n"}};
|
||||
static const EIPhpImportExpect want[] = {
|
||||
{"Ghost", NULL},
|
||||
{"User", "app/Models/User.php"},
|
||||
};
|
||||
ASSERT_TRUE(ei_php_imports_match(f, 3, "app/Http/Guard.php", want, 2));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* SUITE registration
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
@@ -1326,4 +1514,9 @@ SUITE(edge_imports) {
|
||||
RUN_TEST(ei_php_const_use);
|
||||
RUN_TEST(ei_php_multiple_use_statements);
|
||||
RUN_TEST(ei_php_interface_use);
|
||||
RUN_TEST(ei_php_psr4_class_per_file_issue1186);
|
||||
RUN_TEST(ei_php_psr4_nested_subnamespace_issue1186);
|
||||
RUN_TEST(ei_php_psr4_multiple_roots_longest_prefix_issue1186);
|
||||
RUN_TEST(ei_php_psr4_use_function_not_class_mapped_issue1186);
|
||||
RUN_TEST(ei_php_psr4_missing_class_file_unresolved_issue1186);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user