Skip to content

Commit 0045c9b

Browse files
committed
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 <manglasahil2017@gmail.com>
1 parent 00b2a0e commit 0045c9b

2 files changed

Lines changed: 157 additions & 22 deletions

File tree

src/pipeline/registry.c

Lines changed: 126 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -605,20 +605,48 @@ static cbm_resolution_t resolve_import_map(const cbm_registry_t *r, const char *
605605
return empty_result();
606606
}
607607

608+
static bool is_same_module_receiver(const char *prefix, const char *module_qn) {
609+
if (!prefix || !prefix[0]) {
610+
return true; /* Bare names are always candidates for same-module resolution */
611+
}
612+
/* 1. Check known self-receivers */
613+
static const char *const self_receivers[] = {"self", "this", "cls", "@self", NULL};
614+
for (int i = 0; self_receivers[i]; i++) {
615+
if (strcmp(prefix, self_receivers[i]) == 0) {
616+
return true;
617+
}
618+
}
619+
/* 2. Check if prefix matches the module name or its last segment (namespace qualified) */
620+
size_t plen = strlen(prefix);
621+
size_t mlen = strlen(module_qn);
622+
if (mlen == plen && strcmp(module_qn, prefix) == 0) {
623+
return true;
624+
}
625+
if (mlen > plen && module_qn[mlen - plen - 1] == '.' &&
626+
strcmp(module_qn + (mlen - plen), prefix) == 0) {
627+
return true;
628+
}
629+
return false;
630+
}
631+
608632
/* Strategy 2: Same-module match */
609633
static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char *callee_name,
610-
const char *suffix, const char *module_qn) {
634+
const char *prefix, const char *suffix,
635+
const char *module_qn) {
611636
char candidate[CBM_SZ_512];
612637
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, callee_name);
613638
const char *stored_key = cbm_ht_get_key(r->exact, candidate);
614639
if (stored_key) {
615640
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
616641
}
617642
if (suffix && suffix[0]) {
618-
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix);
619-
stored_key = cbm_ht_get_key(r->exact, candidate);
620-
if (stored_key) {
621-
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
643+
if (is_same_module_receiver(prefix, module_qn)) {
644+
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix);
645+
stored_key = cbm_ht_get_key(r->exact, candidate);
646+
if (stored_key) {
647+
return (cbm_resolution_t){
648+
stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
649+
}
622650
}
623651
}
624652
return empty_result();
@@ -710,6 +738,66 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
710738
}
711739
return match;
712740
}
741+
static bool qn_ends_with_qualified(const char *qn, const char *callee_name) {
742+
char dotted[CBM_SZ_512];
743+
size_t w = 0;
744+
for (const char *s = callee_name; *s && w + 1 < sizeof(dotted);) {
745+
if (s[0] == ':' && s[1] == ':') {
746+
dotted[w++] = '.';
747+
s += 2;
748+
} else {
749+
dotted[w++] = *s++;
750+
}
751+
}
752+
dotted[w] = '\0';
753+
754+
size_t qlen = strlen(qn);
755+
if (qlen < w) {
756+
return false;
757+
}
758+
const char *tail = qn + (qlen - w);
759+
if (strcmp(tail, dotted) != 0) {
760+
return false;
761+
}
762+
if (tail != qn && tail[-1] != '.') {
763+
return false;
764+
}
765+
return true;
766+
}
767+
static bool is_type_like_label(const char *label) {
768+
if (!label) {
769+
return false;
770+
}
771+
return strcmp(label, "Class") == 0 || strcmp(label, "Struct") == 0 ||
772+
strcmp(label, "Interface") == 0 || strcmp(label, "Enum") == 0 ||
773+
strcmp(label, "Type") == 0 || strcmp(label, "Trait") == 0;
774+
}
775+
776+
static bool is_candidate_method(const cbm_registry_t *r, const char *qn) {
777+
const char *label = cbm_registry_label_of(r, qn);
778+
if (label && strcmp(label, "Method") == 0) {
779+
return true;
780+
}
781+
782+
char parent_qn[CBM_SZ_512];
783+
size_t len = strlen(qn);
784+
if (len >= sizeof(parent_qn)) {
785+
return false;
786+
}
787+
strcpy(parent_qn, qn);
788+
char *last_dot = strrchr(parent_qn, '.');
789+
if (!last_dot) {
790+
return false;
791+
}
792+
*last_dot = '\0';
793+
794+
const char *parent_label = cbm_registry_label_of(r, parent_qn);
795+
return is_type_like_label(parent_label);
796+
}
797+
798+
static bool is_qualified_callee(const char *callee_name) {
799+
return strchr(callee_name, '.') != NULL || strstr(callee_name, "::") != NULL;
800+
}
713801

714802
/* Strategy 3+4: Name lookup + suffix match */
715803
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
@@ -724,36 +812,52 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
724812
return empty_result(); /* unresolvably ambiguous — see REG_MAX_CANDIDATES */
725813
}
726814

815+
cbm_resolution_t res = empty_result();
816+
727817
/* Strategy 3.5: a qualified callee disambiguates among multiple same-name
728818
* candidates by full qualified tail, before bare-name scoring collapses
729819
* them onto a single winner. */
730820
if (arr->count > 1) {
731821
const char *q = qualified_suffix_match(arr, callee_name);
732822
if (q) {
733-
return (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED};
823+
res = (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED};
734824
}
735825
}
736826

