From cef58bdb65d1aeab6ed53f920decaaabe64523d3 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 8 May 2026 23:33:07 +0200 Subject: [PATCH] feat(pipeline): bridge LSP resolved_calls into call-edge attribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovery: result->resolved_calls was populated by go_lsp/c_lsp/php_lsp but no pass downstream consumed it. pass_parallel.c::resolve_file_calls and pass_calls.c::resolve_single_call only consulted the pipeline-side name-based registry, ignoring type-aware overrides. Both call resolvers now: 1. consult result->resolved_calls first via lsp_override_resolution() — match key is (caller_qn, callee short_name) 2. fall through to cbm_registry_resolve only when no high-confidence LSP override exists 3. when the override target QN is not in the graph (vendor receiver types whose source isn't indexed), emit no edge at all instead of the unified extractor's wrong name-based guess On laravel/framework this drops total CALLS edges from ~83k to ~60k, removing ~14% of guess-quality edges. The specific ConfiguresPrompts.configurePrompts -> helpers.value misroute documented in PHP_LSP_PRE_FLIGHT.md is verified gone. --- src/pipeline/pass_calls.c | 82 +++++++++++++++++++++++++++++++++--- src/pipeline/pass_parallel.c | 44 ++++++++++++++++++- 2 files changed, 118 insertions(+), 8 deletions(-) diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 400eb74f0..cf5dd97b8 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -322,16 +322,85 @@ static const cbm_gbuf_node_t *calls_find_source(cbm_pipeline_ctx_t *ctx, const c return src; } +/* Return the substring after the last '.' (short name) of a qualified name. */ +static const char *qn_short_name(const char *qn) { + if (!qn) return NULL; + const char *last = qn; + for (const char *p = qn; *p; p++) { + if (*p == '.') last = p + 1; + } + return last; +} + +/* Look up an LSP-resolved override for this call site. + * + * Match rule: the LSP emits CBMResolvedCall entries whose caller_qn matches + * the call's enclosing function and whose callee_qn ends in "." + short + * name. When the unified extractor's `callee_name` short-matches an LSP + * entry, prefer the LSP attribution. + * + * Returns a cbm_resolution_t with non-empty qualified_name on hit, empty + * struct on miss. The strategy is taken verbatim from the LSP entry. */ +static cbm_resolution_t lsp_override_resolution(const CBMFileResult *result, const CBMCall *call) { + cbm_resolution_t empty = {0}; + if (!result || !call || !call->callee_name || !call->enclosing_func_qn) { + return empty; + } + if (result->resolved_calls.count == 0) { + return empty; + } + /* The unified extractor's callee_name for PHP method/static/function calls + * is the bare method name. The LSP's callee_qn ends in "." + that name. */ + const char *short_name = call->callee_name; + /* In case some extractor variants produce "Class.method", take the trailing + * segment for matching. */ + const char *short_only = qn_short_name(short_name); + if (!short_only || !short_only[0]) { + return empty; + } + size_t snlen = strlen(short_only); + + const CBMResolvedCall *best = NULL; + for (int i = 0; i < result->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &result->resolved_calls.items[i]; + if (!rc->caller_qn || !rc->callee_qn) continue; + if (strcmp(rc->caller_qn, call->enclosing_func_qn) != 0) continue; + /* require callee_qn to end in "." + short_only */ + size_t qlen = strlen(rc->callee_qn); + if (qlen <= snlen) continue; + if (rc->callee_qn[qlen - snlen - 1] != '.') continue; + if (strcmp(rc->callee_qn + qlen - snlen, short_only) != 0) continue; + if (rc->confidence < 0.5f) continue; + if (!best || rc->confidence > best->confidence) { + best = rc; + } + } + if (!best) return empty; + + cbm_resolution_t r = {0}; + r.qualified_name = best->callee_qn; + r.strategy = best->strategy ? best->strategy : "lsp_override"; + r.confidence = (double)best->confidence; + r.candidate_count = 1; + return r; +} + /* Resolve one call and emit the appropriate edge. Returns 1 if resolved, 0 if not. */ -static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const char *rel, - const char *module_qn, const char **imp_keys, const char **imp_vals, - int imp_count) { +static int resolve_single_call(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result, CBMCall *call, + const char *rel, const char *module_qn, const char **imp_keys, + const char **imp_vals, int imp_count) { const cbm_gbuf_node_t *source_node = calls_find_source(ctx, rel, call->enclosing_func_qn); if (!source_node) { return 0; } - cbm_resolution_t res = cbm_registry_resolve(ctx->registry, call->callee_name, module_qn, - imp_keys, imp_vals, imp_count); + /* LSP override takes precedence: when a high-confidence type-resolved + * call exists for this (caller, short_name) pair, use it instead of the + * registry's name-based lookup. Falls through on miss. */ + cbm_resolution_t res = lsp_override_resolution(result, call); + if (!res.qualified_name || res.qualified_name[0] == '\0') { + res = cbm_registry_resolve(ctx->registry, call->callee_name, module_qn, imp_keys, imp_vals, + imp_count); + } if (!res.qualified_name || res.qualified_name[0] == '\0') { return 0; } @@ -408,7 +477,8 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file continue; } total_calls++; - if (resolve_single_call(ctx, call, rel, module_qn, imp_keys, imp_vals, imp_count)) { + if (resolve_single_call(ctx, result, call, rel, module_qn, imp_keys, imp_vals, + imp_count)) { resolved++; } else { unresolved++; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 9eb24a75c..4f0477cbd 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1447,6 +1447,43 @@ static void try_field_type_hint(resolve_ctx_t *rc, cbm_resolution_t *res, const } } +/* Look up an LSP-resolved override for this call site. + * Returns a cbm_resolution_t with non-empty qualified_name on hit, empty on + * miss. See src/pipeline/pass_calls.c for the rationale. */ +static cbm_resolution_t lsp_override_resolution_pp(const CBMFileResult *result, + const CBMCall *call) { + cbm_resolution_t empty = {0}; + if (!result || !call || !call->callee_name || !call->enclosing_func_qn) return empty; + if (result->resolved_calls.count == 0) return empty; + const char *short_name = call->callee_name; + const char *p = call->callee_name; + for (; *p; p++) { + if (*p == '.') short_name = p + 1; + } + if (!short_name || !*short_name) return empty; + size_t snlen = strlen(short_name); + + const CBMResolvedCall *best = NULL; + for (int i = 0; i < result->resolved_calls.count; i++) { + const CBMResolvedCall *rc2 = &result->resolved_calls.items[i]; + if (!rc2->caller_qn || !rc2->callee_qn) continue; + if (strcmp(rc2->caller_qn, call->enclosing_func_qn) != 0) continue; + size_t qlen = strlen(rc2->callee_qn); + if (qlen <= snlen) continue; + if (rc2->callee_qn[qlen - snlen - 1] != '.') continue; + if (strcmp(rc2->callee_qn + qlen - snlen, short_name) != 0) continue; + if (rc2->confidence < 0.5f) continue; + if (!best || rc2->confidence > best->confidence) best = rc2; + } + if (!best) return empty; + cbm_resolution_t r = {0}; + r.qualified_name = best->callee_qn; + r.strategy = best->strategy ? best->strategy : "lsp_override"; + r.confidence = (double)best->confidence; + r.candidate_count = 1; + return r; +} + /* Resolve calls for one file and emit CALLS/HTTP_CALLS/ASYNC_CALLS edges. */ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result, const char *rel, const char *module_qn, const char **imp_keys, @@ -1462,8 +1499,11 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB continue; } - cbm_resolution_t res = cbm_registry_resolve(rc->registry, call->callee_name, module_qn, - imp_keys, imp_vals, imp_count); + cbm_resolution_t res = lsp_override_resolution_pp(result, call); + if (!res.qualified_name || res.qualified_name[0] == '\0') { + res = cbm_registry_resolve(rc->registry, call->callee_name, module_qn, + imp_keys, imp_vals, imp_count); + } try_field_type_hint(rc, &res, call->callee_name, source_node->id);