Skip to content

Commit 91a1723

Browse files
authored
Merge pull request DeusData#1897 from CaptainMittens/fix/resolve-receiver-chain-mismatch
fix(registry): refuse a name-only match the receiver chain contradicts
2 parents 9d0c6a1 + 4291412 commit 91a1723

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

‎src/pipeline/registry.c‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,87 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
925925
return match;
926926
}
927927

928+
/* A dotted callee whose FIRST segment starts upper-case names a type — URLSession,
929+
* Calendar, JSONEncoder. That receiver chain is evidence the bare-name scorers
930+
* throw away, and throwing it away binds Foundation's URLSession.shared.data to
931+
* a project's own PickedFile.data: high confidence, and nothing in the graph
932+
* shows it is wrong. Require instead that the candidate's own parent segment
933+
* appears somewhere in the chain. Calendar.utcGregorian.startOfDayUTC resolving
934+
* to AuthDTOs.Calendar.startOfDayUTC passes, because Calendar is in the chain.
935+
*
936+
* Only an upper-case first segment is guarded. A lower-case root names a value
937+
* (vm.load, http.Get, os.path.join) whose declared type the chain does not
938+
* show, so the chain proves nothing there and the call passes through
939+
* unchanged. A callee with no separator passes through as well.
940+
*
941+
* Language agnostic by design: the registry holds no language, and every
942+
* language that writes receiver chains gains the same protection. */
943+
static bool receiver_chain_admits(const char *callee_name, const char *candidate_qn) {
944+
/* Normalize "::" -> "." so the chain composes with dotted candidate QNs,
945+
* the same way qualified_suffix_match does. */
946+
char dotted[CBM_SZ_512];
947+
size_t w = 0;
948+
for (const char *s = callee_name; *s && w + SKIP_ONE < sizeof(dotted);) {
949+
if (s[0] == ':' && s[1] == ':') {
950+
dotted[w++] = '.';
951+
s += 2;
952+
} else {
953+
dotted[w++] = *s++;
954+
}
955+
}
956+
dotted[w] = '\0';
957+
958+
const char *last_dot = strrchr(dotted, '.');
959+
if (!last_dot) {
960+
return true; /* bare name — no receiver chain to judge */
961+
}
962+
if (dotted[0] < 'A' || dotted[0] > 'Z') {
963+
return true; /* lower-case root names a value, not a type */
964+
}
965+
/* A name written in capitals with underscores is a constant holding a
966+
* value, not a type: ISO_4217_URL.lower is a string's own method. JSON and
967+
* URL carry no underscore and stay guarded. */
968+
int has_underscore = 0;
969+
int all_caps = 1;
970+
for (const char *c = dotted; c < last_dot && *c != '.'; c++) {
971+
if (*c == '_') {
972+
has_underscore = 1;
973+
} else if (*c >= 'a' && *c <= 'z') {
974+
all_caps = 0;
975+
break;
976+
}
977+
}
978+
if (all_caps && has_underscore) {
979+
return true;
980+
}
981+
982+
/* The candidate's parent segment: the one before its final name. */
983+
const char *cand_last = strrchr(candidate_qn, '.');
984+
if (!cand_last || cand_last == candidate_qn) {
985+
return true; /* top-level candidate — no parent to look for */
986+
}
987+
const char *parent = cand_last;
988+
while (parent > candidate_qn && parent[-1] != '.') {
989+
parent--;
990+
}
991+
size_t parent_len = (size_t)(cand_last - parent);
992+
993+
/* Walk the chain — every segment before the final callee name. A trailing
994+
* "()" is dropped so JSONEncoder().encode reads as JSONEncoder. */
995+
for (const char *seg = dotted; seg < last_dot;) {
996+
const char *end = strchr(seg, '.');
997+
size_t len = (size_t)(end - seg);
998+
if (len >= 2 && seg[len - 2] == '(' && seg[len - 1] == ')') {
999+
len -= 2; /* an empty "()" — JSONEncoder().encode names JSONEncoder */
1000+
}
1001+
if (len == parent_len && strncmp(seg, parent, parent_len) == 0) {
1002+
return true;
1003+
}
1004+
seg = end + SKIP_ONE;
1005+
}
1006+
return false;
1007+
}
1008+
9281009
/* Strategy 3+4: Name lookup + suffix match */
9291010
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
9301011
const char *module_qn, const char **import_vals,
@@ -950,6 +1031,9 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
9501031

9511032
/* Strategy 3: unique name */
9521033
if (arr->count == SKIP_ONE) {
1034+
if (!receiver_chain_admits(callee_name, arr->items[0])) {
1035+
return empty_result();
1036+
}
9531037
double conf = CONF_UNIQUE_NAME;
9541038
if (import_vals && import_count > 0 &&
9551039
!is_import_reachable(arr->items[0], import_vals, import_count)) {
@@ -964,6 +1048,9 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
9641048
}
9651049
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
9661050
if (best) {
1051+
if (!receiver_chain_admits(callee_name, best)) {
1052+
return empty_result();
1053+
}
9671054
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
9681055
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};
9691056
}