737-
/* Strategy 3: unique name */
738-
if (arr->count == SKIP_ONE) {
739-
double conf = CONF_UNIQUE_NAME;
740-
if (import_vals && import_count > 0 &&
741-
!is_import_reachable(arr->items[0], import_vals, import_count)) {
742-
conf *= DEFAULT_CONFIDENCE;
827+
if (!(res.qualified_name && res.qualified_name[0])) {
828+
/* Strategy 3: unique name */
829+
if (arr->count == 1) {
830+
double conf = CONF_UNIQUE_NAME;
831+
if (import_vals && import_count > 0 &&
832+
!is_import_reachable(arr->items[0], import_vals, import_count)) {
833+
conf *= DEFAULT_CONFIDENCE;
834+
}
835+
res = (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED};
743836
}
744-
return (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED};
745837
}
746838

747-
/* Strategy 4: multiple candidates */
748-
if (import_vals && import_count > 0) {
749-
return resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
839+
if (!(res.qualified_name && res.qualified_name[0])) {
840+
/* Strategy 4: multiple candidates */
841+
if (import_vals && import_count > 0) {
842+
res = resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
843+
} else {
844+
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
845+
if (best) {
846+
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
847+
res = (cbm_resolution_t){best, "suffix_match", conf, arr->count};
848+
}
849+
}
750850
}
751-
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
752-
if (best) {
753-
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
754-
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};
851+
852+
if (res.qualified_name && is_qualified_callee(callee_name)) {
853+
if (!is_candidate_method(r, res.qualified_name)) {
854+
if (!qn_ends_with_qualified(res.qualified_name, callee_name)) {
855+
return empty_result();
856+
}
857+
}
755858
}
756-
return empty_result();
859+
860+
return res;
757861
}
758862

759863
cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *callee_name,
@@ -806,7 +910,7 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle
806910
resolve_import_map(r, prefix, suffix, import_map_keys, import_map_vals, import_map_count);
807911
if (!(res.qualified_name && res.qualified_name[0])) {
808912
/* Strategy 2: same module */
809-
res = resolve_same_module(r, callee_name, suffix, module_qn);
913+
res = resolve_same_module(r, callee_name, prefix, suffix, module_qn);
810914
}
811915
if (!(res.qualified_name && res.qualified_name[0])) {
812916
/* Strategy 3+4: name lookup */

tests/test_registry.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,36 @@ TEST(resolve_same_module) {
264264
PASS();
265265
}
266266

267+
TEST(resolve_same_module_only_on_self_receiver) {
268+
cbm_registry_t *r = cbm_registry_new();
269+
cbm_registry_add(r, "get", "proj.pkg.service.get", "Function");
270+
271+
/* Dotted call with unrelated receiver (e.g. "axios.get") -> should NOT resolve */
272+
cbm_resolution_t res1 = cbm_registry_resolve(r, "axios.get", "proj.pkg.service", NULL, NULL, 0);
273+
ASSERT_TRUE(res1.qualified_name == NULL || res1.qualified_name[0] == '\0');
274+
275+
/* Dotted call with delegation pattern (e.g. "_get_store().get") -> should NOT resolve */
276+
cbm_resolution_t res2 = cbm_registry_resolve(r, "_get_store().get", "proj.pkg.service", NULL, NULL, 0);
277+
ASSERT_TRUE(res2.qualified_name == NULL || res2.qualified_name[0] == '\0');
278+
279+
/* Dotted call with self-receiver (e.g. "self.get") -> should resolve */
280+
cbm_resolution_t res3 = cbm_registry_resolve(r, "self.get", "proj.pkg.service", NULL, NULL, 0);
281+
ASSERT_STR_EQ(res3.qualified_name, "proj.pkg.service.get");
282+
ASSERT_STR_EQ(res3.strategy, "same_module");
283+
284+
/* Dotted call with exact module name prefix (e.g. "proj.pkg.service.get") -> should resolve */
285+
cbm_resolution_t res4 = cbm_registry_resolve(r, "proj.pkg.service.get", "proj.pkg.service", NULL, NULL, 0);
286+
ASSERT_STR_EQ(res4.qualified_name, "proj.pkg.service.get");
287+
288+
/* Dotted call with namespace/module last segment (e.g. "service.get") -> should resolve */
289+
cbm_resolution_t res5 = cbm_registry_resolve(r, "service.get", "proj.pkg.service", NULL, NULL, 0);
290+
ASSERT_STR_EQ(res5.qualified_name, "proj.pkg.service.get");
291+
ASSERT_STR_EQ(res5.strategy, "same_module");
292+
293+
cbm_registry_free(r);
294+
PASS();
295+
}
296+
267297
/* A package/namespace-qualified callee whose bare name is defined in several
268298
* places must resolve to the package named in the call — not collapse onto a
269299
* single winner. Regression for qualified cross-file calls (e.g. Perl
@@ -772,6 +802,7 @@ SUITE(registry) {
772802
RUN_TEST(registry_no_duplicates);
773803
/* Resolution */
774804
RUN_TEST(resolve_same_module);
805+
RUN_TEST(resolve_same_module_only_on_self_receiver);
775806
RUN_TEST(resolve_qualified_disambiguates_same_name);
776807
RUN_TEST(resolve_qualified_ambiguous_tail_falls_through);
777808
RUN_TEST(resolve_import_map);

0 commit comments

Comments
 (0)