From 25457d2add225b9166e45c926bcdb4a225a5652f Mon Sep 17 00:00:00 2001 From: sahil-mangla Date: Thu, 16 Jul 2026 22:27:04 +0530 Subject: [PATCH 1/2] fix(registry): restrict same-module suffix resolution to self/namespace receivers --- internal/cbm/extract_defs.c | 90 +++++++++++++--------- src/pipeline/registry.c | 149 ++++++++++++++++++++++++++++++------ tests/test_extraction.c | 18 +++++ tests/test_registry.c | 31 ++++++++ 4 files changed, 230 insertions(+), 58 deletions(-) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 05db1bac..1e386c50 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -3554,8 +3554,9 @@ 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"); } // ObjectScript UDL: class name is a `class_name` child (no "name" field). @@ -3969,77 +3970,93 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec // Find the body/members node inside a class node static TSNode find_class_body(TSNode class_node, CBMLanguage lang) { + TSNode result = {0}; // Try field names first static const char *body_fields[] = {"body", "members", "class_body", "declaration_list", NULL}; 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)) { - return body; + result = body; + break; } } // Go: type_spec -> type field (interface_type or struct_type) - if (lang == CBM_LANG_GO) { + if (ts_node_is_null(result) && lang == CBM_LANG_GO) { TSNode type_inner = ts_node_child_by_field_name(class_node, TS_FIELD("type")); if (!ts_node_is_null(type_inner)) { - return type_inner; + result = type_inner; } } // ObjC: class_implementation/class_interface has no single body node // Methods are inside implementation_definition children directly - if (lang == CBM_LANG_OBJC) { + if (ts_node_is_null(result) && lang == CBM_LANG_OBJC) { return class_node; // iterate children of the class node itself } // Squirrel: class_declaration has no body field — member_declaration nodes // (each wrapping a function_declaration) are direct children of the class. - if (lang == CBM_LANG_SQUIRREL) { + if (ts_node_is_null(result) && lang == CBM_LANG_SQUIRREL) { return class_node; } // Smali: field_definition nodes are direct children of class_definition (no // dedicated body node) — iterate the class node itself. - if (lang == CBM_LANG_SMALI) { + if (ts_node_is_null(result) && lang == CBM_LANG_SMALI) { return class_node; } // GraphQL: object/interface fields live in a fields_definition child. - if (lang == CBM_LANG_GRAPHQL) { + if (ts_node_is_null(result) && lang == CBM_LANG_GRAPHQL) { TSNode b = cbm_find_child_by_kind(class_node, "fields_definition"); if (!ts_node_is_null(b)) { - return b; + result = b; } } // Prisma: model columns live in a statement_block child. Gated to Prisma so - // the common "statement_block" kind can never hijack another language's - // class body via the generic fallback below. - if (lang == CBM_LANG_PRISMA) { + // general fallback behavior is unchanged. + if (ts_node_is_null(result) && lang == CBM_LANG_PRISMA) { TSNode b = cbm_find_child_by_kind(class_node, "statement_block"); if (!ts_node_is_null(b)) { - return b; + result = b; } } // Fallback: search children for known body node types - static const char *body_types[] = {"class_body", - "interface_body", - "enum_body", - "template_body", - "interface_type", - "struct_type", - "field_declaration_list", - "compound_statement", - "block", - "closure", - "implementation_definition", - NULL}; - uint32_t count = ts_node_child_count(class_node); - 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; + if (ts_node_is_null(result)) { + static const char *body_types[] = {"class_body", + "interface_body", + "enum_body", + "template_body", + "interface_type", + "struct_type", + "field_declaration_list", + "compound_statement", + "block", + "closure", + "implementation_definition", + NULL}; + uint32_t count = ts_node_child_count(class_node); + 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) { + 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. @@ -6200,7 +6217,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, "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 diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index cf1b3f00..3e33d3c6 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -673,9 +673,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); @@ -683,10 +708,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(); @@ -778,6 +806,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, @@ -792,36 +880,53 @@ 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, @@ -874,7 +979,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_extraction.c b/tests/test_extraction.c index 04431382..51288aed 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -418,6 +418,23 @@ TEST(java_class_extends_and_implements) { PASS(); } +TEST(java_enum_method) { + CBMFileResult *r = extract( + "enum Day {\n" + " MON, TUE, WED, THU, FRI, SAT, SUN;\n\n" + " public boolean isWeekend() { return this == SAT || this == SUN; }\n" + " public String label() { return name().toLowerCase(); }\n" + "}\n", + CBM_LANG_JAVA, "t", "Day.java"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Enum", "Day")); + ASSERT(has_def(r, "Method", "isWeekend")); + ASSERT(has_def(r, "Method", "label")); + cbm_free_result(r); + PASS(); +} + /* REPRODUCTION (RED until fixed) — Python `class Animal(Base):` must extract the * BARE base name "Base", but extract_base_classes captures the whole * `superclasses` argument_list text "(Base)" instead: collect_bases_from_field @@ -4429,6 +4446,7 @@ SUITE(extraction) { RUN_TEST(java_method); RUN_TEST(java_interface); RUN_TEST(java_class_extends_and_implements); + RUN_TEST(java_enum_method); RUN_TEST(python_class_base_extracted_bare); RUN_TEST(php_class); RUN_TEST(php_function); diff --git a/tests/test_registry.c b/tests/test_registry.c index 725a6f93..0640f974 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 @@ -855,6 +885,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 0b2211271f76d690aaa5004efa9f929b889f202e Mon Sep 17 00:00:00 2001 From: sahil-mangla Date: Fri, 17 Jul 2026 00:30:33 +0530 Subject: [PATCH 2/2] fix(registry): refine qualified receiver checks for methods and add unit tests --- src/pipeline/registry.c | 91 +++++++++++++++++++++++++++++++++++++---- tests/test_registry.c | 68 ++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 3e33d3c6..7ef19df4 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -832,13 +832,61 @@ static bool qn_ends_with_qualified(const char *qn, const char *callee_name) { } return true; } -static bool is_type_like_label(const char *label) { +static bool is_type_or_module_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; + return cbm_label_is_type_like(label) || strcmp(label, "Module") == 0 || strcmp(label, "Package") == 0; +} + +static bool is_known_type_or_namespace(const cbm_registry_t *r, const char *prefix) { + if (!prefix || !prefix[0]) { + return false; + } + /* 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 false; + } + } + + /* 2. Check if prefix exists as a qualified name with a type-like or module/package label */ + const char *label = cbm_registry_label_of(r, prefix); + if (label && is_type_or_module_label(label)) { + return true; + } + + /* 3. If it's a simple name, check if any registered definition with this simple name has a type-like/module label */ + const char **items = NULL; + int count = 0; + if (cbm_registry_find_by_name(r, prefix, &items, &count) == 0 && count > 0) { + for (int i = 0; i < count; i++) { + const char *item_label = cbm_registry_label_of(r, items[i]); + if (item_label && is_type_or_module_label(item_label)) { + return true; + } + } + } + return false; +} + +static bool is_imported_receiver(const char *prefix, const char **import_keys, int import_count) { + if (!prefix || !prefix[0] || !import_keys || import_count <= 0) { + return false; + } + if (_import_map_cache) { + if (cbm_ht_get(_import_map_cache, prefix)) { + return true; + } + } else { + for (int i = 0; i < import_count; i++) { + if (import_keys[i] && strcmp(import_keys[i], prefix) == 0) { + return true; + } + } + } + return false; } static bool is_candidate_method(const cbm_registry_t *r, const char *qn) { @@ -860,7 +908,7 @@ static bool is_candidate_method(const cbm_registry_t *r, const char *qn) { *last_dot = '\0'; const char *parent_label = cbm_registry_label_of(r, parent_qn); - return is_type_like_label(parent_label); + return cbm_label_is_type_like(parent_label); } static bool is_qualified_callee(const char *callee_name) { @@ -869,8 +917,8 @@ static bool is_qualified_callee(const char *callee_name) { /* 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 *module_qn, const char **import_keys, + 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) { @@ -919,7 +967,32 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char } if (res.qualified_name && is_qualified_callee(callee_name)) { - if (!is_candidate_method(r, res.qualified_name)) { + char prefix[CBM_SZ_256] = {0}; + const char *dot = strchr(callee_name, '.'); + const char *colons = strstr(callee_name, "::"); + const char *sep = dot; + if (colons && (!sep || colons < sep)) { + sep = colons; + } + if (sep) { + size_t plen = sep - callee_name; + if (plen >= sizeof(prefix)) { + plen = sizeof(prefix) - 1; + } + memcpy(prefix, callee_name, plen); + prefix[plen] = '\0'; + } + + bool is_method = is_candidate_method(r, res.qualified_name); + bool enforce_qualified = !is_method; + + if (is_method) { + if (is_known_type_or_namespace(r, prefix) || is_imported_receiver(prefix, import_keys, import_count)) { + enforce_qualified = true; + } + } + + if (enforce_qualified) { if (!qn_ends_with_qualified(res.qualified_name, callee_name)) { return empty_result(); } @@ -983,7 +1056,7 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle } 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_keys, import_map_vals, import_map_count); } /* Cache the result (including empty — caching the negative answer diff --git a/tests/test_registry.c b/tests/test_registry.c index 0640f974..c7ed65d0 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -857,9 +857,76 @@ TEST(resolve_import_map_alias_with_suffix_hits_method) { PASS(); } +TEST(resolve_qualified_receiver_awareness) { + cbm_registry_t *r = cbm_registry_new(); + + /* 1. Register a Method candidate under Class Service: proj.pkg.Service.get */ + cbm_registry_add(r, "Service", "proj.pkg.Service", "Class"); + cbm_registry_add(r, "get", "proj.pkg.Service.get", "Method"); + + /* 2. Register a Module axios */ + cbm_registry_add(r, "axios", "proj.axios", "Module"); + + /* Test Case A: Unrelated receiver that is a known module/type -> should NOT resolve */ + /* axios.get called, axios is a known Module */ + { + cbm_resolution_t res = cbm_registry_resolve(r, "axios.get", "proj.app", NULL, NULL, 0); + ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); + } + + /* Test Case B: Unrelated receiver that is an imported receiver -> should NOT resolve */ + /* other_service.get called, other_service is an imported alias/key mapping to OtherService */ + { + const char *keys[] = {"other_service"}; + const char *vals[] = {"proj.pkg.OtherService"}; + cbm_resolution_t res = cbm_registry_resolve(r, "other_service.get", "proj.app", keys, vals, 1); + ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); + } + + /* Test Case B2: Related receiver that is an imported receiver -> should resolve */ + /* my_service.get called, my_service is an imported alias/key mapping to Service */ + { + const char *keys[] = {"my_service"}; + const char *vals[] = {"proj.pkg.Service"}; + cbm_resolution_t res = cbm_registry_resolve(r, "my_service.get", "proj.app", keys, vals, 1); + ASSERT_STR_EQ(res.qualified_name, "proj.pkg.Service.get"); + } + + /* Test Case C: Receiver that is an unregistered/instance variable (e.g. "d.get") */ + /* should resolve to Method, matching the Java enum case where receiver is just an instance */ + { + cbm_resolution_t res = cbm_registry_resolve(r, "d.get", "proj.app", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.pkg.Service.get"); + } + + /* Test Case D: Fully qualified receiver matching the candidate's parent path -> should resolve */ + { + cbm_resolution_t res = cbm_registry_resolve(r, "Service.get", "proj.app", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.pkg.Service.get"); + } + + /* Test Case E: Double colon C++/Perl style qualification (e.g. Service::get) -> should resolve */ + { + cbm_resolution_t res = cbm_registry_resolve(r, "Service::get", "proj.app", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.pkg.Service.get"); + } + + /* Test Case F: If the candidate is a Function (not a Method), we always enforce qualified suffix match */ + /* Register unique Function helper.compute */ + cbm_registry_add(r, "compute", "proj.pkg.helper.compute", "Function"); + { + cbm_resolution_t res = cbm_registry_resolve(r, "unrelated.compute", "proj.app", NULL, NULL, 0); + ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); + } + + cbm_registry_free(r); + PASS(); +} + SUITE(registry) { /* FQN */ RUN_TEST(fqn_simple); + RUN_TEST(fqn_no_name); RUN_TEST(fqn_python_init); RUN_TEST(fqn_js_index); @@ -893,6 +960,7 @@ SUITE(registry) { RUN_TEST(resolve_import_map_bare_alias); RUN_TEST(resolve_import_map_alias_with_suffix_hits_method); RUN_TEST(resolve_unique_name); + RUN_TEST(resolve_qualified_receiver_awareness); RUN_TEST(resolve_unresolved); RUN_TEST(resolve_many_nodes); /* Confidence band */