From 252cfdc973c4d806e656bf4f54fc8ceb9b7fccbb Mon Sep 17 00:00:00 2001 From: sahil-mangla Date: Mon, 6 Jul 2026 00:28:37 +0530 Subject: [PATCH 1/4] fix(registry): restrict same-module and unique-name suffix matches on receivers Limit same-module suffix fallback in resolve_same_module to only fire if the receiver is a self-receiver or matches the module/namespace. Also reject matching dotted/colon-qualified callees targeting a Function to a different prefix in name lookup. This prevents false-positive recursion flags on store/delegation calls. Signed-off-by: sahil-mangla --- src/pipeline/registry.c | 148 ++++++++++++++++++++++++++++++++++------ tests/test_registry.c | 31 +++++++++ 2 files changed, 157 insertions(+), 22 deletions(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 4b70f67d..b41f2a01 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -659,9 +659,34 @@ static cbm_resolution_t resolve_import_map(const cbm_registry_t *r, const char * return empty_result(); } +static bool is_same_module_receiver(const char *prefix, const char *module_qn) { + if (!prefix || !prefix[0]) { + return true; /* Bare names are always candidates for same-module resolution */ + } + /* 1. Check known self-receivers */ + static const char *const self_receivers[] = {"self", "this", "cls", "@self", NULL}; + for (int i = 0; self_receivers[i]; i++) { + if (strcmp(prefix, self_receivers[i]) == 0) { + return true; + } + } + /* 2. Check if prefix matches the module name or its last segment (namespace qualified) */ + size_t plen = strlen(prefix); + size_t mlen = strlen(module_qn); + if (mlen == plen && strcmp(module_qn, prefix) == 0) { + return true; + } + if (mlen > plen && module_qn[mlen - plen - 1] == '.' && + strcmp(module_qn + (mlen - plen), prefix) == 0) { + return true; + } + return false; +} + /* Strategy 2: Same-module match */ static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char *callee_name, - const char *suffix, const char *module_qn) { + const char *prefix, const char *suffix, + const char *module_qn) { char candidate[CBM_SZ_512]; snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, callee_name); const char *stored_key = cbm_ht_get_key(r->exact, candidate); @@ -669,10 +694,13 @@ static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED}; } if (suffix && suffix[0]) { - snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix); - stored_key = cbm_ht_get_key(r->exact, candidate); - if (stored_key) { - return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED}; + if (is_same_module_receiver(prefix, module_qn)) { + snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix); + stored_key = cbm_ht_get_key(r->exact, candidate); + if (stored_key) { + return (cbm_resolution_t){ + stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED}; + } } } return empty_result(); @@ -764,6 +792,66 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal } return match; } +static bool qn_ends_with_qualified(const char *qn, const char *callee_name) { + char dotted[CBM_SZ_512]; + size_t w = 0; + for (const char *s = callee_name; *s && w + 1 < sizeof(dotted);) { + if (s[0] == ':' && s[1] == ':') { + dotted[w++] = '.'; + s += 2; + } else { + dotted[w++] = *s++; + } + } + dotted[w] = '\0'; + + size_t qlen = strlen(qn); + if (qlen < w) { + return false; + } + const char *tail = qn + (qlen - w); + if (strcmp(tail, dotted) != 0) { + return false; + } + if (tail != qn && tail[-1] != '.') { + return false; + } + return true; +} +static bool is_type_like_label(const char *label) { + if (!label) { + return false; + } + return strcmp(label, "Class") == 0 || strcmp(label, "Struct") == 0 || + strcmp(label, "Interface") == 0 || strcmp(label, "Enum") == 0 || + strcmp(label, "Type") == 0 || strcmp(label, "Trait") == 0; +} + +static bool is_candidate_method(const cbm_registry_t *r, const char *qn) { + const char *label = cbm_registry_label_of(r, qn); + if (label && strcmp(label, "Method") == 0) { + return true; + } + + char parent_qn[CBM_SZ_512]; + size_t len = strlen(qn); + if (len >= sizeof(parent_qn)) { + return false; + } + strcpy(parent_qn, qn); + char *last_dot = strrchr(parent_qn, '.'); + if (!last_dot) { + return false; + } + *last_dot = '\0'; + + const char *parent_label = cbm_registry_label_of(r, parent_qn); + return is_type_like_label(parent_label); +} + +static bool is_qualified_callee(const char *callee_name) { + return strchr(callee_name, '.') != NULL || strstr(callee_name, "::") != NULL; +} /* Strategy 3+4: Name lookup + suffix match */ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name, @@ -778,36 +866,52 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char return empty_result(); /* unresolvably ambiguous — see REG_MAX_CANDIDATES */ } + cbm_resolution_t res = empty_result(); + /* 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. */ if (arr->count > 1) { const char *q = qualified_suffix_match(arr, callee_name); if (q) { - return (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED}; + res = (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED}; } } - /* Strategy 3: unique name */ - if (arr->count == SKIP_ONE) { - double conf = CONF_UNIQUE_NAME; - if (import_vals && import_count > 0 && - !is_import_reachable(arr->items[0], import_vals, import_count)) { - conf *= DEFAULT_CONFIDENCE; + if (!(res.qualified_name && res.qualified_name[0])) { + /* Strategy 3: unique name */ + if (arr->count == 1) { + double conf = CONF_UNIQUE_NAME; + if (import_vals && import_count > 0 && + !is_import_reachable(arr->items[0], import_vals, import_count)) { + conf *= DEFAULT_CONFIDENCE; + } + res = (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED}; } - return (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED}; } - /* Strategy 4: multiple candidates */ - if (import_vals && import_count > 0) { - return resolve_multi_with_imports(arr, module_qn, import_vals, import_count); + if (!(res.qualified_name && res.qualified_name[0])) { + /* Strategy 4: multiple candidates */ + if (import_vals && import_count > 0) { + res = resolve_multi_with_imports(arr, module_qn, import_vals, import_count); + } else { + const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn); + if (best) { + double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); + res = (cbm_resolution_t){best, "suffix_match", conf, arr->count}; + } + } } - const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn); - if (best) { - double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); - return (cbm_resolution_t){best, "suffix_match", conf, arr->count}; + + if (res.qualified_name && is_qualified_callee(callee_name)) { + if (!is_candidate_method(r, res.qualified_name)) { + if (!qn_ends_with_qualified(res.qualified_name, callee_name)) { + return empty_result(); + } + } } - return empty_result(); + + return res; } cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *callee_name, @@ -860,7 +964,7 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle resolve_import_map(r, prefix, suffix, import_map_keys, import_map_vals, import_map_count); if (!(res.qualified_name && res.qualified_name[0])) { /* Strategy 2: same module */ - res = resolve_same_module(r, callee_name, suffix, module_qn); + res = resolve_same_module(r, callee_name, prefix, suffix, module_qn); } if (!(res.qualified_name && res.qualified_name[0])) { /* Strategy 3+4: name lookup */ diff --git a/tests/test_registry.c b/tests/test_registry.c index a79aaffd..9cac774a 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -264,6 +264,36 @@ TEST(resolve_same_module) { PASS(); } +TEST(resolve_same_module_only_on_self_receiver) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "get", "proj.pkg.service.get", "Function"); + + /* Dotted call with unrelated receiver (e.g. "axios.get") -> should NOT resolve */ + cbm_resolution_t res1 = cbm_registry_resolve(r, "axios.get", "proj.pkg.service", NULL, NULL, 0); + ASSERT_TRUE(res1.qualified_name == NULL || res1.qualified_name[0] == '\0'); + + /* Dotted call with delegation pattern (e.g. "_get_store().get") -> should NOT resolve */ + cbm_resolution_t res2 = cbm_registry_resolve(r, "_get_store().get", "proj.pkg.service", NULL, NULL, 0); + ASSERT_TRUE(res2.qualified_name == NULL || res2.qualified_name[0] == '\0'); + + /* Dotted call with self-receiver (e.g. "self.get") -> should resolve */ + cbm_resolution_t res3 = cbm_registry_resolve(r, "self.get", "proj.pkg.service", NULL, NULL, 0); + ASSERT_STR_EQ(res3.qualified_name, "proj.pkg.service.get"); + ASSERT_STR_EQ(res3.strategy, "same_module"); + + /* Dotted call with exact module name prefix (e.g. "proj.pkg.service.get") -> should resolve */ + cbm_resolution_t res4 = cbm_registry_resolve(r, "proj.pkg.service.get", "proj.pkg.service", NULL, NULL, 0); + ASSERT_STR_EQ(res4.qualified_name, "proj.pkg.service.get"); + + /* Dotted call with namespace/module last segment (e.g. "service.get") -> should resolve */ + cbm_resolution_t res5 = cbm_registry_resolve(r, "service.get", "proj.pkg.service", NULL, NULL, 0); + ASSERT_STR_EQ(res5.qualified_name, "proj.pkg.service.get"); + ASSERT_STR_EQ(res5.strategy, "same_module"); + + cbm_registry_free(r); + PASS(); +} + /* A package/namespace-qualified callee whose bare name is defined in several * places must resolve to the package named in the call — not collapse onto a * single winner. Regression for qualified cross-file calls (e.g. Perl @@ -814,6 +844,7 @@ SUITE(registry) { RUN_TEST(registry_no_duplicates); /* Resolution */ RUN_TEST(resolve_same_module); + RUN_TEST(resolve_same_module_only_on_self_receiver); RUN_TEST(resolve_qualified_disambiguates_same_name); RUN_TEST(resolve_qualified_ambiguous_tail_falls_through); RUN_TEST(resolve_import_map); From 9ac180d8f38d7aec7473708f2d2372447281eef9 Mon Sep 17 00:00:00 2001 From: sahil-mangla Date: Mon, 6 Jul 2026 16:03:30 +0530 Subject: [PATCH 2/4] style(registry): fix clang-format violations in registry.c Signed-off-by: sahil-mangla --- src/pipeline/registry.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index b41f2a01..5329faac 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -698,8 +698,8 @@ static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix); stored_key = cbm_ht_get_key(r->exact, candidate); if (stored_key) { - return (cbm_resolution_t){ - stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED}; + return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, + REG_RESOLVED}; } } } @@ -895,7 +895,8 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char if (import_vals && import_count > 0) { res = resolve_multi_with_imports(arr, module_qn, import_vals, import_count); } else { - const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn); + const char *best = + best_by_import_distance((const char **)arr->items, arr->count, module_qn); if (best) { double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); res = (cbm_resolution_t){best, "suffix_match", conf, arr->count}; From 6ab8a8405a31ac25c9cd4de3d23ac83a68529aa6 Mon Sep 17 00:00:00 2001 From: sahil-mangla Date: Wed, 8 Jul 2026 16:57:51 +0530 Subject: [PATCH 3/4] fix(java-enum): resolve enum methods from enum_body_declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Java enums with methods (e.g. isWeekend/label in cp_enum_method_java) have their method declarations inside enum_body_declarations, which is a child of enum_body — not a direct child of enum_body itself. find_class_body was returning the raw enum_body node (16 children: enum_constant×7, commas, and the nested enum_body_declarations). As a result, extract_class_methods iterated enum_constant/comma nodes and never visited any method_declaration, so no Method nodes were created and no CALLS edges resolved. Fixes: - find_class_body: if a body resolved via field-name lookup is enum_body, look for enum_body_declarations inside it and return that instead. - find_class_body fallback loop: same unwrapping for the type-scan path. - push_class_body_children: add enum_body and enum_body_declarations to the recognised body-container kinds so the walk_defs stack correctly scopes enum method children under the enum QN. Tests: convergence_probe 47/47 passed (was 46/47, cp_enum_method_java was the failing case). Signed-off-by: sahil-mangla --- internal/cbm/extract_defs.c | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 2de63ffb..7efc8115 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -3502,10 +3502,11 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec } TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); - // ObjC: class name is first identifier child - if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) { + // ObjC/Java: class name is first identifier child (Java fallback for enum_declaration) + if (ts_node_is_null(name_node) && (ctx->language == CBM_LANG_OBJC || ctx->language == CBM_LANG_JAVA)) { name_node = cbm_find_child_by_kind(node, "identifier"); } + // Swift and newer tree-sitter-kotlin: class/object name is a type_identifier // child (no "name" field). if (ts_node_is_null(name_node) && @@ -3918,6 +3919,12 @@ static TSNode find_class_body(TSNode class_node, CBMLanguage lang) { for (const char **f = body_fields; *f; f++) { TSNode body = ts_node_child_by_field_name(class_node, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(body)) { + if (strcmp(ts_node_type(body), "enum_body") == 0) { + TSNode decls = cbm_find_child_by_kind(body, "enum_body_declarations"); + if (!ts_node_is_null(decls)) { + return decls; + } + } return body; } } @@ -3973,17 +3980,31 @@ static TSNode find_class_body(TSNode class_node, CBMLanguage lang) { "implementation_definition", NULL}; uint32_t count = ts_node_child_count(class_node); + TSNode result = {0}; for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_child(class_node, i); const char *ck = ts_node_type(child); for (const char **t = body_types; *t; t++) { if (strcmp(ck, *t) == 0) { - return child; + result = child; + break; } } + if (!ts_node_is_null(result)) { + break; + } } - TSNode null_node = {0}; - return null_node; + if (!ts_node_is_null(result) && strcmp(ts_node_type(result), "enum_body") == 0) { + TSNode decls = cbm_find_child_by_kind(result, "enum_body_declarations"); + if (!ts_node_is_null(decls)) { + return decls; + } + } + if (ts_node_is_null(result)) { + TSNode null_node = {0}; + return null_node; + } + return result; } // Dart: resolve method name from method_signature/function_signature. @@ -5837,6 +5858,7 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 || strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 || strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 || + strcmp(ck, "enum_body") == 0 || strcmp(ck, "enum_body_declarations") == 0 || // Groovy class bodies are a `closure` node; routing through the // nested-class path keeps methods from being re-walked (and thus // double-extracted) as top-level functions. Gated to Groovy so other From 992265172636826a5172e3e83995ec9f90ea5791 Mon Sep 17 00:00:00 2001 From: sahil-mangla Date: Wed, 8 Jul 2026 17:35:55 +0530 Subject: [PATCH 4/4] fix: clang-format violations in extract_defs.c Wrap two if-conditions that exceeded the 100-column limit: - Line 3506: break &&-condition before the parenthesised ObjC/Java language check so the line stays within 100 cols. - Lines 5860-5861: split the combined enum_body / enum_body_declarations comparison onto separate continuation lines to satisfy the limit. No logic change; formatting only. Signed-off-by: sahil-mangla --- internal/cbm/extract_defs.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 7efc8115..e2685ebf 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -3503,7 +3503,8 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); // ObjC/Java: class name is first identifier child (Java fallback for enum_declaration) - if (ts_node_is_null(name_node) && (ctx->language == CBM_LANG_OBJC || ctx->language == CBM_LANG_JAVA)) { + if (ts_node_is_null(name_node) && + (ctx->language == CBM_LANG_OBJC || ctx->language == CBM_LANG_JAVA)) { name_node = cbm_find_child_by_kind(node, "identifier"); } @@ -5857,8 +5858,8 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st const char *ck = ts_node_type(child); if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 || strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 || - strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 || - strcmp(ck, "enum_body") == 0 || strcmp(ck, "enum_body_declarations") == 0 || + strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 || strcmp(ck, "enum_body") == 0 || + strcmp(ck, "enum_body_declarations") == 0 || // Groovy class bodies are a `closure` node; routing through the // nested-class path keeps methods from being re-walked (and thus // double-extracted) as top-level functions. Gated to Groovy so other