mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-21 05:59:35 +08:00
Merge pull request #2705 from DeusData/fix/php-global-use-known-absent
fix(php): prove an unaliased global class use absent from the project
This commit is contained in:
@@ -2833,15 +2833,53 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
|
||||
static const char php_import_complete[] = "{\"php_imports_complete\":1,";
|
||||
|
||||
/* A resolver NULL can also mean unsupported input or a failed lookup. Only
|
||||
* the bounded namespace-only path admits an allocation-free absence proof.
|
||||
* A covered-but-missing PSR-4 file, global use, or unavailable namespace map
|
||||
* leaves the whole file unknown; already resolved IMPORTS are still written. */
|
||||
static bool php_import_known_absent(const char *rel, const CBMImport *imp,
|
||||
* the bounded namespace-only path and an unaliased global class `use` with no
|
||||
* same-named PHP type anywhere admit an absence proof. A covered-but-missing
|
||||
* PSR-4 file, any other global use, or an unavailable namespace map leaves the
|
||||
* whole file unknown; already resolved IMPORTS are still written. */
|
||||
/* A global-namespace class `use Exception;` names a project type only when a
|
||||
* PHP file declares a class-like symbol of that name. With no such node at all
|
||||
* (namespaced ones included, so the proof stays conservative), the import is
|
||||
* absent from the project: the PHP builtin or a vendor class. Without this a
|
||||
* single `use Exception;` left the whole file's evidence unknown, and its
|
||||
* project imports reached the PHP LSP as File QNs again. */
|
||||
static bool php_global_use_known_absent(const cbm_gbuf_t *gbuf, const CBMImport *imp) {
|
||||
if (!gbuf || imp->kind != CBM_IMPORT_KIND_DEFAULT) {
|
||||
return false;
|
||||
}
|
||||
const char *name = imp->module_path[0] == '\\' ? imp->module_path + 1 : imp->module_path;
|
||||
if (!name[0] || strcmp(name, imp->local_name) != 0) {
|
||||
return false; /* `use X as Y;` keeps the old, unknown verdict */
|
||||
}
|
||||
const cbm_gbuf_node_t **nodes = NULL;
|
||||
int count = 0;
|
||||
if (cbm_gbuf_find_by_name(gbuf, name, &nodes, &count) != 0) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
const cbm_gbuf_node_t *n = nodes[i];
|
||||
if (!n || !n->label || !node_in_php_file(n)) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(n->label, "Class") == 0 || strcmp(n->label, "Interface") == 0 ||
|
||||
strcmp(n->label, "Trait") == 0 || strcmp(n->label, "Enum") == 0 ||
|
||||
strcmp(n->label, "File") == 0 || strcmp(n->label, "Module") == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool php_import_known_absent(const cbm_gbuf_t *gbuf, const char *rel, const CBMImport *imp,
|
||||
CBMHashTable *namespace_map) {
|
||||
if (!namespace_map || !imp->module_path || !imp->local_name || !imp->local_name[0] ||
|
||||
(imp->kind != CBM_IMPORT_KIND_DEFAULT && imp->kind != CBM_IMPORT_KIND_FUNCTION &&
|
||||
imp->kind != CBM_IMPORT_KIND_CONST) ||
|
||||
php_use_shape(rel, imp) != PHP_USE_NAMESPACED)
|
||||
imp->kind != CBM_IMPORT_KIND_CONST))
|
||||
return false;
|
||||
php_use_shape_t shape = php_use_shape(rel, imp);
|
||||
if (shape == PHP_USE_GLOBAL)
|
||||
return php_global_use_known_absent(gbuf, imp);
|
||||
if (shape != PHP_USE_NAMESPACED)
|
||||
return false;
|
||||
const char *name = imp->module_path[0] == '\\' ? imp->module_path + 1 : imp->module_path;
|
||||
char key[CBM_SZ_1K];
|
||||
@@ -3072,7 +3110,7 @@ int cbm_pipeline_php_create_import_edges(cbm_pipeline_ctx_t *ctx, const CBMFileR
|
||||
: NULL;
|
||||
if (targets[i] && targets[i]->id == file->id)
|
||||
targets[i] = NULL;
|
||||
if (!targets[i] && !php_import_known_absent(rel, imp, namespace_map))
|
||||
if (!targets[i] && !php_import_known_absent(ctx->gbuf, rel, imp, namespace_map))
|
||||
complete = false;
|
||||
yyjson_mut_val *entry =
|
||||
php_binding_json(doc, imp, targets[i] ? targets[i]->qualified_name : NULL);
|
||||
|
||||
@@ -5233,6 +5233,173 @@ TEST(parallel_php_vendor_types_never_bind_by_name_issue1186) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── PHP LSP types receivers of imported project classes (#1186) ─── */
|
||||
|
||||
/* Foo.php calls methods on receivers typed with classes imported from OTHER
|
||||
* project files: a promoted property (`$this->svc->go()`) and an aliased
|
||||
* import (`R $r` -> `$r->find()`). The import map handed to the PHP LSP held
|
||||
* the IMPORTS edge target -- the imported file's File node QN
|
||||
* (`...Svc.php.__file__`) -- as the class, so no receiver ever typed and the
|
||||
* calls fell to name-only strategies (the #1186 "useful calls at 0.38"). The
|
||||
* decoy App\Other\Svc shares the short name and the method, so a name-only
|
||||
* match cannot tell the two apart. Controls: the vendor-typed receiver
|
||||
* (`Client $c`) must stay unresolved, and the same-file calls keep their LSP
|
||||
* strategies. */
|
||||
static const struct {
|
||||
const char *rel;
|
||||
const char *src;
|
||||
} k_php_xfile_files[] = {
|
||||
{"lib/Services/Svc.php", "<?php\nnamespace App\\Services;\n\nclass Svc {\n"
|
||||
" public function go() { return 1; }\n}\n"},
|
||||
{"lib/Other/Svc.php", "<?php\nnamespace App\\Other;\n\nclass Svc {\n"
|
||||
" public function go() { return 2; }\n}\n"},
|
||||
{"lib/Repo/Repo.php", "<?php\nnamespace App\\Repo;\n\nclass Repo {\n"
|
||||
" public function find() { return 3; }\n}\n"},
|
||||
{"lib/Http/Foo.php",
|
||||
"<?php\nnamespace App\\Http;\n\n"
|
||||
"use App\\Services\\Svc;\nuse App\\Repo\\Repo as R;\nuse Vendor\\Pkg\\Client;\n\n"
|
||||
"class Foo {\n"
|
||||
" public function __construct(private Svc $svc) {}\n"
|
||||
" public function run(R $r, Client $c) {\n"
|
||||
" $this->svc->go();\n"
|
||||
" $r->find();\n"
|
||||
" $c->go();\n"
|
||||
" $this->local();\n"
|
||||
" (new Local())->ping();\n"
|
||||
" }\n"
|
||||
" private function local() { return 4; }\n}\n\n"
|
||||
"class Local {\n"
|
||||
" public function ping() { return 5; }\n}\n"},
|
||||
};
|
||||
enum { PHP_XFILE_FILE_COUNT = (int)(sizeof(k_php_xfile_files) / sizeof(k_php_xfile_files[0])) };
|
||||
|
||||
typedef struct {
|
||||
const cbm_gbuf_t *gbuf;
|
||||
const char *src_rel;
|
||||
const char *tgt_rel;
|
||||
const char *tgt_suffix;
|
||||
const char *strategy_prefix;
|
||||
int total;
|
||||
int with_strategy;
|
||||
} php_xfile_edge_count_t;
|
||||
|
||||
static void php_xfile_count_edge(const cbm_gbuf_edge_t *edge, void *ud) {
|
||||
php_xfile_edge_count_t *c = ud;
|
||||
if (!edge->type || strcmp(edge->type, "CALLS") != 0) {
|
||||
return;
|
||||
}
|
||||
const cbm_gbuf_node_t *s = cbm_gbuf_find_by_id(c->gbuf, edge->source_id);
|
||||
const cbm_gbuf_node_t *t = cbm_gbuf_find_by_id(c->gbuf, edge->target_id);
|
||||
if (!s || !t || !s->file_path || !t->file_path || !t->qualified_name ||
|
||||
strcmp(s->file_path, c->src_rel) != 0 || strcmp(t->file_path, c->tgt_rel) != 0) {
|
||||
return;
|
||||
}
|
||||
size_t qlen = strlen(t->qualified_name);
|
||||
size_t slen = strlen(c->tgt_suffix);
|
||||
if (qlen < slen || strcmp(t->qualified_name + qlen - slen, c->tgt_suffix) != 0) {
|
||||
return;
|
||||
}
|
||||
c->total++;
|
||||
char needle[128];
|
||||
snprintf(needle, sizeof(needle), "\"strategy\":\"%s", c->strategy_prefix);
|
||||
if (edge->properties_json && strstr(edge->properties_json, needle)) {
|
||||
c->with_strategy++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 1 when every cross-file receiver resolved through the PHP LSP, the decoy and
|
||||
* the vendor receiver drew nothing, and the same-file controls held. */
|
||||
static int php_xfile_graph_ok(const cbm_gbuf_t *gbuf, const char *driver) {
|
||||
static const char *const foo = "lib/Http/Foo.php";
|
||||
struct {
|
||||
const char *tgt;
|
||||
const char *suffix;
|
||||
int want_total;
|
||||
int want_lsp;
|
||||
} checks[] = {
|
||||
{"lib/Services/Svc.php", ".Svc.go", 1, 1}, /* $this->svc->go(), not $c->go() */
|
||||
{"lib/Other/Svc.php", ".Svc.go", 0, 0}, /* decoy: same short name + method */
|
||||
{"lib/Repo/Repo.php", ".Repo.find", 1, 1}, /* aliased import */
|
||||
{foo, ".Foo.local", 1, 1}, /* same-file control */
|
||||
{foo, ".Local.ping", 1, 1}, /* same-file control */
|
||||
};
|
||||
int ok = 1;
|
||||
for (size_t i = 0; i < sizeof(checks) / sizeof(checks[0]); i++) {
|
||||
php_xfile_edge_count_t c = {gbuf, foo, checks[i].tgt, checks[i].suffix, "php_", 0, 0};
|
||||
cbm_gbuf_foreach_edge(gbuf, php_xfile_count_edge, &c);
|
||||
if (c.total != checks[i].want_total || c.with_strategy != checks[i].want_lsp) {
|
||||
printf(" [%s] CALLS Foo.php -> %s%s: %d edge(s), %d via php_*; want %d, %d\n", driver,
|
||||
checks[i].tgt, checks[i].suffix, c.total, c.with_strategy, checks[i].want_total,
|
||||
checks[i].want_lsp);
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Run the fixture on both venues. extra_use (nullable) is one more `use` line
|
||||
* added to Foo.php. Returns 1 when both graphs pass, 0 when not, -1 on setup
|
||||
* failure. */
|
||||
static int php_xfile_run(const char *extra_use) {
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_phpxfile_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir)) {
|
||||
return -1;
|
||||
}
|
||||
char paths[PHP_XFILE_FILE_COUNT][512];
|
||||
cbm_file_info_t files[PHP_XFILE_FILE_COUNT];
|
||||
memset(files, 0, sizeof(files));
|
||||
static const char foo_use[] = "use Vendor\\Pkg\\Client;\n";
|
||||
for (int i = 0; i < PHP_XFILE_FILE_COUNT; i++) {
|
||||
snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, k_php_xfile_files[i].rel);
|
||||
char dir[512];
|
||||
snprintf(dir, sizeof(dir), "%s", paths[i]);
|
||||
*strrchr(dir, '/') = '\0';
|
||||
cbm_mkdir_p(dir, 0755);
|
||||
char src[2048];
|
||||
const char *s = k_php_xfile_files[i].src;
|
||||
const char *at = extra_use ? strstr(s, foo_use) : NULL;
|
||||
if (at) {
|
||||
size_t head = (size_t)(at - s) + sizeof(foo_use) - 1;
|
||||
snprintf(src, sizeof(src), "%.*s%s%s", (int)head, s, extra_use, s + head);
|
||||
s = src;
|
||||
}
|
||||
if (th_write_file(paths[i], s) != 0) {
|
||||
th_rmtree(tmpdir);
|
||||
return -1;
|
||||
}
|
||||
files[i].path = paths[i];
|
||||
files[i].rel_path = (char *)k_php_xfile_files[i].rel;
|
||||
files[i].language = CBM_LANG_PHP;
|
||||
}
|
||||
|
||||
cbm_gbuf_t *seq = run_sequential_with_lsp_cross_and_mutator(
|
||||
"cbm_par_phpxfile", tmpdir, files, PHP_XFILE_FILE_COUNT, NULL, NULL, true);
|
||||
cbm_gbuf_t *par = run_parallel_with_extract_opts_and_mutator(
|
||||
"cbm_par_phpxfile", tmpdir, files, PHP_XFILE_FILE_COUNT, 2, NULL, NULL, NULL, true);
|
||||
int seq_ok = seq ? php_xfile_graph_ok(seq, "sequential") : 0;
|
||||
int par_ok = par ? php_xfile_graph_ok(par, "parallel") : 0;
|
||||
cbm_gbuf_free(seq);
|
||||
cbm_gbuf_free(par);
|
||||
th_rmtree(tmpdir);
|
||||
return seq_ok && par_ok;
|
||||
}
|
||||
|
||||
TEST(parallel_php_lsp_types_imported_project_receivers_issue1186) {
|
||||
ASSERT_EQ(php_xfile_run(NULL), 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The same file with one more import that no file in the project declares,
|
||||
* the global class `use Exception;` written in most namespaced PHP files.
|
||||
* The typed import evidence leaves such a file "unknown" as a whole (#2520),
|
||||
* so the class-valued import mapping must not depend on that evidence. */
|
||||
TEST(parallel_php_lsp_types_imported_project_receivers_unknown_bindings_issue1186) {
|
||||
ASSERT_EQ(php_xfile_run("use Exception;\n"), 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
|
||||
/* A fresh PHP external row replaces stale evidence for its exact occurrence,
|
||||
* including an older external row, without clearing a real project invocation
|
||||
* of the same method at the next occurrence. Test both old-row orders. */
|
||||
@@ -5346,6 +5513,8 @@ TEST(parallel_php_external_row_replaces_only_its_exact_occurrence) {
|
||||
SUITE(parallel) {
|
||||
RUN_TEST(parallel_php_external_row_replaces_only_its_exact_occurrence);
|
||||
RUN_TEST(parallel_php_vendor_types_never_bind_by_name_issue1186);
|
||||
RUN_TEST(parallel_php_lsp_types_imported_project_receivers_issue1186);
|
||||
RUN_TEST(parallel_php_lsp_types_imported_project_receivers_unknown_bindings_issue1186);
|
||||
RUN_TEST(usage_semantic_reference_candidate_trusts_marked_producer);
|
||||
RUN_TEST(lsp_bare_segment_skips_preprocessor_spacing);
|
||||
RUN_TEST(lsp_resolve_qualified_static_call_normalizes_colons);
|
||||
|
||||
Reference in New Issue
Block a user