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
241 lines
7.4 KiB
Go
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
|
|
}
|