From cdd8c895d051d4faa7ef85918dcfa04aaf757bd0 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 29 Sep 2026 01:11:14 +0200 Subject: [PATCH] fix(cpp): resolve Class::member calls across files (#1153) A method called on the object a static factory returns got no CALLS edge when the class lives in another file: `SecdManager::getInstance()->m()`, `Registry::instance().m()`, and `auto w = Widget::create(); w->m()` with std::unique_ptr or std::shared_ptr. The same code in a single file already resolved. Why: the cross-file C++ LSP resolved the scope of `Class::member` only as the bare QN or under the caller's own module. A class declared in a header and defined out of line in a .cpp is registered under that file's module (`.app.src.SecdManager.SecdManager`), so the static call missed and fell to a name heuristic (unique_name 0.38, qualified_suffix). Its return type stayed unknown, and every receiver built from it dropped the method call. A name heuristic on the same site could also bind a same-named function elsewhere: luabridge's `UserdataSharedHelper::push(...)` was bound to the free function `luabridge::push`. Fix: once every existing lookup of `Class::member` has missed, resolve the scope to exactly one registered class (bare, caller module, then a type whose QN ends in `.` at a segment boundary) and look the member up on that class: direct, aliases, bases, never re-anchored onto a same-named class elsewhere. Two classes with that name name no single target, so the call stays unresolved. The same resolution types the call, so the declared return type (pointer, reference, unique_ptr, shared_ptr) reaches chained and auto receivers. C/C++ LSP only; no call that resolved before changes. Proof, qBittorrent e8f7c18b (628 C/C++ files), same machine, fresh index with a binary built from origin/main and one with this change: - CALLS 17,401 -> 17,889: +489 edges (487 lsp_type_dispatch, 2 lsp_scoped) and -1 edge, the suffix_match `luabridge::push` false positive above. - 295 existing edges now come from the LSP with the same target (268 qualified_suffix, 27 unique_name -> lsp_scoped). - All 489 added edges checked against their source line: 155 are `Class::f()->m()` or `Class::m()` on that line, 334 go through a receiver initialised from `Class::instance()` in the same function (Preferences, SettingsStorage, UIThemeManager, Logger). None binds a different class. - `X::instance()->m()` sites whose enclosing function has the edge: 4/461 -> 304/461. Namespaced scopes (`RSS::Session::instance()`) stay unresolved: forward declarations mint extra Class nodes, so the scope does not name one class. Tests (pipeline suite; each runs the sequential and the parallel path): one per shape (pointer, reference, unique_ptr, shared_ptr), plus controls: a class with the same method names is never bound, and an undefined receiver type and a class name two libraries declare both stay unresolved. The fixture asserts that every node it names exists, so a missing node cannot pass as a missing edge. RED 3/3 on main, green with the fix, RED again on revert; dropping the uniqueness rule turns the ambiguity control RED. The Python half of #1153, a method call on a module-level instance (`manager = StackManager(); manager.status()`), is fixed by the held Python LSP change 53653c15, not here. Refs #1153 Signed-off-by: Martin Vogel --- internal/cbm/lsp/c_lsp.c | 84 ++++++++ tests/test_pipeline.c | 401 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 485 insertions(+) diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 374f6f404..72da58b49 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 @@ -4122,6 +4188,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 b117d70a9..7394263f5 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -15435,6 +15435,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_lsp_surface_large_object_array_decode); RUN_TEST(pipeline_lsp_surface_persisted_and_body_edit_invariant); @@ -15790,6 +16186,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