Skip to content

Commit d4980ad

Browse files
authored
Merge pull request DeusData#2705 from DeusData/fix/php-global-use-known-absent
fix(php): prove an unaliased global class use absent from the project
2 parents f26da1a + 4ae1909 commit d4980ad

2 files changed

Lines changed: 214 additions & 7 deletions

File tree

‎src/pipeline/pass_pkgmap.c‎

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2833,15 +2833,53 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
28332833
static const char php_import_complete[] = "{\"php_imports_complete\":1,";
28342834

28352835
/* A resolver NULL can also mean unsupported input or a failed lookup. Only
2836-
* the bounded namespace-only path admits an allocation-free absence proof.
2837-
* A covered-but-missing PSR-4 file, global use, or unavailable namespace map
2838-
* leaves the whole file unknown; already resolved IMPORTS are still written. */
2839-
static bool php_import_known_absent(const char *rel, const CBMImport *imp,
2836+
* the bounded namespace-only path and an unaliased global class `use` with no
2837+
* same-named PHP type anywhere admit an absence proof. A covered-but-missing
2838+
* PSR-4 file, any other global use, or an unavailable namespace map leaves the
2839+
* whole file unknown; already resolved IMPORTS are still written. */
2840+
/* A global-namespace class `use Exception;` names a project type only when a
2841+
* PHP file declares a class-like symbol of that name. With no such node at all
2842+
* (namespaced ones included, so the proof stays conservative), the import is
2843+
* absent from the project: the PHP builtin or a vendor class. Without this a
2844+
* single `use Exception;` left the whole file's evidence unknown, and its
2845+
* project imports reached the PHP LSP as File QNs again. */
2846+
static bool php_global_use_known_absent(const cbm_gbuf_t *gbuf, const CBMImport *imp) {
2847+
if (!gbuf || imp->kind != CBM_IMPORT_KIND_DEFAULT) {
2848+
return false;
2849+
}
2850+
const char *name = imp->module_path[0] == '\\' ? imp->module_path + 1 : imp->module_path;
2851+
if (!name[0] || strcmp(name, imp->local_name) != 0) {
2852+
return false; /* `use X as Y;` keeps the old, unknown verdict */
2853+
}
2854+
const cbm_gbuf_node_t **nodes = NULL;
2855+
int count = 0;
2856+
if (cbm_gbuf_find_by_name(gbuf, name, &nodes, &count) != 0) {
2857+
return false;
2858+
}
2859+
for (int i = 0; i < count; i++) {
2860+
const cbm_gbuf_node_t *n = nodes[i];
2861+
if (!n || !n->label || !node_in_php_file(n)) {
2862+
continue;
2863+
}
2864+
if (strcmp(n->label, "Class") == 0 || strcmp(n->label, "Interface") == 0 ||
2865+
strcmp(n->label, "Trait") == 0 || strcmp(n->label, "Enum") == 0 ||
2866+
strcmp(n->label, "File") == 0 || strcmp(n->label, "Module") == 0) {
2867+
return false;
2868+
}
2869+
}
2870+
return true;
2871+
}
2872+
2873+
static bool php_import_known_absent(const cbm_gbuf_t *gbuf, const char *rel, const CBMImport *imp,
28402874
CBMHashTable *namespace_map) {
28412875
if (!namespace_map || !imp->module_path || !imp->local_name || !imp->local_name[0] ||
28422876
(imp->kind != CBM_IMPORT_KIND_DEFAULT && imp->kind != CBM_IMPORT_KIND_FUNCTION &&
2843-
imp->kind != CBM_IMPORT_KIND_CONST) ||
2844-
php_use_shape(rel, imp) != PHP_USE_NAMESPACED)
2877+
imp->kind != CBM_IMPORT_KIND_CONST))
2878+
return false;
2879+
php_use_shape_t shape = php_use_shape(rel, imp);
2880+
if (shape == PHP_USE_GLOBAL)
2881+
return php_global_use_known_absent(gbuf, imp);
2882+
if (shape != PHP_USE_NAMESPACED)
28452883
return false;
28462884
const char *name = imp->module_path[0] == '\\' ? imp->module_path + 1 : imp->module_path;
28472885
char key[CBM_SZ_1K];
@@ -3072,7 +3110,7 @@ int cbm_pipeline_php_create_import_edges(cbm_pipeline_ctx_t *ctx, const CBMFileR
30723110
: NULL;
30733111
if (targets[i] && targets[i]->id == file->id)
30743112
targets[i] = NULL;
3075-
if (!targets[i] && !php_import_known_absent(rel, imp, namespace_map))
3113+
if (!targets[i] && !php_import_known_absent(ctx->gbuf, rel, imp, namespace_map))
30763114
complete = false;
30773115
yyjson_mut_val *entry =
30783116
php_binding_json(doc, imp, targets[i] ? targets[i]->qualified_name : NULL);

‎tests/test_parallel.c‎

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5233,6 +5233,173 @@ TEST(parallel_php_vendor_types_never_bind_by_name_issue1186) {
52335233
PASS();
52345234
}
52355235

5236+
/* ── PHP LSP types receivers of imported project classes (#1186) ─── */
5237+
5238+
/* Foo.php calls methods on receivers typed with classes imported from OTHER
5239+
* project files: a promoted property (`$this->svc->go()`) and an aliased
5240+
* import (`R $r` -> `$r->find()`). The import map handed to the PHP LSP held
5241+
* the IMPORTS edge target -- the imported file's File node QN
5242+
* (`...Svc.php.__file__`) -- as the class, so no receiver ever typed and the
5243+
* calls fell to name-only strategies (the #1186 "useful calls at 0.38"). The
5244+
* decoy App\Other\Svc shares the short name and the method, so a name-only
5245+
* match cannot tell the two apart. Controls: the vendor-typed receiver
5246+
* (`Client $c`) must stay unresolved, and the same-file calls keep their LSP
5247+
* strategies. */
5248+
static const struct {
5249+
const char *rel;
5250+
const char *src;
5251+
} k_php_xfile_files[] = {
5252+
{"lib/Services/Svc.php", "<?php\nnamespace App\\Services;\n\nclass Svc {\n"
5253+
" public function go() { return 1; }\n}\n"},
5254+
{"lib/Other/Svc.php", "<?php\nnamespace App\\Other;\n\nclass Svc {\n"
5255+
" public function go() { return 2; }\n}\n"},
5256+
{"lib/Repo/Repo.php", "<?php\nnamespace App\\Repo;\n\nclass Repo {\n"
5257+
" public function find() { return 3; }\n}\n"},
5258+
{"lib/Http/Foo.php",
5259+
"<?php\nnamespace App\\Http;\n\n"
5260+
"use App\\Services\\Svc;\nuse App\\Repo\\Repo as R;\nuse Vendor\\Pkg\\Client;\n\n"
5261+
"class Foo {\n"
5262+
" public function __construct(private Svc $svc) {}\n"
5263+
" public function run(R $r, Client $c) {\n"
5264+
" $this->svc->go();\n"
5265+
" $r->find();\n"
5266+
" $c->go();\n"
5267+
" $this->local();\n"
5268+
" (new Local())->ping();\n"
5269+
" }\n"
5270+
" private function local() { return 4; }\n}\n\n"
5271+
"class Local {\n"
5272+
" public function ping() { return 5; }\n}\n"},
5273+
};
5274+
enum { PHP_XFILE_FILE_COUNT = (int)(sizeof(k_php_xfile_files) / sizeof(k_php_xfile_files[0])) };
5275+
5276+
typedef struct {
5277+
const cbm_gbuf_t *gbuf;
5278+
const char *src_rel;
5279+
const char *tgt_rel;
5280+
const char *tgt_suffix;
5281+
const char *strategy_prefix;
5282+
int total;
5283+
int with_strategy;
5284+
} php_xfile_edge_count_t;
5285+
5286+
static void php_xfile_count_edge(const cbm_gbuf_edge_t *edge, void *ud) {
5287+
php_xfile_edge_count_t *c = ud;
5288+
if (!edge->type || strcmp(edge->type, "CALLS") != 0) {
5289+
return;
5290+
}
5291+
const cbm_gbuf_node_t *s = cbm_gbuf_find_by_id(c->gbuf, edge->source_id);
5292+
const cbm_gbuf_node_t *t = cbm_gbuf_find_by_id(c->gbuf, edge->target_id);
5293+
if (!s || !t || !s->file_path || !t->file_path || !t->qualified_name ||
5294+
strcmp(s->file_path, c->src_rel) != 0 || strcmp(t->file_path, c->tgt_rel) != 0) {
5295+
return;
5296+
}
5297+
size_t qlen = strlen(t->qualified_name);
5298+
size_t slen = strlen(c->tgt_suffix);
5299+
if (qlen < slen || strcmp(t->qualified_name + qlen - slen, c->tgt_suffix) != 0) {
5300+
return;
5301+
}
5302+
c->total++;
5303+
char needle[128];
5304+
snprintf(needle, sizeof(needle), "\"strategy\":\"%s", c->strategy_prefix);
5305+
if (edge->properties_json && strstr(edge->properties_json, needle)) {
5306+
c->with_strategy++;
5307+
}
5308+
}
5309+
5310+
/* 1 when every cross-file receiver resolved through the PHP LSP, the decoy and
5311+
* the vendor receiver drew nothing, and the same-file controls held. */
5312+
static int php_xfile_graph_ok(const cbm_gbuf_t *gbuf, const char *driver) {
5313+
static const char *const foo = "lib/Http/Foo.php";
5314+
struct {
5315+
const char *tgt;
5316+
const char *suffix;
5317+
int want_total;
5318+
int want_lsp;
5319+
} checks[] = {
5320+
{"lib/Services/Svc.php", ".Svc.go", 1, 1}, /* $this->svc->go(), not $c->go() */
5321+
{"lib/Other/Svc.php", ".Svc.go", 0, 0}, /* decoy: same short name + method */
5322+
{"lib/Repo/Repo.php", ".Repo.find", 1, 1}, /* aliased import */
5323+
{foo, ".Foo.local", 1, 1}, /* same-file control */
5324+
{foo, ".Local.ping", 1, 1}, /* same-file control */
5325+
};
5326+
int ok = 1;
5327+
for (size_t i = 0; i < sizeof(checks) / sizeof(checks[0]); i++) {
5328+
php_xfile_edge_count_t c = {gbuf, foo, checks[i].tgt, checks[i].suffix, "php_", 0, 0};
5329+
cbm_gbuf_foreach_edge(gbuf, php_xfile_count_edge, &c);
5330+
if (c.total != checks[i].want_total || c.with_strategy != checks[i].want_lsp) {
5331+
printf(" [%s] CALLS Foo.php -> %s%s: %d edge(s), %d via php_*; want %d, %d\n", driver,
5332+
checks[i].tgt, checks[i].suffix, c.total, c.with_strategy, checks[i].want_total,
5333+
checks[i].want_lsp);
5334+
ok = 0;
5335+
}
5336+
}
5337+
return ok;
5338+
}
5339+
5340+
/* Run the fixture on both venues. extra_use (nullable) is one more `use` line
5341+
* added to Foo.php. Returns 1 when both graphs pass, 0 when not, -1 on setup
5342+
* failure. */
5343+
static int php_xfile_run(const char *extra_use) {
5344+
char tmpdir[256];
5345+
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_phpxfile_XXXXXX");
5346+
if (!cbm_mkdtemp(tmpdir)) {
5347+
return -1;
5348+
}
5349+
char paths[PHP_XFILE_FILE_COUNT][512];
5350+
cbm_file_info_t files[PHP_XFILE_FILE_COUNT];
5351+
memset(files, 0, sizeof(files));
5352+
static const char foo_use[] = "use Vendor\\Pkg\\Client;\n";
5353+
for (int i = 0; i < PHP_XFILE_FILE_COUNT; i++) {
5354+
snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, k_php_xfile_files[i].rel);
5355+
char dir[512];
5356+
snprintf(dir, sizeof(dir), "%s", paths[i]);
5357+
*strrchr(dir, '/') = '\0';
5358+
cbm_mkdir_p(dir, 0755);
5359+
char src[2048];
5360+
const char *s = k_php_xfile_files[i].src;
5361+
const char *at = extra_use ? strstr(s, foo_use) : NULL;
5362+
if (at) {
5363+
size_t head = (size_t)(at - s) + sizeof(foo_use) - 1;
5364+
snprintf(src, sizeof(src), "%.*s%s%s", (int)head, s, extra_use, s + head);
5365+
s = src;
5366+
}
5367+
if (th_write_file(paths[i], s) != 0) {
5368+
th_rmtree(tmpdir);
5369+
return -1;
5370+
}
5371+
files[i].path = paths[i];
5372+
files[i].rel_path = (char *)k_php_xfile_files[i].rel;
5373+
files[i].language = CBM_LANG_PHP;
5374+
}
5375+
5376+
cbm_gbuf_t *seq = run_sequential_with_lsp_cross_and_mutator(
5377+
"cbm_par_phpxfile", tmpdir, files, PHP_XFILE_FILE_COUNT, NULL, NULL, true);
5378+
cbm_gbuf_t *par = run_parallel_with_extract_opts_and_mutator(
5379+
"cbm_par_phpxfile", tmpdir, files, PHP_XFILE_FILE_COUNT, 2, NULL, NULL, NULL, true);
5380+
int seq_ok = seq ? php_xfile_graph_ok(seq, "sequential") : 0;
5381+
int par_ok = par ? php_xfile_graph_ok(par, "parallel") : 0;
5382+
cbm_gbuf_free(seq);
5383+
cbm_gbuf_free(par);
5384+
th_rmtree(tmpdir);
5385+
return seq_ok && par_ok;
5386+
}
5387+
5388+
TEST(parallel_php_lsp_types_imported_project_receivers_issue1186) {
5389+
ASSERT_EQ(php_xfile_run(NULL), 1);
5390+
PASS();
5391+
}
5392+
5393+
/* The same file with one more import that no file in the project declares,
5394+
* the global class `use Exception;` written in most namespaced PHP files.
5395+
* The typed import evidence leaves such a file "unknown" as a whole (#2520),
5396+
* so the class-valued import mapping must not depend on that evidence. */
5397+
TEST(parallel_php_lsp_types_imported_project_receivers_unknown_bindings_issue1186) {
5398+
ASSERT_EQ(php_xfile_run("use Exception;\n"), 1);
5399+
PASS();
5400+
}
5401+
5402+
52365403
/* A fresh PHP external row replaces stale evidence for its exact occurrence,
52375404
* including an older external row, without clearing a real project invocation
52385405
* 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) {
53465513
SUITE(parallel) {
53475514
RUN_TEST(parallel_php_external_row_replaces_only_its_exact_occurrence);
53485515
RUN_TEST(parallel_php_vendor_types_never_bind_by_name_issue1186);
5516+
RUN_TEST(parallel_php_lsp_types_imported_project_receivers_issue1186);
5517+
RUN_TEST(parallel_php_lsp_types_imported_project_receivers_unknown_bindings_issue1186);
53495518
RUN_TEST(usage_semantic_reference_candidate_trusts_marked_producer);
53505519
RUN_TEST(lsp_bare_segment_skips_preprocessor_spacing);
53515520
RUN_TEST(lsp_resolve_qualified_static_call_normalizes_colons);

0 commit comments

Comments
 (0)