mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
fix(php): drop the name guess when the receiver's class is not indexed
When the PHP LSP knows a receiver's class but the class is not in the index, it emits php_method_typed_unindexed at 0.55, below the LSP confidence floor. The resolver skipped that row and the weak short-name fallback bound the call to a same-named project method: `$file->getUid()` on a vendor File became a CALLS edge to App\Item::getUid. #2520 turns this row into php_external_receiver when a vendor `use` proves the class external, but two cases still reach the fallback. A global function import (`use function sprintf;`) leaves the file's import bindings unproven, since #2705 proves only class imports absent. And a vendor type that arrives through a return type (`$repository->findFile()->getUid()`) has no `use` in the calling file at all. Both resolvers now drop a unique_name or suffix_match edge when the LSP row names a receiver class that no project class-like symbol shares by simple name. A project class with that name keeps the fallback, because the PHP LSP does not follow every cross-file type. The test pair covers both cases, through pass_calls.c and pass_parallel.c. Each case is RED on main on its own (vendorCall, chainedVendorCall); calls on a project-typed receiver and on $this still resolve. On a 211-file TYPO3 project PHP CALLS edges go from 1083 to 1059 (-18 suffix_match, -6 unique_name; typed php_* edges unchanged): 25 removed, and 1 that a removed edge on the same caller had absorbed. Signed-off-by: Wolfgang Klinger <wolfgang@wazum.com>
This commit is contained in:
@@ -414,6 +414,43 @@ static inline bool cbm_pipeline_invocation_leaf_matches(const CBMResolvedCall *r
|
||||
call_leaf && strcmp(cbm_lsp_bare_segment(resolved->reason), call_leaf) == 0;
|
||||
}
|
||||
|
||||
/* #606: a sub-floor `php_method_typed_unindexed` row still proves the receiver
|
||||
* is not a project class, so a weak short-name fallback must not bind it. A
|
||||
* project class with the same simple name keeps the fallback, since the PHP
|
||||
* LSP does not follow every cross-file type. */
|
||||
static inline bool cbm_pipeline_php_receiver_type_unindexed(const CBMResolvedCallArray *arr,
|
||||
const CBMCall *call,
|
||||
const cbm_registry_t *registry) {
|
||||
if (!arr || !call || !call->enclosing_func_qn || !call->callee_name) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < arr->count; i++) {
|
||||
const CBMResolvedCall *rc = &arr->items[i];
|
||||
if (rc->kind != CBM_RESOLVED_INVOCATION || !rc->caller_qn || !rc->callee_qn ||
|
||||
!rc->strategy || strcmp(rc->strategy, "php_method_typed_unindexed") != 0 ||
|
||||
strcmp(rc->caller_qn, call->enclosing_func_qn) != 0) {
|
||||
continue;
|
||||
}
|
||||
int site_rank = cbm_pipeline_invocation_site_rank(rc, call);
|
||||
if (site_rank == 0 || !cbm_pipeline_invocation_leaf_matches(rc, call, site_rank)) {
|
||||
continue;
|
||||
}
|
||||
const char *method_dot = strrchr(rc->callee_qn, '.');
|
||||
if (!method_dot || method_dot == rc->callee_qn) {
|
||||
continue;
|
||||
}
|
||||
char class_qn[CBM_SZ_512];
|
||||
size_t class_len = (size_t)(method_dot - rc->callee_qn);
|
||||
if (class_len >= sizeof(class_qn)) {
|
||||
continue;
|
||||
}
|
||||
memcpy(class_qn, rc->callee_qn, class_len);
|
||||
class_qn[class_len] = '\0';
|
||||
return !cbm_registry_has_type_named(registry, cbm_lsp_bare_segment(class_qn));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Look up the highest-confidence LSP-resolved call entry whose caller QN
|
||||
* matches the textual call's enclosing function and whose callee QN
|
||||
* short-name matches the textual callee. Returns a pointer into `arr`
|
||||
|
||||
@@ -1150,7 +1150,10 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
|
||||
* paths are module chains) — MUST match pass_parallel.c exactly. */
|
||||
(lang == CBM_LANG_PYTHON && cbm_suppress_weak_import_bound_call(true, true, res.strategy) &&
|
||||
cbm_python_import_binding_contradicts(imports, call->callee_name, res.qualified_name,
|
||||
ctx->gbuf, ctx->project_name, rel));
|
||||
ctx->gbuf, ctx->project_name, rel)) ||
|
||||
/* MUST match pass_parallel.c exactly. */
|
||||
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
|
||||
cbm_pipeline_php_receiver_type_unindexed(lsp_calls, call, ctx->registry));
|
||||
|
||||
/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
|
||||
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
|
||||
|
||||
@@ -3409,7 +3409,10 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
cbm_suppress_weak_import_bound_call(true, true, res.strategy) &&
|
||||
cbm_python_import_binding_contradicts(&result->imports, call->callee_name,
|
||||
res.qualified_name, rc->main_gbuf,
|
||||
rc->project_name, rel));
|
||||
rc->project_name, rel)) ||
|
||||
/* MUST match pass_calls.c exactly. */
|
||||
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
|
||||
cbm_pipeline_php_receiver_type_unindexed(&result->resolved_calls, call, rc->registry));
|
||||
|
||||
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
|
||||
* service signal lives in the callee_name. The registry can mis-resolve
|
||||
|
||||
@@ -496,6 +496,8 @@ const char *cbm_registry_value_target(const cbm_registry_t *r, const char *qn);
|
||||
int cbm_registry_find_by_name(const cbm_registry_t *r, const char *name, const char ***out,
|
||||
int *count);
|
||||
|
||||
bool cbm_registry_has_type_named(const cbm_registry_t *r, const char *name);
|
||||
|
||||
/* Return total number of entries. */
|
||||
int cbm_registry_size(const cbm_registry_t *r);
|
||||
|
||||
|
||||
@@ -1467,6 +1467,21 @@ const char *cbm_registry_unique_field_qn(const cbm_registry_t *r, const char *me
|
||||
return only;
|
||||
}
|
||||
|
||||
bool cbm_registry_has_type_named(const cbm_registry_t *r, const char *name) {
|
||||
if (!r || !name || !name[0]) {
|
||||
return false;
|
||||
}
|
||||
const char **qns = NULL;
|
||||
int count = 0;
|
||||
cbm_registry_find_by_name(r, name, &qns, &count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (cbm_label_is_type_like(cbm_registry_label_of(r, qns[i]))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int cbm_registry_size(const cbm_registry_t *r) {
|
||||
return r ? (int)cbm_ht_count(r->exact) : 0;
|
||||
}
|
||||
|
||||
@@ -8644,6 +8644,163 @@ TEST(pipeline_python_receiver_suppresses_weak_method_edge) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #606: a receiver typed as an unindexed (vendor/) class must not weakly bind a
|
||||
* same-named project method. */
|
||||
static void write_php_receiver_fixture(const char *tmp) {
|
||||
write_temp_file(tmp, "src/Item.php",
|
||||
"<?php\n"
|
||||
"namespace App;\n"
|
||||
"\n"
|
||||
"class Item\n"
|
||||
"{\n"
|
||||
" public function getUid(): int\n"
|
||||
" {\n"
|
||||
" return 1;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/Domain/Repository.php",
|
||||
"<?php\n"
|
||||
"namespace App\\Domain;\n"
|
||||
"\n"
|
||||
"use App\\Item;\n"
|
||||
"use Vendor\\Storage\\File;\n"
|
||||
"\n"
|
||||
"class Repository\n"
|
||||
"{\n"
|
||||
" public function findItem(): Item\n"
|
||||
" {\n"
|
||||
" return new Item();\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" public function findFile(): File\n"
|
||||
" {\n"
|
||||
" return new File();\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/Listener.php",
|
||||
"<?php\n"
|
||||
"namespace App;\n"
|
||||
"\n"
|
||||
"use Vendor\\Storage\\File;\n"
|
||||
"use function sprintf;\n"
|
||||
"\n"
|
||||
"class Listener\n"
|
||||
"{\n"
|
||||
" public function vendorCall(File $file): int\n"
|
||||
" {\n"
|
||||
" return $file->getUid();\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" public function selfCall(): int\n"
|
||||
" {\n"
|
||||
" return $this->localHelper();\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" private function localHelper(): int\n"
|
||||
" {\n"
|
||||
" return 1;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/ChainListener.php",
|
||||
"<?php\n"
|
||||
"namespace App;\n"
|
||||
"\n"
|
||||
"use App\\Domain\\Repository;\n"
|
||||
"\n"
|
||||
"class ChainListener\n"
|
||||
"{\n"
|
||||
" public function chainedVendorCall(Repository $repository): int\n"
|
||||
" {\n"
|
||||
" return $repository->findFile()->getUid();\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" public function projectCall(Repository $repository): Item\n"
|
||||
" {\n"
|
||||
" return $repository->findItem();\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
}
|
||||
|
||||
/* Fewer than 50 files exercises pass_calls.c. */
|
||||
TEST(pipeline_php_receiver_suppresses_weak_method_edge) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_php_recv_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_php_receiver_fixture(tmp);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/php_recv.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_GTE(fixture_node_count(s, project, "src/Item.php", "getUid", "Method"), 1);
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "vendorCall", "getUid"));
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "chainedVendorCall", "getUid"));
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "projectCall", "findItem"));
|
||||
ASSERT_GTE(named_edge_count(s, project, "CALLS", "selfCall", "localHelper"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* >= 50 files forces pass_parallel.c. */
|
||||
TEST(pipeline_php_receiver_parallel_suppresses_weak_method_edge) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_php_par_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_php_receiver_fixture(tmp);
|
||||
for (int i = 0; i < 52; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "src/Filler%d.php", i);
|
||||
snprintf(body, sizeof(body), "<?php\nfunction filler%d(): int\n{\n return %d;\n}\n", i,
|
||||
i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/php_par.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_GTE(fixture_node_count(s, project, "src/Item.php", "getUid", "Method"), 1);
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "vendorCall", "getUid"));
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "chainedVendorCall", "getUid"));
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "projectCall", "findItem"));
|
||||
ASSERT_GTE(named_edge_count(s, project, "CALLS", "selfCall", "localHelper"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The member guard's one exemption (2026-09-16, v0.10.8 → v0.11.0 A/B on
|
||||
* django/django): a member call whose receiver is an attribute chain rooted at
|
||||
* self/cls, whose callee has exactly ONE definition in the project and is not a
|
||||
@@ -26177,6 +26334,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354);
|
||||
RUN_TEST(pipeline_ts_param_property_injection_issue514);
|
||||
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_php_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_php_receiver_parallel_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call);
|
||||
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);
|
||||
RUN_TEST(pipeline_go_multi_return_receiver_is_typed);
|
||||
|
||||
Reference in New Issue
Block a user