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);