PHP usages recall: references-driven enrichment + dispatch resolution#231
Merged
Conversation
Carry InitializationOptions on a winning AlternativeCommand — either a static blob or a resolve-time InitOptionsFunc(repoRoot) — through NewProviderFromSpec to the initialize request. When intelephense stands in for an absent phpactor, this roots its storagePath/globalStoragePath under the gortex cache home per repo (honoring XDG overrides) instead of writing to intelephense's default global location outside the engine's isolation.
An LSP server that advertises textDocument/references but no call hierarchy (e.g. intelephense) previously only confirmed existing edges during enrichment — the per-file sweep's call-hierarchy hops never ran, so the dispatch call sites the server can enumerate were never added (edges_added stayed 0). Add a references-driven add pass: for each function/method declaration, query references, attribute every in-repo site to its enclosing caller, and mint an lsp_resolved EdgeCalls at the call-site line (deduping/promoting against existing edges). Targets are ordered dispatch-anchors-first (interface/trait members, abstract methods, then by fan-in) under the targeted deadline budget. A references_add_pass marker rides the enrichment report so index_health distinguishes this add mode from the call-hierarchy mode.
…ierarchy Ambiguous PHP member/scoped calls that static resolution left unresolved (handle/format/write across dozens of handlers) now bind through the class hierarchy, mirroring the language server's fan-out-to- implementations semantics: - parent::/self:: calls walk the enclosing class's extends chain to the nearest declaring type (a precise single bind), so parent::__construct resolves through a multi-level chain, not only the direct parent. - plain member calls whose same-name candidates form an override family related through a common interface, abstract base, or used trait fan out to every implementation. Edges land at the ast_inferred tier (visible in default find_usages) rather than the speculative tier the Java analogue uses, because the PHP dispatch surface is the primary recall source for the language; the relatedness gate keeps precision high by leaving unrelated same-name methods ambiguous. The PHP extractor now records a class's implemented interfaces and an interface's parent interfaces (scope_interfaces meta) so the ancestor closure spans implements + extends + trait use. Runs after the cross-package guard so its edges are never reverted.
A class/trait docblock's @method tags (magic accessors, mixins) had no method_declaration node, so their ids resolved not_found and member calls to them were invisible. Parse the full raw docblock and mint a KindMethod node per @method tag (virtual=phpdoc_method, positioned on the class declaration line so the LSP hover path still works), unless a concrete method of the same name already exists. Classes declaring __call/__callStatic are stamped has_magic_call so the read path knows unresolved members on them are an intentional dynamic surface, not a gap. The virtual nodes are ordinary KindMethod nodes, so the resolver binds calls to them like any other method.
…ites] A synthesized producer that can only mint one edge per (from,to) — the references-add pass, which sees N reference sites for one declaration — previously collapsed those N sites to a single usage row. Keep one edge (primary site in FilePath/Line) and record the extra sites in Meta[call_sites] (sorted, deduped file:line); find_usages expands them into one row per site, deduping against any real per-line edge so a site is never double-counted. Both the GCX and plain-JSON find_usages paths render from the same expanded edge slice. Meta-only, so no Edge struct or wire-contract change; the sites round-trip through the store (PersistEdge encodes Meta) so they survive a warm restart. AST extractors already key edges per call site, so their multiplicity is untouched.
A min_tier that filtered every edge returned a bare empty result
indistinguishable from genuine dead code — and it even attached a
likely_unused / extraction_gap caveat, actively misleading a safety
gate. FilterByMinTier now records a tier_filtered caveat {class,
edges_below_min_tier, max_available_tier} whenever it empties the
visible set while lower-tier edges remain, and that caveat takes
precedence over the zero-edge classification. It rides plain-JSON via
the embedded SubGraph and is emitted into GCX meta. index_health also
gains a per-language lsp_resolved edge rollup (from the enrichment
statuses, no graph pass) so an agent can see up front when
min_tier=lsp_resolved is unusable on a given language.
The C# extractor mints a <file>::<Iface>.<name> KindMethod node for a bodyless interface-method signature (marked iface_member), so its id resolves and dispatch calls bind to it. Add a regression test pinning that behavior — the parity shape PHP's extractMethod establishes.
The Rust extractor mints a <file>::<Trait>.<name> KindMethod node for a bodyless trait-method signature (marked trait_decl) — the node rust-analyzer binds every dispatch call site to. Add a regression test pinning that behavior, parity with C#'s bodyless interface methods and PHP's extractMethod.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lifts PHP
find_usagesrecall from near-zero on OOP-heavy libraries (monolog-shaped:handle/format/writedispatch across dozens of handlers) while preserving the existing precision guard. The dispatch surface is maximally ambiguous by design, so the graph's edges were right but there were almost none of them; this closes the recall gap on two independent fronts — the LSP enrichment path and the in-engine resolver — plus the supporting node/model fixes.LSP enrichment
9535a874).ServerAltcan now carryInitializationOptions(static or a resolve-timeInitOptionsFunc(repoRoot)), threaded throughNewProviderFromSpecto theinitializerequest. When intelephense stands in for an absent phpactor, itsstoragePath/globalStoragePathare rooted under the gortex cache home per repo (honoring XDG) instead of intelephense's default global location.86727bb4) — the load-bearing fix. intelephense advertisestextDocument/referencesbut no call hierarchy, so enrichment previously only confirmed existing edges (edges_addedstuck at 0). A references-driven add pass now queries references per declaration, attributes each in-repo site to its enclosing caller, and mints anlsp_resolvedEdgeCallsat the call site. Provider-generic (any references-capable, call-hierarchy-absent server benefits), dispatch-anchors-first ordering under the existing deadline budget, with areferences_add_passmarker on the enrichment report.In-engine resolver
df171475).parent::/self::calls walk the enclosing class's extends chain to the nearest declaring type (precise single bind —parent::__constructnow resolves through a multi-level chain); plain member calls whose same-name candidates form an override family related through a common interface / abstract base / used trait fan out to every implementation. Edges land atast_inferred(visible in defaultfind_usages), and the relatedness gate keeps precision high by leaving unrelated same-name methods ambiguous. The PHP extractor now recordsimplements/ interface-extends(scope_interfaces) so the ancestor closure spans implements + extends + trait use.@methodvirtuals and mark__callclasses (f7e3e9d9). Docblock@methodtags (magic accessors, mixins) get aKindMethodnode so their ids resolve and calls bind; classes declaring__call/__callStaticare stampedhas_magic_call.Cross-cutting model fixes
072b3a22). A synthesized producer that mints one edge per (from,to) records extra sites inEdge.Meta[call_sites];find_usagesexpands them into one row per site, deduped against any real per-line edge. Meta-only (noEdgestruct / wire change); survives a warm restart. AST extractors already key per call site, so their multiplicity is untouched.f1f671c8). Amin_tierthat emptied the result was indistinguishable from dead code (and even attached alikely_unusedcaveat).FilterByMinTiernow records atier_filteredcaveat{class, edges_below_min_tier, max_available_tier}that takes precedence over the zero-edge classification;index_healthgains a per-languagelsp_resolvededge rollup.50328900,7f65ffb1) — regression tests pinning that the C# and Rust extractors mint<file>::<Iface|Trait>.<name>KindMethodnodes for bodyless signatures (parity with PHP'sextractMethod).Verification
Per commit:
go build ./cmd/gortex/+go test -raceon the touched packages, golangci-lint, and the cmd/gortex golden. Rebased onto currentmain; post-rebase the full repo builds and 6099-racetests across the eight touched packages pass, lint clean.