chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zzet/gortex/internal/graph"
|
||||
)
|
||||
|
||||
// phpOverrideDispatchCap bounds how many overrides a single ambiguous PHP
|
||||
// call may fan out to. A name shared by more definitions than this is too
|
||||
// generic to attribute confidently, so the call is left ambiguous.
|
||||
const phpOverrideDispatchCap = 8
|
||||
|
||||
// resolvePHPOverrideDispatch binds ambiguous PHP member / scoped calls that
|
||||
// static resolution left unresolved, using the class hierarchy:
|
||||
//
|
||||
// - scope_kind parent|self → the enclosing class's ancestor chain is
|
||||
// walked to the nearest type declaring the method (a precise single
|
||||
// bind). This makes parent::__construct() resolve through a multi-level
|
||||
// extends chain, not only the direct parent.
|
||||
// - plain member calls → when the same-name candidates form an
|
||||
// override family related through the hierarchy (a common interface /
|
||||
// abstract base / used trait), the call fans out to every override — the
|
||||
// fan-out-to-implementations semantics a language server presents.
|
||||
//
|
||||
// Edges land at the ast_inferred tier (visible in default find_usages, unlike
|
||||
// the speculative Java analogue) because the PHP dispatch surface is the
|
||||
// primary recall source for the language and the relatedness gate keeps
|
||||
// precision high: an untyped receiver whose candidates share no common
|
||||
// ancestor is left unresolved rather than sprayed repo-wide. Runs AFTER the
|
||||
// cross-package guard so its edges are never reverted. PHP-only.
|
||||
func (r *Resolver) resolvePHPOverrideDispatch() int {
|
||||
g := r.graph
|
||||
if g == nil {
|
||||
return 0
|
||||
}
|
||||
direct, closure := r.phpTypeHierarchy()
|
||||
if len(direct) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type job struct {
|
||||
edge *graph.Edge
|
||||
single *graph.Node // scope-bind target; nil for a fan-out job
|
||||
base *graph.Node
|
||||
others []*graph.Node
|
||||
}
|
||||
var jobs []job
|
||||
|
||||
// Collect first — mutating the graph while ranging EdgesByKind is unsafe.
|
||||
for e := range g.EdgesByKind(graph.EdgeCalls) {
|
||||
if e == nil || e.IsSpeculative() {
|
||||
continue
|
||||
}
|
||||
// Scoped warm pass: an unchanged repo's calls were already dispatched (or
|
||||
// left ambiguous) by a prior full pass over the same hierarchy, so only
|
||||
// reconsider the changed repos' calls.
|
||||
if !r.edgeFromInScope(e.From) {
|
||||
continue
|
||||
}
|
||||
name := javaUnresolvedMemberName(e.To)
|
||||
if name == "" || strings.HasSuffix(name, ".<init>") {
|
||||
continue
|
||||
}
|
||||
caller := r.cachedGetNode(e.From)
|
||||
if caller == nil || caller.Language != "php" {
|
||||
continue
|
||||
}
|
||||
repo := r.callerRepoPrefix(e)
|
||||
|
||||
// Scope path: parent:: / self:: / static:: bind precisely up the
|
||||
// enclosing class's ancestor chain.
|
||||
if sk := phpEdgeMetaString(e, "scope_kind"); sk == "parent" || sk == "self" {
|
||||
encloser := phpEnclosingClass(caller)
|
||||
if encloser == "" {
|
||||
continue
|
||||
}
|
||||
var start []string
|
||||
if sk == "parent" {
|
||||
for p := range direct[encloser] {
|
||||
start = append(start, p)
|
||||
}
|
||||
} else {
|
||||
start = []string{encloser}
|
||||
}
|
||||
if len(start) == 0 {
|
||||
continue
|
||||
}
|
||||
if target := r.nearestPHPMethod(name, start, direct, repo); target != nil && target.ID != caller.ID {
|
||||
jobs = append(jobs, job{edge: e, single: target})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Fan-out path: a plain member call whose same-name candidates are an
|
||||
// override family related through the hierarchy.
|
||||
cands := phpOverrideCandidates(r.cachedFindNodesByNameInRepo(name, repo))
|
||||
if len(cands) < 2 || len(cands) > phpOverrideDispatchCap {
|
||||
continue
|
||||
}
|
||||
if !javaOverridesRelated(cands, closure) {
|
||||
continue
|
||||
}
|
||||
jobs = append(jobs, job{edge: e, base: cands[0], others: cands[1:]})
|
||||
}
|
||||
|
||||
n := 0
|
||||
for _, j := range jobs {
|
||||
if j.single != nil {
|
||||
if j.edge.To == j.single.ID {
|
||||
continue
|
||||
}
|
||||
oldTo := j.edge.To
|
||||
j.edge.To = j.single.ID
|
||||
j.edge.Origin = graph.OriginASTInferred
|
||||
if j.edge.Confidence < phpDispatchConfidence {
|
||||
j.edge.Confidence = phpDispatchConfidence
|
||||
}
|
||||
phpEnsureMeta(j.edge)["dispatch"] = "scope"
|
||||
g.ReindexEdges([]graph.EdgeReindex{{Edge: j.edge, OldTo: oldTo}})
|
||||
n++
|
||||
continue
|
||||
}
|
||||
oldTo := j.edge.To
|
||||
j.edge.To = j.base.ID
|
||||
j.edge.Origin = graph.OriginASTInferred
|
||||
j.edge.Confidence = phpDispatchConfidence
|
||||
phpEnsureMeta(j.edge)["dispatch"] = "override"
|
||||
g.ReindexEdges([]graph.EdgeReindex{{Edge: j.edge, OldTo: oldTo}})
|
||||
n++
|
||||
for _, o := range j.others {
|
||||
if phpEdgeExists(g, j.edge.From, o.ID) {
|
||||
continue // idempotent: the extra override edge is already present
|
||||
}
|
||||
g.AddEdge(&graph.Edge{
|
||||
From: j.edge.From, To: o.ID, Kind: graph.EdgeCalls,
|
||||
FilePath: j.edge.FilePath, Line: j.edge.Line,
|
||||
Origin: graph.OriginASTInferred,
|
||||
Confidence: phpDispatchConfidence,
|
||||
Meta: map[string]any{"dispatch": "override"},
|
||||
})
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// phpDispatchConfidence is the confidence stamped on a dispatch-resolved edge
|
||||
// — in the ast_inferred band so DefaultOriginFor agrees with the explicit
|
||||
// Origin and the read path keeps the edge visible.
|
||||
const phpDispatchConfidence = 0.7
|
||||
|
||||
// phpTypeHierarchy builds the PHP class hierarchy: `direct` maps each type's
|
||||
// simple name to its DIRECT parents (superclass via scope_parent, implemented
|
||||
// / extended interfaces via scope_interfaces, used traits via EdgeExtends),
|
||||
// and `closure` is the transitive ancestor set used by the relatedness gate.
|
||||
func (r *Resolver) phpTypeHierarchy() (direct, closure map[string]map[string]bool) {
|
||||
g := r.graph
|
||||
direct = map[string]map[string]bool{}
|
||||
add := func(child, parent string) {
|
||||
child = phpBaseTypeName(child)
|
||||
parent = phpBaseTypeName(parent)
|
||||
if child == "" || parent == "" || child == parent {
|
||||
return
|
||||
}
|
||||
set := direct[child]
|
||||
if set == nil {
|
||||
set = map[string]bool{}
|
||||
direct[child] = set
|
||||
}
|
||||
set[parent] = true
|
||||
}
|
||||
for _, kind := range []graph.NodeKind{graph.KindType, graph.KindInterface} {
|
||||
for n := range g.NodesByKind(kind) {
|
||||
if n == nil || n.Language != "php" || n.Name == "" || n.Meta == nil {
|
||||
continue
|
||||
}
|
||||
if p, ok := n.Meta[MetaScopeParentClass].(string); ok {
|
||||
add(n.Name, p)
|
||||
}
|
||||
if ifaces, ok := n.Meta["scope_interfaces"].(string); ok && ifaces != "" {
|
||||
for _, iface := range strings.Split(ifaces, ",") {
|
||||
add(n.Name, iface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Trait `use` (and any other extends-shaped) relationship is a graph edge.
|
||||
for e := range g.EdgesByKind(graph.EdgeExtends) {
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
from := r.cachedGetNode(e.From)
|
||||
if from == nil || from.Language != "php" || from.Name == "" {
|
||||
continue
|
||||
}
|
||||
add(from.Name, phpEdgeTargetName(g, e.To))
|
||||
}
|
||||
if len(direct) == 0 {
|
||||
return direct, nil
|
||||
}
|
||||
closure = make(map[string]map[string]bool, len(direct))
|
||||
var visit func(t string, acc, seen map[string]bool)
|
||||
visit = func(t string, acc, seen map[string]bool) {
|
||||
for p := range direct[t] {
|
||||
if seen[p] {
|
||||
continue
|
||||
}
|
||||
seen[p] = true
|
||||
acc[p] = true
|
||||
visit(p, acc, seen)
|
||||
}
|
||||
}
|
||||
for t := range direct {
|
||||
acc := map[string]bool{}
|
||||
visit(t, acc, map[string]bool{t: true})
|
||||
closure[t] = acc
|
||||
}
|
||||
return direct, closure
|
||||
}
|
||||
|
||||
// nearestPHPMethod walks up the hierarchy from start (breadth-first, nearest
|
||||
// first) to the first ancestor type declaring method name, returning that
|
||||
// method node. Used by the parent::/self:: scope bind.
|
||||
func (r *Resolver) nearestPHPMethod(name string, start []string, direct map[string]map[string]bool, repo string) *graph.Node {
|
||||
byRecv := map[string]*graph.Node{}
|
||||
for _, m := range r.cachedFindNodesByNameInRepo(name, repo) {
|
||||
if m == nil || m.Language != "php" || m.Kind != graph.KindMethod {
|
||||
continue
|
||||
}
|
||||
if graph.IsStub(m.ID) || graph.IsUnresolvedTarget(m.ID) {
|
||||
continue
|
||||
}
|
||||
if rc := nodeReceiverType(m); rc != "" {
|
||||
if _, ok := byRecv[rc]; !ok {
|
||||
byRecv[rc] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(byRecv) == 0 {
|
||||
return nil
|
||||
}
|
||||
visited := map[string]bool{}
|
||||
queue := append([]string{}, start...)
|
||||
for _, s := range start {
|
||||
visited[s] = true
|
||||
}
|
||||
for len(queue) > 0 {
|
||||
cur := queue[0]
|
||||
queue = queue[1:]
|
||||
if m := byRecv[cur]; m != nil {
|
||||
return m
|
||||
}
|
||||
for p := range direct[cur] {
|
||||
if !visited[p] {
|
||||
visited[p] = true
|
||||
queue = append(queue, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// phpOverrideCandidates filters name-matched nodes to in-repo PHP method
|
||||
// definitions, one per declaring type (deduped by receiver), excluding stubs
|
||||
// and definitions with no declaring type.
|
||||
func phpOverrideCandidates(raw []*graph.Node) []*graph.Node {
|
||||
var out []*graph.Node
|
||||
seen := map[string]bool{}
|
||||
for _, n := range raw {
|
||||
if n == nil || n.Language != "php" || n.Kind != graph.KindMethod {
|
||||
continue
|
||||
}
|
||||
if graph.IsStub(n.ID) || graph.IsUnresolvedTarget(n.ID) {
|
||||
continue
|
||||
}
|
||||
recv := nodeReceiverType(n)
|
||||
if recv == "" || seen[recv] {
|
||||
continue
|
||||
}
|
||||
seen[recv] = true
|
||||
out = append(out, n)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// phpEnclosingClass returns the simple name of the class a caller node belongs
|
||||
// to (its receiver / scope_class), or "" for a free function.
|
||||
func phpEnclosingClass(caller *graph.Node) string {
|
||||
if caller == nil || caller.Meta == nil {
|
||||
return ""
|
||||
}
|
||||
if r, ok := caller.Meta["receiver"].(string); ok && r != "" {
|
||||
return phpBaseTypeName(r)
|
||||
}
|
||||
if r, ok := caller.Meta["scope_class"].(string); ok && r != "" {
|
||||
return phpBaseTypeName(r)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// phpEdgeExists reports whether a call edge from→to already exists (used to
|
||||
// keep the fan-out idempotent across re-resolution).
|
||||
func phpEdgeExists(g graph.Store, from, to string) bool {
|
||||
for _, e := range g.GetOutEdges(from) {
|
||||
if e != nil && e.To == to && e.Kind == graph.EdgeCalls {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// phpEdgeTargetName resolves an edge target id to a simple type name, handling
|
||||
// both an unresolved::<name> stub and a resolved node id.
|
||||
func phpEdgeTargetName(g graph.Store, to string) string {
|
||||
if graph.IsUnresolvedTarget(to) {
|
||||
return graph.UnresolvedName(to)
|
||||
}
|
||||
if n := g.GetNode(to); n != nil {
|
||||
return n.Name
|
||||
}
|
||||
return graph.UnresolvedName(to)
|
||||
}
|
||||
|
||||
// phpBaseTypeName reduces a possibly namespace-qualified PHP type reference to
|
||||
// its simple name (`App\Service\User` → `User`, `\Foo` → `Foo`).
|
||||
func phpBaseTypeName(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.TrimPrefix(s, `\`)
|
||||
if i := strings.LastIndexByte(s, '\\'); i >= 0 {
|
||||
s = s[i+1:]
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// phpEdgeMetaString reads a string-valued edge meta key, "" when absent.
|
||||
func phpEdgeMetaString(e *graph.Edge, key string) string {
|
||||
if e == nil || e.Meta == nil {
|
||||
return ""
|
||||
}
|
||||
if s, ok := e.Meta[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// phpEnsureMeta returns the edge's meta map, allocating it when nil.
|
||||
func phpEnsureMeta(e *graph.Edge) map[string]any {
|
||||
if e.Meta == nil {
|
||||
e.Meta = map[string]any{}
|
||||
}
|
||||
return e.Meta
|
||||
}
|
||||
Reference in New Issue
Block a user