fix(pipeline): suppress weak external lsp fallbacks

When LSP resolves a call to an external or unindexed target, keep strong registry evidence such as same-module/import-map matches but drop weak non-import-reachable short-name fallbacks. This prevents false CALLS edges like scope.get -> local *.get while preserving Python re-export/import behavior such as fastapi.Header.

Add a focused pipeline canary for external LSP fallback suppression and keep the existing Python re-export and super().__init__ canaries green. FastAPI matrix remains correctness-safe through the scoped_lsp_gap full-reindex guard, so this is not a default-performance closure.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-05 02:05:54 -04:00
parent 1bc2d11151
commit 53e2bc24b7
4 changed files with 93 additions and 2 deletions
+16
View File
@@ -298,6 +298,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
/* LSP-resolved calls take precedence over registry-textual matching. */
const CBMResolvedCall *lsp =
cbm_lsp_resolution_index_find(lsp_idx, lsp_calls, call, ctx->lsp_confidence_floor);
bool lsp_target_unindexed = false;
if (lsp) {
const cbm_gbuf_node_t *target_node = calls_lsp_target_node(ctx, lsp->callee_qn);
if (target_node && source_node->id != target_node->id) {
@@ -314,6 +315,16 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
}
return SKIP_ONE;
}
if (cbm_service_pattern_route_method(call->callee_name) != NULL) {
const char *handler_ref = NULL;
const char *route_path = cbm_pipeline_call_route_path_and_handler(call, &handler_ref);
if (route_path) {
handle_route_registration(ctx, call, source_node, route_path, handler_ref,
module_qn, imp_keys, imp_vals, imp_count);
return SKIP_ONE;
}
}
lsp_target_unindexed = true;
}
if (cbm_service_pattern_route_method(call->callee_name) != NULL) {
@@ -334,6 +345,11 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
cbm_pipeline_try_field_type_hint(ctx->registry, ctx->gbuf, &res, call->callee_name,
source_node->id);
if (lsp_target_unindexed && cbm_registry_strategy_is_weak_short_name(res.strategy) &&
!cbm_registry_is_import_reachable(res.qualified_name, imp_vals, imp_count)) {
return 0;
}
/* Perl call-graph noise guard (#476). Perl has no LSP resolver, so the
* generic registry chain is the only resolver; for builtins (push/shift/
* keys/...) and method calls ($obj->m with an unresolved receiver), a *weak*
+4
View File
@@ -289,6 +289,10 @@ bool cbm_registry_exists(const cbm_registry_t *r, const char *qn);
* the name. Perl-scoped: callers gate on the file language. */
bool cbm_perl_is_builtin(const char *name);
/* True for registry strategies that are only weak short-name guesses. Strong
* same-module and import-map matches return false. */
bool cbm_registry_strategy_is_weak_short_name(const char *strategy);
/* Decide whether a resolved Perl call edge is generic-resolver noise to drop
* (#476): true only for Perl, only for a builtin/method call, and only when the
* match used a weak short-name strategy — high-confidence same_module/import_map
+12 -2
View File
@@ -393,13 +393,23 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
if (!strategy || !strategy[0]) {
return false;
}
if (strcmp(strategy, "same_module") == 0 || strcmp(strategy, "import_map") == 0 ||
strcmp(strategy, "import_map_suffix") == 0) {
if (!cbm_registry_strategy_is_weak_short_name(strategy)) {
return false; /* high-confidence import/same-module match — keep the genuine edge */
}
return true; /* weak short-name match (suffix_match / unique_name / …) → drop */
}
bool cbm_registry_strategy_is_weak_short_name(const char *strategy) {
if (!strategy || !strategy[0]) {
return false;
}
if (strcmp(strategy, "same_module") == 0 || strcmp(strategy, "import_map") == 0 ||
strcmp(strategy, "import_map_suffix") == 0) {
return false;
}
return true;
}
/* ── Lifecycle ──────────────────────────────────────────────────── */
cbm_registry_t *cbm_registry_new(void) {
+61
View File
@@ -7956,6 +7956,66 @@ TEST(pipeline_python_super_init_external_lsp_suppresses_suffix_fallback) {
PASS();
}
TEST(pipeline_external_lsp_target_suppresses_suffix_fallback) {
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/tmp/proj");
cbm_registry_t *reg = cbm_registry_new();
ASSERT_NOT_NULL(gb);
ASSERT_NOT_NULL(reg);
int64_t source_id =
cbm_gbuf_upsert_node(gb, "Function", "caller", "proj.app.caller", "app.py", 1, 10,
"{}");
int64_t suffix_target_id = cbm_gbuf_upsert_node(
gb, "Method", "get", "proj.fastapi.routing.APIRouter.get", "routing.py", 1, 10, "{}");
int64_t second_suffix_target_id = cbm_gbuf_upsert_node(
gb, "Method", "get", "proj.other.Mapping.get", "other.py", 1, 10, "{}");
ASSERT_GT(source_id, 0);
ASSERT_GT(suffix_target_id, 0);
ASSERT_GT(second_suffix_target_id, 0);
cbm_registry_add(reg, "get", "proj.fastapi.routing.APIRouter.get", "Method");
cbm_registry_add(reg, "get", "proj.other.Mapping.get", "Method");
CBMFileResult result;
memset(&result, 0, sizeof(result));
cbm_arena_init(&result.arena);
CBMCall call = {.callee_name = "scope.get",
.enclosing_func_qn = "proj.app.caller",
.start_line = 2};
cbm_calls_push(&result.calls, &result.arena, call);
CBMResolvedCall resolved = {.caller_qn = "proj.app.caller",
.callee_qn = "external.collections.Mapping.get",
.strategy = "lsp_external_method",
.confidence = 0.95f};
cbm_resolvedcall_push(&result.resolved_calls, &result.arena, resolved);
CBMFileResult *result_cache[1] = {&result};
atomic_int cancelled;
atomic_init(&cancelled, 0);
cbm_pipeline_ctx_t ctx = {.project_name = "proj",
.repo_path = "/tmp/proj",
.gbuf = gb,
.registry = reg,
.cancelled = &cancelled,
.mode = CBM_MODE_FAST,
.result_cache = result_cache};
cbm_file_info_t files[1] = {{.path = "/tmp/proj/app.py",
.rel_path = "app.py",
.language = CBM_LANG_PYTHON}};
ASSERT_EQ(cbm_pipeline_pass_calls(&ctx, files, 1), 0);
const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
ASSERT_EQ(cbm_gbuf_find_edges_by_source_type(gb, source_id, "CALLS", &edges, &edge_count),
0);
ASSERT_EQ(edge_count, 0);
cbm_arena_destroy(&result.arena);
cbm_registry_free(reg);
cbm_gbuf_free(gb);
PASS();
}
TEST(registry_fuzzy_confidence_single) {
cbm_registry_t *reg = cbm_registry_new();
cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function");
@@ -15198,6 +15258,7 @@ SUITE(pipeline) {
RUN_TEST(registry_confidence_unique_name);
RUN_TEST(registry_confidence_suffix_match);
RUN_TEST(pipeline_python_super_init_external_lsp_suppresses_suffix_fallback);
RUN_TEST(pipeline_external_lsp_target_suppresses_suffix_fallback);
RUN_TEST(registry_fuzzy_confidence_single);
RUN_TEST(registry_fuzzy_confidence_distance);
RUN_TEST(registry_negative_import_rejects);