Merge pull request #2466 from DeusData/fix/issue-1153-instance-receivers

fix(cpp): resolve Class::member calls across files (#1153)
This commit is contained in:
Martin Vogel
2026-10-04 16:22:51 +02:00
committed by GitHub
2 changed files with 485 additions and 0 deletions
+84
View File
@@ -1025,6 +1025,53 @@ static const CBMType *c_resolve_name_to_type(CLSPContext *ctx, const char *name)
return cbm_type_named(ctx->arena, name);
}
/* Resolve the class of a qualified member `Scope::member` -- scope_qn is the
* dotted scope ("SecdManager", "ns.Widget") -- to the QN of ONE registered
* class, or NULL. The bare and caller-module spellings come first. A class
* declared in another file is registered under that file's module
* ("<proj>.app.src.SecdManager.SecdManager"), which neither spelling reaches,
* so then every registered type whose QN ends in ".<scope_qn>" at a segment
* boundary is a candidate. More than one distinct candidate (two libraries
* each declaring `Twin`) names no single class: the call stays unresolved
* rather than bind one of them by guess. */
static const char *c_resolve_scope_class_qn(CLSPContext *ctx, const char *scope_qn) {
if (!ctx->registry || !scope_qn || !scope_qn[0]) {
return NULL;
}
if (cbm_registry_lookup_type(ctx->registry, scope_qn)) {
return scope_qn;
}
if (ctx->module_qn) {
const char *mod_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, scope_qn);
if (cbm_registry_lookup_type(ctx->registry, mod_qn)) {
return mod_qn;
}
}
const char *last_dot = strrchr(scope_qn, '.');
const char *short_name = last_dot ? last_dot + 1 : scope_qn;
size_t scope_len = strlen(scope_qn);
const char *found = NULL;
CBMTypeShortIter it;
cbm_registry_types_by_short_name_chain(ctx->registry, short_name, &it);
int i;
while ((i = cbm_type_short_iter_next(&it)) >= 0) {
const char *q = it.reg->types[i].qualified_name;
size_t qlen = q ? strlen(q) : 0;
if (qlen <= scope_len || q[qlen - scope_len - 1] != '.' ||
strcmp(q + qlen - scope_len, scope_qn) != 0) {
continue;
}
if (found && strcmp(found, q) != 0) {
return NULL;
}
found = q;
}
return found;
}
static const CBMRegisteredFunc *c_lookup_scope_member(CLSPContext *ctx, const char *class_qn,
const char *member_name);
// ============================================================================
// c_parse_type_node: AST type node -> CBMType
// ============================================================================
@@ -1821,6 +1868,16 @@ static const CBMType *c_eval_expr_type_inner(CLSPContext *ctx, TSNode node) {
}
}
}
// Class::member of a class declared in another file: its signature
// carries the return type a chained or auto receiver dispatches on.
const char *scope_class =
c_resolve_scope_class_qn(ctx, cbm_arena_strndup(ctx->arena, qn, prefix_len));
const CBMRegisteredFunc *sm =
scope_class ? c_lookup_scope_member(ctx, scope_class, dot + 1) : NULL;
if (sm && sm->signature) {
return sm->signature;
}
}
return cbm_type_unknown();
@@ -2860,6 +2917,15 @@ const CBMRegisteredFunc *c_lookup_member(CLSPContext *ctx, const char *type_qn,
return c_lookup_member_depth(ctx, type_qn, member_name, 0);
}
/* A member of a class c_resolve_scope_class_qn already pinned down: direct,
* then through its aliases and base classes. Entering below depth 0 skips the
* short-name fallback, which would re-anchor the lookup onto a same-named
* class elsewhere -- the scope names exactly this class. */
static const CBMRegisteredFunc *c_lookup_scope_member(CLSPContext *ctx, const char *class_qn,
const char *member_name) {
return c_lookup_member_depth(ctx, class_qn, member_name, 1);
}
// True if any BASE class of type_qn (not type_qn itself) declares member_name —
// i.e. a method found directly on type_qn is an OVERRIDE of an inherited method.
// This mirrors the existing virtual-dispatch notion (a derived override of a base
@@ -4142,6 +4208,24 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) {
goto recurse;
}
}
// Class::method of a class declared in another file (#1153).
if (dot) {
const char *scope_class = c_resolve_scope_class_qn(
ctx, cbm_arena_strndup(ctx->arena, qn, (size_t)(dot - qn)));
const CBMRegisteredFunc *sm = NULL;
if (scope_class) {
sm = cbm_registry_lookup_method_by_types(ctx->registry, scope_class,
dot + 1, arg_types, arg_count);
if (!sm) {
sm = c_lookup_scope_member(ctx, scope_class, dot + 1);
}
}
if (sm) {
c_emit_resolved_call_at(ctx, sm->qualified_name, "lsp_scoped", 0.95f,
node);
goto recurse;
}
}
c_emit_unresolved_call(ctx, qn, "scoped_not_in_registry");
}
goto recurse;
+401
View File
@@ -16513,6 +16513,402 @@ TEST(pipeline_objectscript_export_range_join_keeps_one_trailing_marker) {
}
#endif
/* #1153: a C++ method called on the object a static factory returns
* (`SecdManager::getInstance()->m()`, `Registry::instance().m()`,
* `auto w = Widget::create(); w->m()`) got no CALLS edge when the class lives
* in another file. The class is declared in a header, its methods are defined
* out of line in a .cpp, and the call is in a third file -- the reporter's
* layout. The cross-file C++ LSP resolved `Class::member` only under the
* caller's own module QN or as a bare QN, so the static call missed, its return
* type stayed unknown, and every receiver built from it dropped the method
* call. In one file the same code already resolved.
*
* `Other` declares the same method names in the same header: a correct
* resolution must never bind them. `Twin` is declared by two libraries, so
* `Twin::get()` names no single class and must stay unresolved. `Mystery` is
* never defined, so `m->paint()` has no receiver type to dispatch on. 52
* fillers + CBM_WORKERS=4 force the parallel path; each test runs both. */
/* write_temp_file creates one directory level; this layout nests two. */
static void cpp_recv_write(const char *tmp, const char *name, const char *content) {
char dir[512];
snprintf(dir, sizeof(dir), "%s/%s", tmp, name);
char *slash = strrchr(dir, '/');
if (slash) {
*slash = '\0';
cbm_mkdir_p(dir, 0755);
}
write_temp_file(tmp, name, content);
}
static void cpp_recv_write_fixture(const char *tmp, bool parallel) {
cpp_recv_write(tmp, "app/src/SecdManager.h",
"#pragma once\n"
"#include <memory>\n\n"
"class SecdManager {\n"
"public:\n"
" static SecdManager *getInstance();\n"
" bool secd_retrieve_pic_fpc_i2c_id(int ifdh, int &fpc_id, int &pic_id);\n"
"};\n\n"
"class Registry {\n"
"public:\n"
" static Registry &instance();\n"
" void registerItem(int id);\n"
"};\n\n"
"class Widget {\n"
"public:\n"
" static std::unique_ptr<Widget> create();\n"
" void paint();\n"
"};\n\n"
"class Session {\n"
"public:\n"
" static std::shared_ptr<Session> open();\n"
" void close();\n"
"};\n\n"
"class Other {\n"
"public:\n"
" bool secd_retrieve_pic_fpc_i2c_id(int ifdh, int &fpc_id, int &pic_id);\n"
" void registerItem(int id);\n"
" void paint();\n"
" void close();\n"
"};\n");
cpp_recv_write(
tmp, "app/src/SecdManager.cpp",
"#include \"SecdManager.h\"\n\n"
"SecdManager *SecdManager::getInstance() {\n"
" static SecdManager inst;\n"
" return &inst;\n"
"}\n\n"
"bool SecdManager::secd_retrieve_pic_fpc_i2c_id(int ifdh, int &fpc_id, "
"int &pic_id) {\n"
" fpc_id = ifdh;\n"
" pic_id = ifdh;\n"
" return true;\n"
"}\n\n"
"Registry &Registry::instance() {\n"
" static Registry r;\n"
" return r;\n"
"}\n\n"
"void Registry::registerItem(int id) { (void)id; }\n\n"
"std::unique_ptr<Widget> Widget::create() { return std::make_unique<Widget>(); }\n\n"
"void Widget::paint() {}\n\n"
"std::shared_ptr<Session> Session::open() { return std::make_shared<Session>(); }\n\n"
"void Session::close() {}\n\n"
"bool Other::secd_retrieve_pic_fpc_i2c_id(int ifdh, int &fpc_id, int &pic_id) {\n"
" fpc_id = pic_id = ifdh;\n"
" return false;\n"
"}\n"
"void Other::registerItem(int id) { (void)id; }\n"
"void Other::paint() {}\n"
"void Other::close() {}\n");
for (int lib = 0; lib < 2; lib++) {
char path[64];
snprintf(path, sizeof(path), "lib_%c/Twin.h", 'a' + lib);
cpp_recv_write(tmp, path,
"#pragma once\n"
"class Twin {\n"
"public:\n"
" static Twin *get();\n"
" void run();\n"
"};\n");
snprintf(path, sizeof(path), "lib_%c/Twin.cpp", 'a' + lib);
cpp_recv_write(tmp, path,
"#include \"Twin.h\"\n\n"
"Twin *Twin::get() {\n"
" static Twin t;\n"
" return &t;\n"
"}\n\n"
"void Twin::run() {}\n");
}
cpp_recv_write(
tmp, "pil/src/SecdCfgHandlerThread.cpp",
"#include \"SecdManager.h\"\n"
"#include \"Twin.h\"\n\n"
"int GetExternalPhyPresent(int ifdh) {\n"
" int fpc_id = 0, pic_id = 0;\n"
" if (!SecdManager::getInstance()->secd_retrieve_pic_fpc_i2c_id(ifdh, fpc_id, "
"pic_id)) {\n"
" return -1;\n"
" }\n"
" return fpc_id + pic_id;\n"
"}\n\n"
"void UseRegistry() {\n"
" Registry::instance().registerItem(7);\n"
"}\n\n"
"void UseWidget() {\n"
" auto w = Widget::create();\n"
" w->paint();\n"
"}\n\n"
"void UseSession() {\n"
" auto s = Session::open();\n"
" s->close();\n"
"}\n\n"
"void UseUnknown(struct Mystery *m) {\n"
" m->paint();\n"
"}\n\n"
"void UseTwin() {\n"
" Twin::get()->run();\n"
"}\n");
for (int i = 0; parallel && i < 52; i++) {
char name[64];
char pad[128];
snprintf(name, sizeof(name), "pad/cpp_pad_%02d.cpp", i);
snprintf(pad, sizeof(pad), "int cpp_pad_%02d() { return %d; }\n", i, i);
write_temp_file(tmp, name, pad);
}
}
typedef struct {
char tmp[256];
char db[512];
char *saved_workers;
cbm_pipeline_t *p;
cbm_store_t *s;
const char *project;
} CppRecvRun;
static void cpp_recv_close(CppRecvRun *r) {
if (r->s) {
cbm_store_close(r->s);
}
if (r->p) {
cbm_pipeline_free(r->p);
}
if (r->saved_workers) {
cbm_setenv("CBM_WORKERS", r->saved_workers, 1);
free(r->saved_workers);
} else {
cbm_unsetenv("CBM_WORKERS");
}
if (r->tmp[0]) {
th_rmtree(r->tmp);
}
memset(r, 0, sizeof(*r));
}
/* Every node an assertion names must exist, or "no CALLS edge" would also
* pass for a fixture that never got indexed. */
static bool cpp_recv_fixture_indexed(const CppRecvRun *r) {
static const char *const required[] = {
"pil.src.SecdCfgHandlerThread.GetExternalPhyPresent",
"pil.src.SecdCfgHandlerThread.UseRegistry",
"pil.src.SecdCfgHandlerThread.UseWidget",
"pil.src.SecdCfgHandlerThread.UseSession",
"pil.src.SecdCfgHandlerThread.UseUnknown",
"pil.src.SecdCfgHandlerThread.UseTwin",
"app.src.SecdManager.SecdManager.getInstance",
"app.src.SecdManager.SecdManager.secd_retrieve_pic_fpc_i2c_id",
"app.src.SecdManager.Registry.registerItem",
"app.src.SecdManager.Widget.paint",
"app.src.SecdManager.Session.close",
"app.src.SecdManager.Other.secd_retrieve_pic_fpc_i2c_id",
"app.src.SecdManager.Other.registerItem",
"app.src.SecdManager.Other.paint",
"app.src.SecdManager.Other.close",
"lib_a.Twin.Twin.get",
"lib_a.Twin.Twin.run",
"lib_b.Twin.Twin.get",
"lib_b.Twin.Twin.run",
};
bool all = true;
for (size_t i = 0; i < sizeof(required) / sizeof(required[0]); i++) {
char qn[512];
snprintf(qn, sizeof(qn), "%s.%s", r->project, required[i]);
cbm_node_t n;
memset(&n, 0, sizeof(n));
if (cbm_store_find_node_by_qn(r->s, r->project, qn, &n) != CBM_STORE_OK) {
printf(" fixture node missing: %s\n", qn);
all = false;
}
cbm_node_free_fields(&n);
}
return all;
}
/* Index the fixture; false (after cleanup) when the run itself fails. */
static bool cpp_recv_open(CppRecvRun *r, bool parallel) {
memset(r, 0, sizeof(*r));
snprintf(r->tmp, sizeof(r->tmp), "/tmp/cbm_cpp_recv_XXXXXX");
if (!cbm_mkdtemp(r->tmp)) {
r->tmp[0] = '\0';
return false;
}
cpp_recv_write_fixture(r->tmp, parallel);
const char *old_workers = getenv("CBM_WORKERS");
r->saved_workers = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", parallel ? "4" : "1", 1);
snprintf(r->db, sizeof(r->db), "%s/cpp_recv.db", r->tmp);
r->p = cbm_pipeline_new(r->tmp, r->db, CBM_MODE_FULL);
if (!r->p || cbm_pipeline_run(r->p) != 0) {
cpp_recv_close(r);
return false;
}
r->s = cbm_store_open_path(r->db);
r->project = cbm_pipeline_project_name(r->p);
if (!r->s || !r->project || !cpp_recv_fixture_indexed(r)) {
cpp_recv_close(r);
return false;
}
return true;
}
/* Properties of the CALLS edge caller -> target (QNs below the project), or
* NULL when there is no such edge. The caller frees the result. */
static char *cpp_recv_calls(const CppRecvRun *r, const char *caller, const char *target) {
char src_qn[512];
char tgt_qn[512];
snprintf(src_qn, sizeof(src_qn), "%s.%s", r->project, caller);
snprintf(tgt_qn, sizeof(tgt_qn), "%s.%s", r->project, target);
cbm_node_t src;
cbm_node_t tgt;
memset(&src, 0, sizeof(src));
memset(&tgt, 0, sizeof(tgt));
char *props = NULL;
if (cbm_store_find_node_by_qn(r->s, r->project, src_qn, &src) == CBM_STORE_OK &&
cbm_store_find_node_by_qn(r->s, r->project, tgt_qn, &tgt) == CBM_STORE_OK) {
cbm_edge_t *edges = NULL;
int ec = 0;
cbm_store_find_edges_by_source_type(r->s, src.id, "CALLS", &edges, &ec);
for (int i = 0; i < ec && !props; i++) {
if (edges[i].target_id == tgt.id) {
props = strdup(edges[i].properties_json ? edges[i].properties_json : "{}");
}
}
if (edges) {
cbm_store_free_edges(edges, ec);
}
}
cbm_node_free_fields(&src);
cbm_node_free_fields(&tgt);
return props;
}
#define CPP_RECV_CALLER "pil.src.SecdCfgHandlerThread."
#define CPP_RECV_CLASS "app.src.SecdManager."
/* 1 = an lsp_* CALLS edge, 0 = no edge, -1 = an edge from a name heuristic. */
static int cpp_recv_edge_kind(const CppRecvRun *r, bool parallel, const char *caller,
const char *target) {
char *props = cpp_recv_calls(r, caller, target);
int kind = props ? (strstr(props, "\"strategy\":\"lsp_") ? 1 : -1) : 0;
if (kind != 1) {
printf(" [%s] %s -> %s: %s\n", parallel ? "parallel" : "sequential", caller, target,
props ? props : "(no CALLS edge)");
}
free(props);
return kind;
}
/* Reported shape: a raw pointer from a singleton accessor. */
TEST(pipeline_cpp_static_factory_pointer_receiver_issue1153) {
for (int mode = 0; mode < 2; mode++) {
bool parallel = mode == 1;
CppRecvRun r;
ASSERT_TRUE(cpp_recv_open(&r, parallel));
int factory = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "GetExternalPhyPresent",
CPP_RECV_CLASS "SecdManager.getInstance");
int method = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "GetExternalPhyPresent",
CPP_RECV_CLASS "SecdManager.secd_retrieve_pic_fpc_i2c_id");
char *wrong = cpp_recv_calls(&r, CPP_RECV_CALLER "GetExternalPhyPresent",
CPP_RECV_CLASS "Other.secd_retrieve_pic_fpc_i2c_id");
cpp_recv_close(&r);
bool no_wrong = wrong == NULL;
free(wrong);
ASSERT_EQ(factory, 1); /* RED: only a unique_name guess */
ASSERT_EQ(method, 1); /* RED: the call through the returned pointer dropped */
ASSERT_TRUE(no_wrong); /* control: never the same-named Other method */
}
PASS();
}
/* `Foo::instance().bar()`: a reference-returning accessor. */
TEST(pipeline_cpp_static_factory_reference_receiver_issue1153) {
for (int mode = 0; mode < 2; mode++) {
bool parallel = mode == 1;
CppRecvRun r;
ASSERT_TRUE(cpp_recv_open(&r, parallel));
int method = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "UseRegistry",
CPP_RECV_CLASS "Registry.registerItem");
char *wrong =
cpp_recv_calls(&r, CPP_RECV_CALLER "UseRegistry", CPP_RECV_CLASS "Other.registerItem");
cpp_recv_close(&r);
bool no_wrong = wrong == NULL;
free(wrong);
ASSERT_EQ(method, 1); /* RED: dropped */
ASSERT_TRUE(no_wrong); /* control: never the same-named Other method */
}
PASS();
}
/* `auto w = Widget::create(); w->paint();` with std::unique_ptr<Widget>. */
TEST(pipeline_cpp_static_factory_unique_ptr_receiver_issue1153) {
for (int mode = 0; mode < 2; mode++) {
bool parallel = mode == 1;
CppRecvRun r;
ASSERT_TRUE(cpp_recv_open(&r, parallel));
int method = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "UseWidget",
CPP_RECV_CLASS "Widget.paint");
char *wrong = cpp_recv_calls(&r, CPP_RECV_CALLER "UseWidget", CPP_RECV_CLASS "Other.paint");
cpp_recv_close(&r);
bool no_wrong = wrong == NULL;
free(wrong);
ASSERT_EQ(method, 1);
ASSERT_TRUE(no_wrong);
}
PASS();
}
/* `auto s = Session::open(); s->close();` with std::shared_ptr<Session>. */
TEST(pipeline_cpp_static_factory_shared_ptr_receiver_issue1153) {
for (int mode = 0; mode < 2; mode++) {
bool parallel = mode == 1;
CppRecvRun r;
ASSERT_TRUE(cpp_recv_open(&r, parallel));
int method = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "UseSession",
CPP_RECV_CLASS "Session.close");
char *wrong =
cpp_recv_calls(&r, CPP_RECV_CALLER "UseSession", CPP_RECV_CLASS "Other.close");
cpp_recv_close(&r);
bool no_wrong = wrong == NULL;
free(wrong);
ASSERT_EQ(method, 1);
ASSERT_TRUE(no_wrong);
}
PASS();
}
/* Controls: no receiver type, or a class name two libraries declare, gives
* the LSP nothing to dispatch on -- neither may gain an edge. */
TEST(pipeline_cpp_unresolvable_receiver_stays_unbound_issue1153) {
static const char *const twin_targets[] = {"lib_a.Twin.Twin.get", "lib_a.Twin.Twin.run",
"lib_b.Twin.Twin.get", "lib_b.Twin.Twin.run"};
for (int mode = 0; mode < 2; mode++) {
bool parallel = mode == 1;
CppRecvRun r;
ASSERT_TRUE(cpp_recv_open(&r, parallel));
char *widget =
cpp_recv_calls(&r, CPP_RECV_CALLER "UseUnknown", CPP_RECV_CLASS "Widget.paint");
char *other =
cpp_recv_calls(&r, CPP_RECV_CALLER "UseUnknown", CPP_RECV_CLASS "Other.paint");
int twin_lsp = 0;
for (int t = 0; t < 4; t++) {
char *props = cpp_recv_calls(&r, CPP_RECV_CALLER "UseTwin", twin_targets[t]);
if (props && strstr(props, "\"strategy\":\"lsp_")) {
printf(" [%s] UseTwin -> %s: %s\n", parallel ? "parallel" : "sequential",
twin_targets[t], props);
twin_lsp++;
}
free(props);
}
cpp_recv_close(&r);
bool unknown_unbound = widget == NULL && other == NULL;
free(widget);
free(other);
ASSERT_TRUE(unknown_unbound);
ASSERT_EQ(twin_lsp, 0);
}
PASS();
}
SUITE(pipeline) {
RUN_TEST(pipeline_nested_fixture_files_are_written);
RUN_TEST(pipeline_fixture_file_parent_is_preserved);
@@ -16883,6 +17279,11 @@ SUITE(pipeline) {
RUN_TEST(pipeline_markdown_and_config_prose_reaches_fts_body);
RUN_TEST(pipeline_semantic_edges_no_functions);
RUN_TEST(pipeline_semantic_batched_matches_unbatched);
RUN_TEST(pipeline_cpp_static_factory_pointer_receiver_issue1153);
RUN_TEST(pipeline_cpp_static_factory_reference_receiver_issue1153);
RUN_TEST(pipeline_cpp_static_factory_unique_ptr_receiver_issue1153);
RUN_TEST(pipeline_cpp_static_factory_shared_ptr_receiver_issue1153);
RUN_TEST(pipeline_cpp_unresolvable_receiver_stays_unbound_issue1153);
}
/* Focused semantic-manifest and publication contracts. Kept separate from the