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,240 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user