diff --git a/internal/cbm/lsp/php_lsp.c b/internal/cbm/lsp/php_lsp.c index 4caf5ba0f..0ca1266ee 100644 --- a/internal/cbm/lsp/php_lsp.c +++ b/internal/cbm/lsp/php_lsp.c @@ -1760,8 +1760,32 @@ static void resolve_static_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind } } } - if (!class_qn) - return; + if (!class_qn) { + /* Fallback: enclosing_class_qn is NULL (shouldn't happen in well-formed + * code, but may occur if process_class_decl didn't run). Emit a low- + * confidence self-reference so pass_calls can still attempt registry + * resolution instead of dropping the call entirely. */ + if (ctx->enclosing_func_qn && strcmp(strategy, "php_self_static") == 0) { + /* Extract class QN from enclosing_func_qn: "Proj.path.Class.method" → "Proj.path.Class" */ + const char *last_dot = strrchr(ctx->enclosing_func_qn, '.'); + if (last_dot && last_dot > ctx->enclosing_func_qn) { + size_t class_len = (size_t)(last_dot - ctx->enclosing_func_qn); + char *inferred_class = (char *)cbm_arena_alloc(ctx->arena, class_len + 1); + if (inferred_class) { + memcpy(inferred_class, ctx->enclosing_func_qn, class_len); + inferred_class[class_len] = '\0'; + class_qn = inferred_class; + /* Continue to method lookup with inferred class */ + } else { + return; + } + } else { + return; + } + } else { + return; + } + } const CBMRegisteredFunc *f = php_lookup_method(ctx, class_qn, method_name); if (f) { diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index fa926d3c5..b8a5365eb 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1029,32 +1029,28 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char } } - /* Strategy 3: unique name */ + /* Strategy 3: unique name - require imports for weak name-only matching. + * Without explicit imports, unique_name/suffix_match can fabricate spurious + * cross-language edges (e.g., Python builtin 'get' matching every unrelated + * project 'get'). Strict import requirement reduces false positives at the + * cost of missing legitimate same-language calls in import-sparse codebases. */ + if (!import_vals || import_count == 0) { + return empty_result(); + } + if (arr->count == SKIP_ONE) { if (!receiver_chain_admits(callee_name, arr->items[0])) { return empty_result(); } double conf = CONF_UNIQUE_NAME; - if (import_vals && import_count > 0 && - !is_import_reachable(arr->items[0], import_vals, import_count)) { + if (!is_import_reachable(arr->items[0], import_vals, import_count)) { conf *= DEFAULT_CONFIDENCE; } return (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED}; } /* Strategy 4: multiple candidates */ - if (import_vals && import_count > 0) { - return resolve_multi_with_imports(arr, module_qn, import_vals, import_count); - } - const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn); - if (best) { - if (!receiver_chain_admits(callee_name, best)) { - return empty_result(); - } - double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); - return (cbm_resolution_t){best, "suffix_match", conf, arr->count}; - } - return empty_result(); + return resolve_multi_with_imports(arr, module_qn, import_vals, import_count); } /* The strategy chain shared by both public resolve variants (no caching here —