‎tests/test_pipeline.c‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9456,6 +9456,78 @@ TEST(registry_confidence_suffix_match) {
94569456
PASS();
94579457
}
94589458

9459+
/* Issue #1893: a call on a library type bound to a same-named project member.
9460+
* URLSession is Foundation's, not this project's, so PickedFile.data is the
9461+
* wrong target — and with one candidate it won the top name-only confidence. */
9462+
TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) {
9463+
cbm_registry_t *reg = cbm_registry_new();
9464+
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable");
9465+
9466+
cbm_resolution_t r =
9467+
cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0);
9468+
ASSERT_NULL(r.qualified_name);
9469+
9470+
cbm_registry_free(reg);
9471+
PASS();
9472+
}
9473+
9474+
/* The same refusal on the other name-only exit, where several candidates share
9475+
* the final name and import distance picks the winner. */
9476+
TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) {
9477+
cbm_registry_t *reg = cbm_registry_new();
9478+
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable");
9479+
cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable");
9480+
9481+
cbm_resolution_t r =
9482+
cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0);
9483+
ASSERT_NULL(r.qualified_name);
9484+
9485+
cbm_registry_free(reg);
9486+
PASS();
9487+
}
9488+
9489+
/* The true positive the gate must not eat: the project extends Calendar itself,
9490+
* so Calendar really is in the receiver chain. */
9491+
TEST(registry_receiver_chain_keeps_project_extension_issue1893) {
9492+
cbm_registry_t *reg = cbm_registry_new();
9493+
cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method");
9494+
9495+
cbm_resolution_t r = cbm_registry_resolve(reg, "Calendar.utcGregorian.startOfDayUTC",
9496+
"HomeboxUI.Stats", NULL, NULL, 0);
9497+
ASSERT_STR_EQ(r.qualified_name, "AuthDTOs.Calendar.startOfDayUTC");
9498+
ASSERT_STR_EQ(r.strategy, "unique_name");
9499+
9500+
cbm_registry_free(reg);
9501+
PASS();
9502+
}
9503+
9504+
/* A lower-case root names a value, whose type the chain does not show. The gate
9505+
* must not look at it, or every ordinary vm.load style call would be refused. */
9506+
TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) {
9507+
cbm_registry_t *reg = cbm_registry_new();
9508+
cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method");
9509+
9510+
cbm_resolution_t r = cbm_registry_resolve(reg, "vm.load", "HomeboxUI.Views", NULL, NULL, 0);
9511+
ASSERT_STR_EQ(r.qualified_name, "HomeboxUI.EntityListViewModel.load");
9512+
ASSERT_STR_EQ(r.strategy, "unique_name");
9513+
9514+
cbm_registry_free(reg);
9515+
PASS();
9516+
}
9517+
9518+
/* An unqualified callee has no chain at all and must pass through unchanged. */
9519+
TEST(registry_receiver_chain_ignores_bare_name_issue1893) {
9520+
cbm_registry_t *reg = cbm_registry_new();
9521+
cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function");
9522+
9523+
cbm_resolution_t r = cbm_registry_resolve(reg, "helper", "proj.other", NULL, NULL, 0);
9524+
ASSERT_STR_EQ(r.qualified_name, "proj.pkg.helper");
9525+
ASSERT_STR_EQ(r.strategy, "unique_name");
9526+
9527+
cbm_registry_free(reg);
9528+
PASS();
9529+
}
9530+
94599531
TEST(registry_fuzzy_confidence_single) {
94609532
cbm_registry_t *reg = cbm_registry_new();
94619533
cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function");
@@ -13491,6 +13563,11 @@ SUITE(pipeline) {
1349113563
RUN_TEST(registry_confidence_same_module);
1349213564
RUN_TEST(registry_confidence_unique_name);
1349313565
RUN_TEST(registry_confidence_suffix_match);
13566+
RUN_TEST(registry_receiver_chain_refuses_library_unique_name_issue1893);
13567+
RUN_TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893);
13568+
RUN_TEST(registry_receiver_chain_keeps_project_extension_issue1893);
13569+
RUN_TEST(registry_receiver_chain_ignores_lowercase_root_issue1893);
13570+
RUN_TEST(registry_receiver_chain_ignores_bare_name_issue1893);
1349413571
RUN_TEST(registry_fuzzy_confidence_single);
1349513572
RUN_TEST(registry_fuzzy_confidence_distance);
1349613573
RUN_TEST(registry_negative_import_rejects);

0 commit comments

Comments
 (0)