From 70926a2956ce986bfacbf6e7ebfd398fd91ff266 Mon Sep 17 00:00:00 2001 From: "carnival.zhang" Date: Thu, 10 Sep 2026 18:12:47 +0800 Subject: [PATCH 1/2] fix(php): add fallback for self:: static calls when enclosing_class_qn is NULL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: PHP LSP's resolve_static_call() returns early when enclosing_class_qn is NULL, causing self::/static::/parent:: calls to fall back to low-confidence heuristic strategies (unique_name 0.75 or suffix_match 0.55) instead of high-confidence php_self_static (0.95). Root Cause: When process_class_decl doesn't populate enclosing_class_qn (e.g., complex method bodies with closures), resolve_static_call short-circuits without generating any LSP resolution, leaving pass_calls.c to use registry fallbacks. Fix: Add fallback logic to infer class_qn from enclosing_func_qn when handling self::/static:: scopes. Extract class QN by stripping the last .method segment from "Project.path.Class.method" → "Project.path.Class", then continue with normal method lookup and emit php_self_static strategy at 0.95 confidence. Impact: - PHP only (no other languages affected) - Improves self:: call resolution from 0.55-0.75 → 0.95 confidence - Verified on iCMS v8.0.0: AdminAdmincp.do_index self::setContext/tableData/view Co-Authored-By: Claude Sonnet 4.5 --- internal/cbm/lsp/php_lsp.c | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) 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) { From 9115f5154383bd091e125c956aa4f241ebac1c7b Mon Sep 17 00:00:00 2001 From: "carnival.zhang" Date: Thu, 10 Sep 2026 18:20:22 +0800 Subject: [PATCH 2/2] feat(registry): require explicit imports for unique_name/suffix_match strategies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: weak name-only matching (unique_name/suffix_match) can fabricate spurious cross-language edges when no imports are present, e.g., Python builtin 'get' matching every unrelated project 'get' function. Change: Add strict import requirement in resolve_name_lookup() - return empty result when import_vals is NULL/empty, forcing reliance on high-confidence strategies (import_map 0.95, same_module 0.90) only. Trade-off: - Eliminates false positive cross-language edges - May miss legitimate same-language calls in import-sparse codebases - Reduces noise in call graphs at the cost of coverage Impact: - unique_name: was 0.75 → 0.375 (degraded) → now rejected entirely - suffix_match: was 0.55 (module distance) → now rejected entirely - High-confidence strategies unaffected Co-Authored-By: Claude Sonnet 4.5 --- src/pipeline/registry.c | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) 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 —