Skip to content

Commit 2c76563

Browse files
fix(registry): refuse a name-only match the receiver chain contradicts
A dotted callee whose first segment starts upper-case names a type. That receiver chain is evidence, and the two name-only strategies threw it away: they matched on the final segment alone, so Foundation's URLSession.shared.data bound to a project's own PickedFile.data — at 0.75 confidence, with nothing in the graph to show the answer was wrong. receiver_chain_admits() now requires the candidate's own parent segment to appear somewhere in that chain. Calendar.utcGregorian.startOfDayUTC still resolves to AuthDTOs.Calendar.startOfDayUTC, because the project really does extend Calendar and Calendar is in the chain. Three shapes pass through untouched: - a callee with no separator, which has no chain to judge; - a lower-case root, which names a value whose declared type the chain does not show (vm.load, http.Get, os.path.join); - a name in capitals with underscores, which is a constant holding a value rather than a type. Measured: without this carve-out the gate refused ISO_4217_URL.lower -> builtins.str.lower, which is correct. JSON and URL carry no underscore and stay guarded. The gate applies only at the two name-only exits of resolve_name_lookup. import_map, same_module and qualified_suffix already carry real evidence and are left alone. Language agnostic by design: the registry holds no language, and every language that writes receiver chains gains the same protection. Fixes DeusData#1893 Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
1 parent c38aa35 commit 2c76563

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
@@ -888,6 +888,87 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
888888
return match;
889889
}
890890

891+
/* A dotted callee whose FIRST segment starts upper-case names a type — URLSession,
892+
* Calendar, JSONEncoder. That receiver chain is evidence the bare-name scorers
893+
* throw away, and throwing it away binds Foundation's URLSession.shared.data to
894+
* a project's own PickedFile.data: high confidence, and nothing in the graph
895+
* shows it is wrong. Require instead that the candidate's own parent segment
896+
* appears somewhere in the chain. Calendar.utcGregorian.startOfDayUTC resolving
897+
* to AuthDTOs.Calendar.startOfDayUTC passes, because Calendar is in the chain.
898+
*
899+
* Only an upper-case first segment is guarded. A lower-case root names a value
900+
* (vm.load, http.Get, os.path.join) whose declared type the chain does not
901+
* show, so the chain proves nothing there and the call passes through
902+
* unchanged. A callee with no separator passes through as well.
903+
*
904+
* Language agnostic by design: the registry holds no language, and every
905+
* language that writes receiver chains gains the same protection. */
906+
static bool receiver_chain_admits(const char *callee_name, const char *candidate_qn) {
907+
/* Normalize "::" -> "." so the chain composes with dotted candidate QNs,
908+
* the same way qualified_suffix_match does. */
909+
char dotted[CBM_SZ_512];
910+
size_t w = 0;
911+
for (const char *s = callee_name; *s && w + SKIP_ONE < sizeof(dotted);) {
912+
if (s[0] == ':' && s[1] == ':') {
913+
dotted[w++] = '.';
914+
s += 2;
915+
} else {
916+
dotted[w++] = *s++;
917+
}
918+
}
919+
dotted[w] = '\0';
920+
921+
const char *last_dot = strrchr(dotted, '.');
922+
if (!last_dot) {
923+
return true; /* bare name — no receiver chain to judge */
924+
}
925+
if (dotted[0] < 'A' || dotted[0] > 'Z') {
926+
return true; /* lower-case root names a value, not a type */
927+
}
928+
/* A name written in capitals with underscores is a constant holding a
929+
* value, not a type: ISO_4217_URL.lower is a string's own method. JSON and
930+
* URL carry no underscore and stay guarded. */
931+
int has_underscore = 0;
932+
int all_caps = 1;
933+
for (const char *c = dotted; c < last_dot && *c != '.'; c++) {
934+
if (*c == '_') {
935+
has_underscore = 1;
936+
} else if (*c >= 'a' && *c <= 'z') {
937+
all_caps = 0;
938+
break;
939+
}
940+
}
941+
if (all_caps && has_underscore) {
942+
return true;
943+
}
944+
945+
/* The candidate's parent segment: the one before its final name. */
946+
const char *cand_last = strrchr(candidate_qn, '.');
947+
if (!cand_last || cand_last == candidate_qn) {
948+
return true; /* top-level candidate — no parent to look for */
949+
}
950+
const char *parent = cand_last;
951+
while (parent > candidate_qn && parent[-1] != '.') {
952+
parent--;
953+
}
954+
size_t parent_len = (size_t)(cand_last - parent);
955+
956+
/* Walk the chain — every segment before the final callee name. A trailing
957+
* "()" is dropped so JSONEncoder().encode reads as JSONEncoder. */
958+
for (const char *seg = dotted; seg < last_dot;) {
959+
const char *end = strchr(seg, '.');
960+
size_t len = (size_t)(end - seg);
961+
if (len >= 2 && seg[len - 2] == '(' && seg[len - 1] == ')') {
962+
len -= 2; /* an empty "()" — JSONEncoder().encode names JSONEncoder */
963+
}
964+
if (len == parent_len && strncmp(seg, parent, parent_len) == 0) {
965+
return true;
966+
}
967+
seg = end + SKIP_ONE;
968+
}
969+
return false;
970+
}
971+
891972
/* Strategy 3+4: Name lookup + suffix match */
892973
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
893974
const char *module_qn, const char **import_vals,
@@ -913,6 +994,9 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
913994

