Files
zzet--gortex/internal/resolver/java_override_dispatch.go
wehub-resource-sync a06f331eb8
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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

241 lines
7.4 KiB
Go

package resolver
import (
"strings"
"github.com/zzet/gortex/internal/graph"
)
// javaOverrideDispatchCap bounds how many overrides a single ambiguous 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 rather than sprayed
// across the graph.
const javaOverrideDispatchCap = 8
// resolveJavaOverrideDispatch fans out an ambiguous Java member call whose
// same-name candidates are overrides related through the class hierarchy into
// one call edge per override — the call-hierarchy semantics jdtls and gopls
// present, where a call on a supertype-typed receiver is a usage of every
// override in that hierarchy. Without this, a `x.toString()` site whose static
// type is a base class stays unresolved (two candidate overrides, no exact
// type match) and reports as a usage of neither override.
//
// Because the picked target set is a best guess over legal runtime targets that
// no receiver type disambiguated, the edges land at the speculative tier
// (OriginSpeculative + Meta["speculative"]): hidden from default find_usages so
// they never inflate a code symbol's usage set, surfaced on demand with
// include_speculative and via analyze kind=speculative, and marked
// Meta["dispatch"]="override". Resolving the primary out of the
// `unresolved::*` state also clears the ambiguous_multi_match classification.
// Scoped to Java so Go/TS/Python dispatch presentation is unchanged.
func (r *Resolver) resolveJavaOverrideDispatch() int {
g := r.graph
if g == nil {
return 0
}
ancestors := javaTypeAncestors(g)
if len(ancestors) == 0 {
return 0
}
type fanout struct {
edge *graph.Edge
base *graph.Node
others []*graph.Node
}
var jobs []fanout
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 != "java" {
continue
}
cands := javaOverrideCandidates(r.cachedFindNodesByNameInRepo(name, r.callerRepoPrefix(e)))
if len(cands) < 2 || len(cands) > javaOverrideDispatchCap {
continue
}
if !javaOverridesRelated(cands, ancestors) {
continue
}
jobs = append(jobs, fanout{edge: e, base: cands[0], others: cands[1:]})
}
n := 0
for _, j := range jobs {
oldTo := j.edge.To
j.edge.To = j.base.ID
j.edge.Origin = graph.OriginSpeculative
j.edge.Confidence = 0.3
if j.edge.Meta == nil {
j.edge.Meta = map[string]any{}
}
j.edge.Meta[graph.MetaSpeculative] = true
j.edge.Meta["dispatch"] = "override"
g.ReindexEdges([]graph.EdgeReindex{{Edge: j.edge, OldTo: oldTo}})
n++
for _, o := range j.others {
g.AddEdge(&graph.Edge{
From: j.edge.From, To: o.ID, Kind: graph.EdgeCalls,
FilePath: j.edge.FilePath, Line: j.edge.Line,
Origin: graph.OriginSpeculative,
Confidence: 0.3,
Meta: map[string]any{graph.MetaSpeculative: true, "dispatch": "override"},
})
n++
}
}
return n
}
// javaUnresolvedMemberName returns the method name of an `unresolved::*.<name>`
// member-call target, or "" for any other target shape.
func javaUnresolvedMemberName(to string) string {
name := graph.UnresolvedName(to)
if name == "" {
return ""
}
rest, ok := strings.CutPrefix(name, "*.")
if !ok || strings.Contains(rest, "::") {
return ""
}
return rest
}
// javaOverrideCandidates filters name-matched nodes to the in-repo Java method
// definitions, one per declaring type (deduped by receiver), excluding stubs
// and definitions with no declaring type.
func javaOverrideCandidates(raw []*graph.Node) []*graph.Node {
var out []*graph.Node
seen := map[string]bool{}
for _, n := range raw {
if n == nil || n.Language != "java" || 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
}
// javaOverridesRelated reports whether the candidate methods are overrides of a
// common supertype: their declaring types share at least one common ancestor
// (or one is an ancestor of another). This is the precision gate — same-name
// methods on unrelated types (no shared ancestor) are never sprayed together,
// while genuine overrides of a common base (two entities overriding
// BaseEntity's toString) fan out to every override the way a language server's
// call hierarchy attributes them.
func javaOverridesRelated(cands []*graph.Node, ancestors map[string]map[string]bool) bool {
var common map[string]bool
for i, c := range cands {
rc := nodeReceiverType(c)
if rc == "" {
return false
}
// Ancestor-or-self set of this candidate's declaring type.
set := map[string]bool{rc: true}
for a := range ancestors[rc] {
set[a] = true
}
if i == 0 {
common = set
continue
}
for k := range common {
if !set[k] {
delete(common, k)
}
}
if len(common) == 0 {
return false
}
}
return len(common) > 0
}
// javaBaseTypeName reduces a possibly package-qualified, generic type reference
// to its simple class name (`model.Person<X>` → `Person`).
func javaBaseTypeName(s string) string {
s = strings.TrimSpace(s)
if i := strings.IndexByte(s, '<'); i >= 0 {
s = s[:i]
}
if i := strings.LastIndexByte(s, '.'); i >= 0 {
s = s[i+1:]
}
return s
}
// javaTypeAncestors builds, for each Java type simple name, the transitive set
// of its superclass simple names. The direct superclass is read from each type
// node's scope_parent meta — the same source the scope resolver's super-method
// walk uses — because regular Java `extends` is recorded there, not as a graph
// EdgeExtends (only anonymous classes emit that). So a cross-package
// inheritance chain (`owner.Owner extends model.Person extends model.BaseEntity`)
// contributes to the hierarchy even though its supertype references never
// resolved to a type node. Empty when the graph indexes no Java hierarchy.
func javaTypeAncestors(g graph.Store) map[string]map[string]bool {
direct := map[string]map[string]bool{}
add := func(childName, parentName string) {
if childName == "" || parentName == "" || childName == parentName {
return
}
set := direct[childName]
if set == nil {
set = map[string]bool{}
direct[childName] = set
}
set[parentName] = true
}
for _, kind := range []graph.NodeKind{graph.KindType, graph.KindInterface} {
for n := range g.NodesByKind(kind) {
if n == nil || n.Language != "java" || n.Name == "" || n.Meta == nil {
continue
}
if p, ok := n.Meta[MetaScopeParentClass].(string); ok {
add(n.Name, javaBaseTypeName(p))
}
}
}
if len(direct) == 0 {
return nil
}
// Transitive closure via DFS from each type.
closure := make(map[string]map[string]bool, len(direct))
var visit func(t string, acc map[string]bool, 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 closure
}