From 55f1b5fe909834bffeb81be899d372e1b7d5c76e Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 13 Oct 2026 17:50:15 +0200 Subject: [PATCH] fix(resolve): a bare C call never weighs struct members or methods A struct member in the short-name registry turned a unique header function into a two-candidate name: `put_page(page)` beside the arm64 `kvm_pgtable_mm_ops.put_page` member. The lookup then answered by suffix_match, and the #725 cross-language guard refused it because .h is C++ in the extension table -- the call vanished, not even a CALL_REFERENCE remained. On the Linux kernel every header function with a same-named member elsewhere lost its callers: put_page 345 -> 9, resource_size 1,159 -> 4, alloc_skb 419 -> 4 (v0.11.0 -> main). A bare C call (no `.`/`->`) can name neither a struct member -- reached only through its object, cbm_call_onto_field_policy already refuses every Field it lands on -- nor a method, which C does not have. The CALLS resolvers (sequential and parallel twins) now ask cbm_registry_resolve_call_lang(), which sets those candidates aside before the name strategies run; the per-file resolve cache keys on that choice. References and every other language are unchanged. Setting aside only the Fields was not enough: redis's libc `read()` then became the unique name of a C++ Qt adapter's `read` Method (62 false edges in v0.11.0). Exempting C/C++ from the #725 guard instead opened 375k C -> header suffix_match edges, ~40 % of them false on redis -- rejected. Proof (macOS arm64, Linux kernel 2d2338c93): - put_page 9 -> 344, resource_size 4 -> 1,158, alloc_skb 4 -> 417, cpu_online 5 -> 188, device_unlock 1 -> 140, dma_unmap_sgtable 2 -> 77 (v0.11.0: 345 / 1,159 / 419 / 198 / 140 / 77). - CALLS 1,845,215 -> 1,924,199 (+4.3 %); a seeded sample of 300 gained edges: 300 have a bare `name(` call in the caller's span. - redis: +8 call pairs; libc read/write no longer bind project members. - New tests (RED without the change): registry c_bare_call_does_not_weigh_field_namesakes; pipeline pipeline_c_call_reaches_header_function_beside_field (+ _parallel). - registry, pipeline, extraction, extraction_imports, c_lsp, edge_types_probe, lsp_resolution_probe, complexity, parallel, incremental: 2134 passed. Signed-off-by: Martin Vogel --- src/pipeline/pass_calls.c | 4 +- src/pipeline/pass_parallel.c | 4 +- src/pipeline/pipeline_internal.h | 7 ++ src/pipeline/registry.c | 122 ++++++++++++++++++++----- tests/test_pipeline.c | 152 +++++++++++++++++++++++++++++++ tests/test_registry.c | 43 +++++++++ 6 files changed, 305 insertions(+), 27 deletions(-) diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 7682a0acb..1eb3bddff 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -967,8 +967,8 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM lang, lsp->strategy, lsp->callee_qn, ctx->project_name); cbm_resolution_t res = {0}; if (!rust_external && !php_vendor_call) { - res = cbm_registry_resolve_lang(ctx->registry, call->callee_name, module_qn, imp_keys, - imp_vals, imp_count, lang); + res = cbm_registry_resolve_call_lang(ctx->registry, call->callee_name, module_qn, imp_keys, + imp_vals, imp_count, lang); /* Cross-language veto: a name-only guess never binds another * language's symbol (a JS app.get must not become Python's * builtins.dict.get). The vetoed answer becomes EMPTY here, before diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index efc407934..dbac7be35 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -3252,8 +3252,8 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB lang, lsp->strategy, lsp->callee_qn, rc->project_name); if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution && !rust_external && !php_vendor_call) { - res = cbm_registry_resolve_lang(rc->registry, call->callee_name, module_qn, imp_keys, - imp_vals, imp_count, lang); + res = cbm_registry_resolve_call_lang(rc->registry, call->callee_name, module_qn, + imp_keys, imp_vals, imp_count, lang); } /* `p->open(fd)`: the registry does not split a callee on the arrow. * The object's type decides, as for the dot call handled further diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index c3891f9a2..194100473 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -253,6 +253,13 @@ cbm_resolution_t cbm_registry_resolve_lang(const cbm_registry_t *r, const char * const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, CBMLanguage lang); +/* The CALLS variant: as cbm_registry_resolve_lang, except that a bare C call + * (no `.` / `->`) does not consider Field or Method nodes, which it can never + * call. */ +cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const char *callee_name, + const char *module_qn, const char **import_map_keys, + const char **import_map_vals, int import_map_count, + CBMLanguage lang); #ifdef CBM_ENABLE_TEST_SEAMS void cbm_pxc_test_import_probe_fail_after(int successful_allocations); #endif diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index ce5c4f8a7..f53d1dd12 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -438,7 +438,8 @@ void cbm_registry_import_map_cache_end(void) { * eliminated. */ typedef struct { cbm_resolution_t res; - size_t scope_len; /* raw and Python-scoped requests may share one file cache */ + size_t scope_len; /* raw and Python-scoped requests may share one file cache */ + bool skip_members; /* a C call and a C reference to the same name share it too */ } resolve_cache_entry_t; static CBM_TLS CBMHashTable *_resolve_cache = NULL; @@ -1667,19 +1668,41 @@ static bool receiver_chain_admits(const char *callee_name, const char *candidate return false; } -/* Strategy 3+4: Name lookup + suffix match */ -static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name, - const char *module_qn, const char **import_vals, - int import_count) { - const char *lookup = simple_name(callee_name); - qn_array_t *arr = cbm_ht_get(r->by_name, lookup); - if (!arr || arr->count == 0) { - return empty_result(); - } - if (arr->count > REG_MAX_CANDIDATES) { - return empty_result(); /* unresolvably ambiguous — see REG_MAX_CANDIDATES */ +/* The same-name candidates without the members -- Field and Method nodes -- + * in `view` (backed by the caller's buffers, at most REG_MAX_CANDIDATES + * entries). The side arrays are carried over only when the bucket has them + * for every entry. */ +static void candidates_without_members(const cbm_registry_t *r, const qn_array_t *arr, + qn_array_t *view, char **items, uint8_t *is_test, + uint8_t *lang) { + const uint8_t *flags = qn_test_flags(arr); + const uint8_t *langs = qn_lang_flags(arr); + bool side = flags && langs; + int n = 0; + for (int i = 0; i < arr->count; i++) { + const char *label = cbm_registry_label_of(r, arr->items[i]); + if (label && (strcmp(label, "Field") == 0 || strcmp(label, "Method") == 0)) { + continue; + } + items[n] = arr->items[i]; + if (side) { + is_test[n] = flags[i]; + lang[n] = langs[i]; + } + n++; } + *view = (qn_array_t){.items = items, + .count = n, + .cap = n, + .is_test = side ? is_test : NULL, + .is_test_cap = side ? n : 0, + .lang = side ? lang : NULL}; +} +/* Strategies 3.5, 3 and 4 over one same-name candidate set. */ +static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const qn_array_t *arr, + const char *callee_name, const char *module_qn, + const char **import_vals, int import_count) { /* Strategy 3.5: a qualified callee disambiguates among multiple same-name * candidates by full qualified tail, before bare-name scoring collapses * them onto a single winner. */ @@ -1719,12 +1742,44 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char return empty_result(); } +/* Strategy 3+4: Name lookup + suffix match. skip_members: the callee cannot + * name a member (a bare C call: C has no methods, and a struct member is only + * ever reached through its object), so Field and Method nodes do not compete. + * With a Field in the bucket, a unique header function became a suffix_match + * that the #725 guard then refused (.h is C++): kernel `put_page(page)` + * 345 -> 9 callers (2026-10-13 graph audit). Dropping only the Fields made + * libc `read()` the unique name of a C++ adapter's Method instead. */ +static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name, + const char *module_qn, const char **import_vals, + int import_count, bool skip_members) { + const char *lookup = simple_name(callee_name); + const qn_array_t *arr = cbm_ht_get(r->by_name, lookup); + if (!arr || arr->count == 0) { + return empty_result(); + } + if (arr->count > REG_MAX_CANDIDATES) { + return empty_result(); /* unresolvably ambiguous — see REG_MAX_CANDIDATES */ + } + if (skip_members) { + qn_array_t view; + char *view_items[REG_MAX_CANDIDATES]; + uint8_t view_test[REG_MAX_CANDIDATES]; + uint8_t view_lang[REG_MAX_CANDIDATES]; + candidates_without_members(r, arr, &view, view_items, view_test, view_lang); + if (view.count == 0) { + return empty_result(); + } + return resolve_name_candidates(r, &view, callee_name, module_qn, import_vals, import_count); + } + return resolve_name_candidates(r, arr, callee_name, module_qn, import_vals, import_count); +} + /* The strategy chain shared by both public resolve variants (no caching here — * cbm_registry_resolve owns the per-file cache). */ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, - size_t scope_len) { + size_t scope_len, bool skip_members) { /* Split callee at the first path separator: "pkg.Func" → prefix="pkg", * suffix="Func". Rust/C++ use "::" rather than ".", so honor whichever * separator appears first ("lib::square" → prefix="lib", suffix="square"). @@ -1761,7 +1816,8 @@ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const ch } if (!(res.qualified_name && res.qualified_name[0])) { /* Strategy 3+4: name lookup */ - res = resolve_name_lookup(r, callee_name, module_qn, import_map_vals, import_map_count); + res = resolve_name_lookup(r, callee_name, module_qn, import_map_vals, import_map_count, + skip_members); } return res; } @@ -1812,7 +1868,7 @@ bool cbm_registry_name_guess_vetoed(const cbm_registry_t *r, CBMLanguage caller_ static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, - size_t scope_len) { + size_t scope_len, bool skip_members) { if (!r || !callee_name) { return empty_result(); } @@ -1822,11 +1878,12 @@ static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const c * cache key only needs callee_name. */ resolve_cache_entry_t *cached = _resolve_cache ? (resolve_cache_entry_t *)cbm_ht_get(_resolve_cache, callee_name) : NULL; - if (cached && cached->scope_len == scope_len) + if (cached && cached->scope_len == scope_len && cached->skip_members == skip_members) return cached->res; - cbm_resolution_t res = registry_resolve_chain(r, callee_name, module_qn, import_map_keys, - import_map_vals, import_map_count, scope_len); + cbm_resolution_t res = + registry_resolve_chain(r, callee_name, module_qn, import_map_keys, import_map_vals, + import_map_count, scope_len, skip_members); /* Data relations (Table/View) are lineage-only registry members: common * table names (users, orders, config) collide with code identifiers across @@ -1846,11 +1903,13 @@ static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const c /* Same key, different effective scope: reuse its owned entry/key. */ cached->res = res; cached->scope_len = scope_len; + cached->skip_members = skip_members; } else if (_resolve_cache) { resolve_cache_entry_t *e = (resolve_cache_entry_t *)malloc(sizeof(*e)); if (e) { e->res = res; e->scope_len = scope_len; + e->skip_members = skip_members; char *kdup = strdup(callee_name); if (kdup) { cbm_ht_set(_resolve_cache, kdup, e); @@ -1866,17 +1925,34 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count) { return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals, - import_map_count, module_qn ? strlen(module_qn) : 0); + import_map_count, module_qn ? strlen(module_qn) : 0, false); +} + +static size_t lang_scope_len(const char *module_qn, CBMLanguage lang) { + return lang == CBM_LANG_PYTHON ? cbm_fqn_symbol_scope_len(module_qn) + : (module_qn ? strlen(module_qn) : 0); } cbm_resolution_t cbm_registry_resolve_lang(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, CBMLanguage lang) { - size_t scope_len = lang == CBM_LANG_PYTHON ? cbm_fqn_symbol_scope_len(module_qn) - : (module_qn ? strlen(module_qn) : 0); return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals, - import_map_count, scope_len); + import_map_count, lang_scope_len(module_qn, lang), false); +} + +cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const char *callee_name, + const char *module_qn, const char **import_map_keys, + const char **import_map_vals, int import_map_count, + CBMLanguage lang) { + /* A bare C call never names a member: a struct member is reached through + * its object (cbm_call_onto_field_policy refuses every Field it lands on) + * and C has no methods. So its Field and Method namesakes do not compete + * for the name; a C member call and every other language keep them. */ + bool skip_members = lang == CBM_LANG_C && callee_name && !strchr(callee_name, '.') && + !strstr(callee_name, "->"); + return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals, + import_map_count, lang_scope_len(module_qn, lang), skip_members); } /* ── Class-qualified route handlers (PHP, #1146) ─────────────────── */ @@ -2044,7 +2120,7 @@ cbm_resolution_t cbm_registry_resolve_lineage(const cbm_registry_t *r, const cha * it would poison one variant with the other's semantics. SQL files hold * few distinct relation refs, so the chain walk stays cheap. */ return registry_resolve_chain(r, callee_name, module_qn, import_map_keys, import_map_vals, - import_map_count, module_qn ? strlen(module_qn) : 0); + import_map_count, module_qn ? strlen(module_qn) : 0, false); } /* ── Fuzzy Resolve ──────────────────────────────────────────────── */ diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index ce5659c72..55a10f507 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8193,6 +8193,156 @@ TEST(pipeline_go_bare_ref_never_binds_field_parallel) { PASS(); } +/* A C caller of a header function that shares its name with a struct member + * elsewhere (kernel: include/linux/mm.h put_page beside the arm64 + * kvm_pgtable_mm_ops.put_page member). pad_files pushes the run past + * MIN_FILES_FOR_PARALLEL for the parallel twin. */ +static void write_c_header_namesake_fixture(const char *tmp, int pad_files) { + write_temp_file(tmp, "include/linux/mm.h", + "struct page;\n" + "static inline void put_page(struct page *page)\n" + "{\n" + "\t(void)page;\n" + "}\n"); + write_temp_file(tmp, "arch/arm64/include/asm/kvm_pgtable.h", + "struct kvm_pgtable_mm_ops {\n" + "\tvoid (*put_page)(void *addr);\n" + "};\n"); + write_temp_file(tmp, "arch/arm64/kernel/elfcore.c", + "#include \n" + "#include \n" + "int mte_dump_tag_range(struct page *page)\n" + "{\n" + "\tput_page(page);\n" + "\treturn 0;\n" + "}\n"); + write_temp_file(tmp, "drivers/net/rx.c", + "#include \n" + "void recv_buffer_free(struct page *page)\n" + "{\n" + "\tput_page(page);\n" + "}\n"); + for (int i = 0; i < pad_files; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "pad/filler%d.c", i); + snprintf(body, sizeof(body), "int filler%d(void) { return %d; }\n", i, i); + write_temp_file(tmp, name, body); + } +} + +/* CALLS edges from the nodes named src_name into nodes named tgt_name that + * carry tgt_label: the name-only probes cannot tell the header Function from + * its struct-member namesake. */ +static int labelled_call_count(cbm_store_t *s, const char *project, const char *src_name, + const char *tgt_name, const char *tgt_label) { + cbm_node_t *srcs = NULL; + cbm_node_t *tgts = NULL; + int sc = 0; + int tc = 0; + int n = 0; + cbm_store_find_nodes_by_name(s, project, src_name, &srcs, &sc); + cbm_store_find_nodes_by_name(s, project, tgt_name, &tgts, &tc); + for (int i = 0; i < sc; i++) { + cbm_edge_t *edges = NULL; + int ec = 0; + cbm_store_find_edges_by_source_type(s, srcs[i].id, "CALLS", &edges, &ec); + for (int j = 0; j < ec; j++) { + for (int k = 0; k < tc; k++) { + if (edges[j].target_id == tgts[k].id && tgts[k].label && + strcmp(tgts[k].label, tgt_label) == 0) { + n++; + } + } + } + if (edges) { + cbm_store_free_edges(edges, ec); + } + } + if (srcs) { + cbm_store_free_nodes(srcs, sc); + } + if (tgts) { + cbm_store_free_nodes(tgts, tc); + } + return n; +} + +TEST(pipeline_c_call_reaches_header_function_beside_field) { + /* 2026-10-13 kernel audit: `put_page(page)` 345 -> 9 callers. The struct + * member in the short-name registry makes the bare name two candidates, + * so the lookup answers by suffix_match; .h is C++ in the extension table + * and the #725 guard dropped the C caller's edge as cross-language. + * Reproduce-first: RED before the fix. Sequential-path twin. */ + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_hdr_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_c_header_namesake_fixture(tmp, 0); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/c_hdr.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); + + /* Both namesakes must exist for the probe to mean anything. */ + ASSERT_GTE(fixture_node_count(s, project, "include/linux/mm.h", "put_page", "Function"), 1); + ASSERT_GTE(fixture_node_count(s, project, "arch/arm64/include/asm/kvm_pgtable.h", "put_page", + "Field"), + 1); + ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Function"), 1); + ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Function"), 1); + /* A bare C call never names the struct member. */ + ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Field"), 0); + ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Field"), 0); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +TEST(pipeline_c_call_reaches_header_function_beside_field_parallel) { + /* Parallel twin: the kernel resolves through pass_parallel.c, which asks + * the same #725 guard. */ + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_hdrp_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_c_header_namesake_fixture(tmp, 52); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/c_hdrp.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, "include/linux/mm.h", "put_page", "Function"), 1); + ASSERT_GTE(fixture_node_count(s, project, "arch/arm64/include/asm/kvm_pgtable.h", "put_page", + "Field"), + 1); + ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Function"), 1); + ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Function"), 1); + ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Field"), 0); + ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Field"), 0); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + /* True when a node named src_name has an edge of edge_type to the Field * `field` OWNED BY `owner` (qualified name ends in ".owner.field"). The * name-only cross_file_edge_exists cannot tell two same-named fields apart, @@ -22443,6 +22593,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel); RUN_TEST(pipeline_go_bare_ref_never_binds_field); RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel); + RUN_TEST(pipeline_c_call_reaches_header_function_beside_field); + RUN_TEST(pipeline_c_call_reaches_header_function_beside_field_parallel); RUN_TEST(pipeline_c_member_access_binds_field_by_object_type); RUN_TEST(pipeline_c_member_access_binds_field_by_object_type_parallel); RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges); diff --git a/tests/test_registry.c b/tests/test_registry.c index 564262f48..45fd6db14 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1259,6 +1259,48 @@ TEST(go_bare_ref_never_binds_field) { PASS(); } +TEST(c_bare_call_does_not_weigh_field_namesakes) { + /* 2026-10-13 kernel audit: a struct member named like a header function + * made the bare name two candidates, the answer a suffix_match, and the + * #725 guard (.h is C++) refused it -- `put_page(page)` 345 -> 9 callers. + * A bare C call never names a member, so its Field namesakes do not + * compete: the function is the unique name it was in v0.11.0. */ + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add_lang(r, "put_page", "proj.include.linux.mm.put_page", "Function", + CBM_LANG_CPP); + cbm_registry_add_lang(r, "put_page", + "proj.arch.arm64.include.asm.kvm_pgtable.kvm_pgtable_mm_ops.put_page", + "Field", CBM_LANG_CPP); + const char *mod = "proj.arch.arm64.kernel.elfcore"; + cbm_resolution_t res = + cbm_registry_resolve_call_lang(r, "put_page", mod, NULL, NULL, 0, CBM_LANG_C); + ASSERT_STR_EQ(res.qualified_name, "proj.include.linux.mm.put_page"); + ASSERT_STR_EQ(res.strategy, "unique_name"); + /* The reference resolver and C++ (which reaches its own members bare) still + * weigh the member: two candidates. */ + res = cbm_registry_resolve_lang(r, "put_page", mod, NULL, NULL, 0, CBM_LANG_C); + ASSERT_STR_EQ(res.strategy, "suffix_match"); + res = cbm_registry_resolve_call_lang(r, "put_page", mod, NULL, NULL, 0, CBM_LANG_CPP); + ASSERT_STR_EQ(res.strategy, "suffix_match"); + /* A name only members carry is no bare C call target at all -- neither a + * struct member nor a C++ method: redis's libc `read()` became the unique + * name of a Qt adapter's `read` Method once only Fields were set aside. */ + cbm_registry_add_lang(r, "on_close", "proj.net.sock.sock_ops.on_close", "Field", CBM_LANG_C); + res = cbm_registry_resolve_call_lang(r, "on_close", mod, NULL, NULL, 0, CBM_LANG_C); + ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); + cbm_registry_add_lang(r, "read", "proj.deps.hiredis.adapters.qt.RedisQtAdapter.read", "Method", + CBM_LANG_CPP); + cbm_registry_add_lang(r, "read", "proj.src.connection.ConnectionType.read", "Field", + CBM_LANG_C); + res = cbm_registry_resolve_call_lang(r, "read", mod, NULL, NULL, 0, CBM_LANG_C); + ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); + /* C++ still reaches the method by name. */ + res = cbm_registry_resolve_call_lang(r, "read", mod, NULL, NULL, 0, CBM_LANG_CPP); + ASSERT_TRUE(res.qualified_name && res.qualified_name[0] != '\0'); + cbm_registry_free(r); + PASS(); +} + TEST(call_onto_field_follows_language_and_shape) { /* A call the registry resolved onto a Field node. */ /* Another language: only the spelling connects them. */ @@ -2012,6 +2054,7 @@ SUITE(registry) { RUN_TEST(cross_language_ref_drops_go_vs_c); RUN_TEST(go_bare_ref_never_binds_field); RUN_TEST(call_onto_field_follows_language_and_shape); + RUN_TEST(c_bare_call_does_not_weigh_field_namesakes); RUN_TEST(c_arrow_member_call_is_c_family_and_arrow_only); RUN_TEST(unique_field_qn_needs_exactly_one_field_of_that_name); RUN_TEST(c_member_access_binds_by_owner_only);