feat(pipeline): bridge LSP resolved_calls into call-edge attribution

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.
This commit is contained in:
Martin Vogel
2026-05-08 23:33:07 +02:00
parent d674b43e0d
commit cef58bdb65
2 changed files with 118 additions and 8 deletions
+76 -6
View File
@@ -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++;
+42 -2
View File
@@ -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);