Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,87 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
return match;
}

/* A dotted callee whose FIRST segment starts upper-case names a type — URLSession,
* Calendar, JSONEncoder. That receiver chain is evidence the bare-name scorers
* throw away, and throwing it away binds Foundation's URLSession.shared.data to
* a project's own PickedFile.data: high confidence, and nothing in the graph
* shows it is wrong. Require instead that the candidate's own parent segment
* appears somewhere in the chain. Calendar.utcGregorian.startOfDayUTC resolving
* to AuthDTOs.Calendar.startOfDayUTC passes, because Calendar is in the chain.
*
* Only an upper-case first segment is guarded. A lower-case root names a value
* (vm.load, http.Get, os.path.join) whose declared type the chain does not
* show, so the chain proves nothing there and the call passes through
* unchanged. A callee with no separator passes through as well.
*
* Language agnostic by design: the registry holds no language, and every
* language that writes receiver chains gains the same protection. */
static bool receiver_chain_admits(const char *callee_name, const char *candidate_qn) {
/* Normalize "::" -> "." so the chain composes with dotted candidate QNs,
* the same way qualified_suffix_match does. */
char dotted[CBM_SZ_512];
size_t w = 0;
for (const char *s = callee_name; *s && w + SKIP_ONE < sizeof(dotted);) {
if (s[0] == ':' && s[1] == ':') {
dotted[w++] = '.';
s += 2;
} else {
dotted[w++] = *s++;
}
}
dotted[w] = '\0';

const char *last_dot = strrchr(dotted, '.');
if (!last_dot) {
return true; /* bare name — no receiver chain to judge */
}
if (dotted[0] < 'A' || dotted[0] > 'Z') {
return true; /* lower-case root names a value, not a type */
}
/* A name written in capitals with underscores is a constant holding a
* value, not a type: ISO_4217_URL.lower is a string's own method. JSON and
* URL carry no underscore and stay guarded. */
int has_underscore = 0;
int all_caps = 1;
for (const char *c = dotted; c < last_dot && *c != '.'; c++) {
if (*c == '_') {
has_underscore = 1;
} else if (*c >= 'a' && *c <= 'z') {
all_caps = 0;
break;
}
}
if (all_caps && has_underscore) {
return true;
}

/* The candidate's parent segment: the one before its final name. */
const char *cand_last = strrchr(candidate_qn, '.');
if (!cand_last || cand_last == candidate_qn) {
return true; /* top-level candidate — no parent to look for */
}
const char *parent = cand_last;
while (parent > candidate_qn && parent[-1] != '.') {
parent--;
}
size_t parent_len = (size_t)(cand_last - parent);

/* Walk the chain — every segment before the final callee name. A trailing
* "()" is dropped so JSONEncoder().encode reads as JSONEncoder. */
for (const char *seg = dotted; seg < last_dot;) {
const char *end = strchr(seg, '.');
size_t len = (size_t)(end - seg);
if (len >= 2 && seg[len - 2] == '(' && seg[len - 1] == ')') {
len -= 2; /* an empty "()" — JSONEncoder().encode names JSONEncoder */
}
if (len == parent_len && strncmp(seg, parent, parent_len) == 0) {
return true;
}
seg = end + SKIP_ONE;
}
return false;
}

/* 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,
Expand All @@ -850,6 +931,9 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char

/* Strategy 3: unique name */
if (arr->count == SKIP_ONE) {
if (!receiver_chain_admits(callee_name, arr->items[0])) {
return empty_result();
}
double conf = CONF_UNIQUE_NAME;
if (import_vals && import_count > 0 &&
!is_import_reachable(arr->items[0], import_vals, import_count)) {
Expand All @@ -864,6 +948,9 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
}
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
if (best) {
if (!receiver_chain_admits(callee_name, best)) {
return empty_result();
}
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};
}
Expand Down
77 changes: 77 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -8787,6 +8787,78 @@ TEST(registry_confidence_suffix_match) {
PASS();
}

/* Issue #1893: a call on a library type bound to a same-named project member.
* URLSession is Foundation's, not this project's, so PickedFile.data is the
* wrong target — and with one candidate it won the top name-only confidence. */
TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) {
cbm_registry_t *reg = cbm_registry_new();
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable");

cbm_resolution_t r =
cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0);
ASSERT_NULL(r.qualified_name);

cbm_registry_free(reg);
PASS();
}

/* The same refusal on the other name-only exit, where several candidates share
* the final name and import distance picks the winner. */
TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) {
cbm_registry_t *reg = cbm_registry_new();
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable");
cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable");

cbm_resolution_t r =
cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0);
ASSERT_NULL(r.qualified_name);

cbm_registry_free(reg);
PASS();
}

/* The true positive the gate must not eat: the project extends Calendar itself,
* so Calendar really is in the receiver chain. */
TEST(registry_receiver_chain_keeps_project_extension_issue1893) {
cbm_registry_t *reg = cbm_registry_new();
cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method");

cbm_resolution_t r = cbm_registry_resolve(reg, "Calendar.utcGregorian.startOfDayUTC",
"HomeboxUI.Stats", NULL, NULL, 0);
ASSERT_STR_EQ(r.qualified_name, "AuthDTOs.Calendar.startOfDayUTC");
ASSERT_STR_EQ(r.strategy, "unique_name");

cbm_registry_free(reg);
PASS();
}

/* A lower-case root names a value, whose type the chain does not show. The gate
* must not look at it, or every ordinary vm.load style call would be refused. */
TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) {
cbm_registry_t *reg = cbm_registry_new();
cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method");

cbm_resolution_t r = cbm_registry_resolve(reg, "vm.load", "HomeboxUI.Views", NULL, NULL, 0);
ASSERT_STR_EQ(r.qualified_name, "HomeboxUI.EntityListViewModel.load");
ASSERT_STR_EQ(r.strategy, "unique_name");

cbm_registry_free(reg);
PASS();
}

/* An unqualified callee has no chain at all and must pass through unchanged. */
TEST(registry_receiver_chain_ignores_bare_name_issue1893) {
cbm_registry_t *reg = cbm_registry_new();
cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function");

cbm_resolution_t r = cbm_registry_resolve(reg, "helper", "proj.other", NULL, NULL, 0);
ASSERT_STR_EQ(r.qualified_name, "proj.pkg.helper");
ASSERT_STR_EQ(r.strategy, "unique_name");

cbm_registry_free(reg);
PASS();
}

TEST(registry_fuzzy_confidence_single) {
cbm_registry_t *reg = cbm_registry_new();
cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function");
Expand Down Expand Up @@ -12676,6 +12748,11 @@ SUITE(pipeline) {
RUN_TEST(registry_confidence_same_module);
RUN_TEST(registry_confidence_unique_name);
RUN_TEST(registry_confidence_suffix_match);
RUN_TEST(registry_receiver_chain_refuses_library_unique_name_issue1893);
RUN_TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893);
RUN_TEST(registry_receiver_chain_keeps_project_extension_issue1893);
RUN_TEST(registry_receiver_chain_ignores_lowercase_root_issue1893);
RUN_TEST(registry_receiver_chain_ignores_bare_name_issue1893);
RUN_TEST(registry_fuzzy_confidence_single);
RUN_TEST(registry_fuzzy_confidence_distance);
RUN_TEST(registry_negative_import_rejects);
Expand Down
Loading