914995
/* Strategy 3: unique name */
915996
if (arr->count == SKIP_ONE) {
997+
if (!receiver_chain_admits(callee_name, arr->items[0])) {
998+
return empty_result();
999+
}
9161000
double conf = CONF_UNIQUE_NAME;
9171001
if (import_vals && import_count > 0 &&
9181002
!is_import_reachable(arr->items[0], import_vals, import_count)) {
@@ -927,6 +1011,9 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
9271011
}
9281012
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
9291013
if (best) {
1014+
if (!receiver_chain_admits(callee_name, best)) {
1015+
return empty_result();
1016+
}
9301017
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
9311018
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};
9321019
}

tests/test_pipeline.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9275,6 +9275,78 @@ TEST(registry_confidence_suffix_match) {
92759275
PASS();
92769276
}
92779277

9278+
/* Issue #1893: a call on a library type bound to a same-named project member.
9279+
* URLSession is Foundation's, not this project's, so PickedFile.data is the
9280+
* wrong target — and with one candidate it won the top name-only confidence. */
9281+
TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) {
9282+
cbm_registry_t *reg = cbm_registry_new();
9283+
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable");
9284+
9285+
cbm_resolution_t r =
9286+
cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0);
9287+
ASSERT_NULL(r.qualified_name);
9288+
9289+
cbm_registry_free(reg);
9290+
PASS();
9291+
}
9292+
9293+
/* The same refusal on the other name-only exit, where several candidates share
9294+
* the final name and import distance picks the winner. */
9295+
TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) {
9296+
cbm_registry_t *reg = cbm_registry_new();
9297+
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable");
9298+
cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable");
9299+
9300+
cbm_resolution_t r =
9301+
cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0);
9302+
ASSERT_NULL(r.qualified_name);
9303+
9304+
cbm_registry_free(reg);
9305+
PASS();
9306+
}
9307+
9308+
/* The true positive the gate must not eat: the project extends Calendar itself,
9309+
* so Calendar really is in the receiver chain. */
9310+
TEST(registry_receiver_chain_keeps_project_extension_issue1893) {
9311+
cbm_registry_t *reg = cbm_registry_new();
9312+
cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method");
9313+
9314+
cbm_resolution_t r = cbm_registry_resolve(reg, "Calendar.utcGregorian.startOfDayUTC",
9315+
"HomeboxUI.Stats", NULL, NULL, 0);
9316+
ASSERT_STR_EQ(r.qualified_name, "AuthDTOs.Calendar.startOfDayUTC");
9317+
ASSERT_STR_EQ(r.strategy, "unique_name");
9318+
9319+
cbm_registry_free(reg);
9320+
PASS();
9321+
}
9322+
9323+
/* A lower-case root names a value, whose type the chain does not show. The gate
9324+
* must not look at it, or every ordinary vm.load style call would be refused. */
9325+
TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) {
9326+
cbm_registry_t *reg = cbm_registry_new();
9327+
cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method");
9328+
9329+
cbm_resolution_t r = cbm_registry_resolve(reg, "vm.load", "HomeboxUI.Views", NULL, NULL, 0);
9330+
ASSERT_STR_EQ(r.qualified_name, "HomeboxUI.EntityListViewModel.load");
9331+
ASSERT_STR_EQ(r.strategy, "unique_name");
9332+
9333+
cbm_registry_free(reg);
9334+
PASS();
9335+
}
9336+
9337+
/* An unqualified callee has no chain at all and must pass through unchanged. */
9338+
TEST(registry_receiver_chain_ignores_bare_name_issue1893) {
9339+
cbm_registry_t *reg = cbm_registry_new();
9340+
cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function");
9341+
9342+
cbm_resolution_t r = cbm_registry_resolve(reg, "helper", "proj.other", NULL, NULL, 0);
9343+
ASSERT_STR_EQ(r.qualified_name, "proj.pkg.helper");
9344+
ASSERT_STR_EQ(r.strategy, "unique_name");
9345+
9346+
cbm_registry_free(reg);
9347+
PASS();
9348+
}
9349+
92789350
TEST(registry_fuzzy_confidence_single) {
92799351
cbm_registry_t *reg = cbm_registry_new();
92809352
cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function");
@@ -13307,6 +13379,11 @@ SUITE(pipeline) {
1330713379
RUN_TEST(registry_confidence_same_module);
1330813380
RUN_TEST(registry_confidence_unique_name);
1330913381
RUN_TEST(registry_confidence_suffix_match);
13382+
RUN_TEST(registry_receiver_chain_refuses_library_unique_name_issue1893);
13383+
RUN_TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893);
13384+
RUN_TEST(registry_receiver_chain_keeps_project_extension_issue1893);
13385+
RUN_TEST(registry_receiver_chain_ignores_lowercase_root_issue1893);
13386+
RUN_TEST(registry_receiver_chain_ignores_bare_name_issue1893);
1331013387
RUN_TEST(registry_fuzzy_confidence_single);
1331113388
RUN_TEST(registry_fuzzy_confidence_distance);
1331213389
RUN_TEST(registry_negative_import_rejects);

0 commit comments

Comments
 (0)