diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 74b97d69d..22d630020 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -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 + * (".app.src.SecdManager.SecdManager"), which neither spelling reaches, + * so then every registered type whose QN ends in "." 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; diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 49f912703..f01073a10 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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 \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 create();\n" + " void paint();\n" + "};\n\n" + "class Session {\n" + "public:\n" + " static std::shared_ptr 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::create() { return std::make_unique(); }\n\n" + "void Widget::paint() {}\n\n" + "std::shared_ptr Session::open() { return std::make_shared(); }\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. */ +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. */ +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