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 <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-13 18:33:39 +02:00
1 parent bb636d1ff7
commit 55f1b5fe90
6 files changed
+305 -27

No files matched your search

+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+7
View File
@@ -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
+99 -23
View File
@@ -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 ──────────────────────────────────────────────── */
+152
View File
@@ -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 <linux/mm.h>\n"
"#include <asm/kvm_pgtable.h>\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 <linux/mm.h>\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);
+43
View File
@@ -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);