Repository navigation
Description
Version: codebase-memory-mcp v0.8.1 (Linux amd64, release binary)
Project: real-world Drupal 11 / PHP module, 2,869 nodes / 7,069 edges
Public fixture: the repo is public at https://git.drupalcode.org/project/ai_eval (clone + index_repository, full mode)
Summary
On PHP, the generic short-name call resolver wires framework / core method calls to unrelated project methods that merely share a method name, ignoring the receiver's type. The real callee (a Drupal core method like EntityStorageInterface::load()) is not a node in the graph — it's an external dependency — so the resolver picks the only same-named definition it can see (a project method) and fabricates a CALLS edge.
This is the same root cause already fixed for Perl in #476/#477 ("the textual resolver … falls back to a generic short-name matcher with no language or call-kind awareness … wires framework method calls … to unrelated project subs that merely share a name"), and the same name-vs-receiver defect reported for the recursion detector in #599. The fix in #466 does not cover this case: #466 ranks among same-named definition nodes and reports ties, but here there is no tie — the correct target is out-of-graph entirely, so #466 has nothing to prefer.
Repro
MATCH (caller)-[:CALLS]->(m)
WHERE m.qualified_name ENDS WITH 'DatasetLoaderInterface.load'
RETURN caller.qualified_name
Three representative false positives (verified against source — none of these call the dataset loader):
| Edge (caller → callee) | Actual call in body | Why it's wrong |
|---|---|---|
AiLogTraceSource::traceById → DatasetLoaderInterface::load |
$this->logStorage()->load($id) (src/Service/AiLogTraceSource.php:90) |
receiver is the ai_log entity storage (Drupal core EntityStorageInterface::load), not the dataset loader |
LabelStore::loadForTrace → DatasetLoaderInterface::load |
$storage->load(reset($ids)) where $storage = getStorage('ai_eval_review_label') (src/Service/LabelStore.php, loadForTrace()) |
receiver is the review-label entity storage, not the dataset loader |
build_viewer.load_log → DatasetSourceInterface::load |
a Python function in skills/view-results/scripts/build_viewer.py |
cross-language edge: a Python function cannot call a PHP interface method |
Inconsistency worth noting: the resolver gets EvalRunner::doStartRun/runTarget → DatasetLoaderInterface::load correct (those genuinely call $this->datasetLoader->load()) while getting AiLogTraceSource/LabelStore wrong — so identical-looking ->load() call sites resolve to the right or wrong target non-deterministically from the consumer's point of view.
Minimal example
// Project interface — the only `load()` definition node in the graph
interface DatasetLoaderInterface { public function load(string $ref): array; }
final class TraceSource {
public function traceById(int $id): ?array {
// receiver is a Drupal core EntityStorageInterface — NOT in the graph.
// Today this emits a phantom CALLS edge to DatasetLoaderInterface::load.
return $this->logStorage()->load($id);
}
}Expected
- A
CALLSedge should be emitted only when the call resolves to the same method on a compatible receiver type. When the receiver type is unknown/external, prefer no edge (or a low-confidence/unresolved marker) over a fabricated edge to a same-named project method. $storage->load()/$this->logStorage()->load()(core/framework methods not in the graph) must not bind to a projectload()purely on short-name match — exactly the call-kind awareness that Perl call graph polluted by false-positive CALLS edges (builtins, framework method calls, config strings) #476 added for Perl, applied to PHP.- Cross-language edges (Python caller → PHP callee) should never be produced.
Impact
trace_path (inbound) and any query_graph over CALLS return phantom callers for the affected methods, so caller-set / impact-analysis answers are wrong for the most common Drupal/PHP idiom (->load(), and by extension ->save(), ->get(), ->create(), ->delete() — all core storage/entity methods). PHP call-graph coverage on the tracker is currently thin (only Blade #258 and a vendor-exclude #234, both closed), so this is offered as a clean, public PHP/Drupal fixture for the same fix #476 shipped for Perl.
Related
- Perl call graph polluted by false-positive CALLS edges (builtins, framework method calls, config strings) #476 / fix(perl): eliminate false-positive Perl CALLS edges (builtins, framework method calls, config strings) #477 — same defect, fixed for Perl only
- [bug]
self_recursive/unguarded_recursionmatch calls by bare name →super().save()and same-named wrappers are false positives #599 — same name-vs-receiver root cause, recursion detector - fix(mcp): prefer definitions and report ambiguity in name resolution #466 — definition-preference fix; does not cover out-of-graph receivers
- task: Graph queries & trace_path resolution #594 — trace_path/query resolution umbrella (those sub-issues are under-resolution; this is over-resolution)
Activity
gkastanis commentedon Jun 24, 2026
Update — minimal repros sharpen the mechanism (this is narrower than my original wording).
My initial framing implied "PHP isn't type-resolved." That's wrong, and I want to correct it so the fix targets the right place. Tested on v0.8.1 (Linux amd64) with three minimal single-purpose repos:
1. Explicit property type, both candidates in-graph → resolves correctly.
run→StorageInterface::load✅ (picks the right one via the type hint)2. Method-return-typed receiver, type in-graph → also resolves correctly.
run→StorageInterface::load✅3. Receiver type EXTERNAL / not indexed → phantom edge.
run→LoaderInterface::load❌ — the only projectloadin the repo. The correct target (EntityStorageInterface::load) isn't a node, so resolution falls back to bare-name and binds to an arbitrary same-named project method.So the defect is specifically: when a receiver's type resolves but is not a node in the graph, the resolver drops to the bare-name fallback instead of declining. This is exactly the Drupal
$this->entityTypeManager->getStorage(...)->load()idiom — core types are never indexed (they're undervendor//web/core, which the tool correctly gitignores), so every->load()/->save()/->get()into core is a phantom-edge candidate.Confirmed that indexing the whole site does not help: a full Drupal site index contains only the custom code (0 matches for
EntityStorageInterface,EntityTypeManagerInterface,ContentEntityBaseafter indexing one), because core/contrib/vendor are gitignored.Suggested fix: when the receiver type is resolved but has no corresponding node in the graph, emit no
CALLSedge (or anunresolvedmarker) rather than the bare-name/suffix fallback. The in-graph cases above already work, so this is a narrow change to the out-of-graph branch. Happy to share the three repro repos.kumayizxcvb commentedon Jun 24, 2026
Tree-sitter 的解析速度确实无敌,但单纯靠语法树做语义分析经常会被动态语言的“魔法”搞晕。你们这个 Hybrid LSP 的思路很对胃口,不过把 LSP Server 这种内存大户和易阻塞进程塞进单静态二进制里,是怎么解决进程间通信和超时容错的?之前用 Go 封装过类似方案,经常因为某个语言服务器崩溃导致整个索引挂起,C 这边处理这种并发模型感觉挑战更大。
DeusData commentedon Jun 24, 2026
We basically built a light version of the LSP Servers, where we attempt to only use the type resolution logic, not everything else. Bundled with tree-sitter this leads to the "hybrid". It's still a bit experimental but I am working on tuning it. Maybe have a look, feedback is always appreciated :)
DeusData commentedon Jun 24, 2026
@halindrome could this be an issue for you? It's related to your pearl work and could be a good fit :)
DeusData commentedon Jun 29, 2026
Thanks. I’ve triaged this under #592. This is a receiver/type-aware call-resolution issue; the fixed Perl class is a useful precedent, but PHP/Drupal needs its own fixture proving framework/core methods do not resolve to same-named project methods when the receiver type is external.
fix(extract): flag TS/JS member calls with non-this receivers as meth…
rudi193-cmd commentedon Jul 5, 2026
Same resolver family — Python store wrappers (willow-2.0)
Different language, same bare-name / receiver-blind pattern as your Drupal
$storage->load()report.On v0.8.1 / willow-2.0,
core.soil.getis flaggedunguarded_recursion+self_recursivebecause the body calls_get_store().get(...)— not a self-call. Same onsoil.put→_get_store().put(...).This is the measurement-side cousin of false-positive CALLS edges: the resolver matches name without receiver. We filed #876 (recursion flags) and document F-007/F-008 in our tool-eval addendum.
A fix that disambiguates receiver type for framework/store calls would likely touch the same subsystem as #476/#599/#725.
fix(py-lsp): activate Python weak-method suppression, follow re-expor…
feat(lsp): resolve cross-file base classes for the Python/TS cross-LSP
fix(pipeline): suppress weak short-name matches for Go selector calls
fix(pipeline): suppress weak short-name matches for Go selector calls
fix(pipeline): run the external-import guard after route classification
fix(scala): suppress weak short-name matches for receiver calls
fix(scala): suppress weak short-name matches for receiver calls
wazum commentedon Sep 30, 2026
@DeusData I can reproduce this on v0.11.0 with a real PHP project of about 200 files. Many weak
CALLSedges link framework calls to project methods that only share the name.I have a small fix ready. When the PHP LSP knows the receiver's class but that class is not indexed, it already emits
php_method_typed_unindexed. That row is below the confidence floor, so the pipeline skips it, and the name fallback links the call anyway. My change stops the weak fallback in that case, unless the project has a class with the same short name.pass_calls.candpass_parallel.cget the same check.There are tests for the sequential and the parallel path:
Calls on a receiver typed as a project class still resolve, and so do
$this->calls. On the real project the fix removes 55 wrong edges and keeps all typed edges. It does not cover untyped receivers. Those need a separate change.Can I open a PR for this?
fix(scala): suppress weak short-name matches for receiver calls
fix(scala): suppress weak short-name matches for receiver calls
DeusData commentedon Oct 17, 2026
Thank you, @wazum, for digging into this and for offering a fix, and sorry it took us a while to come back to you. Yes, please open the PR!
Some context so you can scope it. Since your comment, #2520 (merged on 12 October) added typed, per-file import evidence for PHP. A receiver typed as a class imported from a vendor namespace now gets a
php_external_receiverrow at 0.95, and the name fallback no longer binds that call to a same-named project method.Your change covers the next case, which is still open on main: the PHP LSP knows the receiver's class (
php_method_typed_unindexed), but the import evidence cannot prove the class is a vendor one. That row is emitted at 0.55, belowCBM_LSP_CONFIDENCE_FLOOR, so the pipeline ignores it and the weak name fallback links the call anyway. Exactly the gap you found.What would help us merge it quickly:
src/pipeline/pass_calls.candsrc/pipeline/pass_parallel.c(the sequential resolver runs for repos with 50 files or fewer, the parallel one above that), gated on PHP files, with a test for each path;File $file->getUid()case, plus a control showing that a receiver typed as a project class still resolves;Signed-off-byon every commit (git commit -s, DCO);Untyped receivers (for example
$storage->load()where nothing types$storage) are a separate change we are working on, so please leave those out. Thanks again!DeusData commentedon Oct 20, 2026
Typed vendor receivers are now handled on main by 9a2d1d6 (PR #2710, thanks to @wazum): a typed receiver whose class is only known from vendor code no longer falls back to a name-guess CALLS edge, in both resolvers. Untyped receivers such as
$storage->load()are still open here.DeusData commentedon Oct 25, 2026
Thank you for your patience. Sorry for the delay. The October20 update records typed vendor receiver handling as landed; untyped receivers remain the follow-up here. I’ll follow up soon.
fix(pipeline): suppress weak short-name matches for Go selector calls