mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-02 07:53:41 +08:00
fix(registry): reject unreachable qualified fallbacks
When a dotted or namespace-qualified callee has import evidence but no candidate is import-reachable, stop before weak same-simple-name fallback. This prevents external calls such as email.message.Message or receiver calls such as response.get from resolving to unrelated project symbols while preserving import_map, import_map_suffix, qualified_suffix, and bare-name fallback behavior. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
+13
-4
@@ -104,6 +104,10 @@ static const char *simple_name(const char *qn) {
|
||||
return seg;
|
||||
}
|
||||
|
||||
static bool callee_has_qualified_separator(const char *callee_name) {
|
||||
return callee_name && (strchr(callee_name, '.') != NULL || strstr(callee_name, "::") != NULL);
|
||||
}
|
||||
|
||||
/* Extract everything before the last dot. Returns heap-allocated string. */
|
||||
|
||||
/* Check if a qualified name looks like a test/mock path. */
|
||||
@@ -613,7 +617,8 @@ static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char
|
||||
|
||||
/* Strategy 4: multiple candidates with import filtering. */
|
||||
static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const char *module_qn,
|
||||
const char **import_vals, int import_count) {
|
||||
const char **import_vals, int import_count,
|
||||
bool callee_qualified) {
|
||||
const char *filtered[CBM_SZ_256];
|
||||
int fcount = 0;
|
||||
for (int i = 0; i < arr->count && fcount < CBM_SZ_256; i++) {
|
||||
@@ -632,7 +637,10 @@ static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const
|
||||
return (cbm_resolution_t){best, "suffix_match", conf, fcount};
|
||||
}
|
||||
}
|
||||
/* No import-reachable — use all candidates with penalty */
|
||||
if (callee_qualified) {
|
||||
return empty_result();
|
||||
}
|
||||
/* No import-reachable for a bare callee — use all candidates with penalty. */
|
||||
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 * REG_HALF_PENALTY, arr->count);
|
||||
@@ -672,7 +680,7 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
|
||||
dotted[w] = '\0';
|
||||
/* Must be qualified (contain a '.') — a bare name matches every candidate
|
||||
* and carries no disambiguating signal. */
|
||||
if (!strchr(dotted, '.')) {
|
||||
if (!callee_has_qualified_separator(dotted)) {
|
||||
return NULL;
|
||||
}
|
||||
const char *match = NULL;
|
||||
@@ -733,7 +741,8 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
|
||||
|
||||
/* Strategy 4: multiple candidates */
|
||||
if (import_vals && import_count > 0) {
|
||||
return resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
|
||||
return resolve_multi_with_imports(arr, module_qn, import_vals, import_count,
|
||||
callee_has_qualified_separator(callee_name));
|
||||
}
|
||||
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
|
||||
if (best) {
|
||||
|
||||
@@ -326,6 +326,40 @@ TEST(resolve_qualified_ambiguous_tail_falls_through) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resolve_qualified_imported_external_rejects_unreachable_suffix) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Message", "proj.docs.additional.Message", "Class");
|
||||
cbm_registry_add(r, "Message", "proj.models.Message", "Class");
|
||||
cbm_registry_add(r, "Message", "proj.other.Message", "Class");
|
||||
|
||||
const char *keys[] = {"email.message"};
|
||||
const char *vals[] = {"email.message"};
|
||||
cbm_resolution_t res =
|
||||
cbm_registry_resolve(r, "email.message.Message", "proj.fastapi.routing", keys, vals, 1);
|
||||
ASSERT_TRUE(!res.qualified_name || res.qualified_name[0] == '\0');
|
||||
ASSERT_TRUE(!res.strategy || res.strategy[0] == '\0');
|
||||
|
||||
cbm_registry_free(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resolve_dotted_receiver_rejects_unreachable_suffix_when_imports_exist) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "get", "proj.fastapi.routing.APIRouter.get", "Method");
|
||||
cbm_registry_add(r, "get", "proj.datastructures.Headers.get", "Method");
|
||||
cbm_registry_add(r, "get", "proj.other.Mapping.get", "Method");
|
||||
|
||||
const char *keys[] = {"Request"};
|
||||
const char *vals[] = {"starlette.requests.Request"};
|
||||
cbm_resolution_t res =
|
||||
cbm_registry_resolve(r, "response.get", "proj.fastapi.routing", keys, vals, 1);
|
||||
ASSERT_TRUE(!res.qualified_name || res.qualified_name[0] == '\0');
|
||||
ASSERT_TRUE(!res.strategy || res.strategy[0] == '\0');
|
||||
|
||||
cbm_registry_free(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resolve_import_map) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function");
|
||||
@@ -797,6 +831,8 @@ SUITE(registry) {
|
||||
RUN_TEST(resolve_same_module);
|
||||
RUN_TEST(resolve_qualified_disambiguates_same_name);
|
||||
RUN_TEST(resolve_qualified_ambiguous_tail_falls_through);
|
||||
RUN_TEST(resolve_qualified_imported_external_rejects_unreachable_suffix);
|
||||
RUN_TEST(resolve_dotted_receiver_rejects_unreachable_suffix_when_imports_exist);
|
||||
RUN_TEST(resolve_import_map);
|
||||
RUN_TEST(resolve_import_map_bare_function);
|
||||
RUN_TEST(resolve_unique_name);
|
||||
|
||||
Reference in New Issue
Block a user