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
229 lines
7.1 KiB
Go
229 lines
7.1 KiB
Go
package mcp
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"github.com/zzet/gortex/internal/astquery"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// This file owns enclosing-scope resolution shared across the search
|
|
// and AST tools: the per-file line->symbol index (fileSymbolIndex)
|
|
// and the enclosing-owner derivation (enclosingName). search_ast,
|
|
// search_text, search_symbols, and the analyze-* detectors all need
|
|
// to answer "which symbol contains this?" -- they share this code so
|
|
// the answer stays consistent.
|
|
|
|
// buildFileSymbolIndex returns one fileSymbolIndex per Target's graph
|
|
// path. Building all indexes up-front (instead of lazily on first
|
|
// match) is fine because the cost is one graph pass per file's
|
|
// symbol list, and the alternative — locking inside the worker pool
|
|
// hot path — is worse for parallel runs.
|
|
func (s *Server) buildFileSymbolIndex(targets []astquery.Target) map[string]*fileSymbolIndex {
|
|
if s.graph == nil {
|
|
return nil
|
|
}
|
|
wanted := make(map[string]struct{}, len(targets))
|
|
for _, t := range targets {
|
|
wanted[t.GraphPath] = struct{}{}
|
|
}
|
|
out := make(map[string]*fileSymbolIndex, len(wanted))
|
|
for _, n := range s.graph.AllNodes() {
|
|
if _, ok := wanted[n.FilePath]; !ok {
|
|
continue
|
|
}
|
|
// Functions, methods, closures are the meaningful
|
|
// "enclosing scope" candidates. KindType (struct/class)
|
|
// is included too so a match in a class-body declaration
|
|
// still gets a symbol_id (e.g. a Java field that's
|
|
// flagged by string-equality detection).
|
|
switch n.Kind {
|
|
case graph.KindFunction, graph.KindMethod, graph.KindClosure, graph.KindType, graph.KindInterface:
|
|
idx := out[n.FilePath]
|
|
if idx == nil {
|
|
idx = &fileSymbolIndex{}
|
|
out[n.FilePath] = idx
|
|
}
|
|
idx.add(n)
|
|
}
|
|
}
|
|
for _, idx := range out {
|
|
idx.finalise()
|
|
}
|
|
return out
|
|
}
|
|
|
|
// fileSymbolIndex is the per-file lookup used by the SymbolLookup
|
|
// closure. We keep symbols sorted by [StartLine, EndLine] descending
|
|
// width so `find` returns the deepest enclosing scope (a closure
|
|
// inside a method beats the method itself).
|
|
type fileSymbolIndex struct {
|
|
syms []*graph.Node
|
|
}
|
|
|
|
func (i *fileSymbolIndex) add(n *graph.Node) { i.syms = append(i.syms, n) }
|
|
|
|
func (i *fileSymbolIndex) finalise() {
|
|
sort.Slice(i.syms, func(a, b int) bool {
|
|
if i.syms[a].StartLine != i.syms[b].StartLine {
|
|
return i.syms[a].StartLine < i.syms[b].StartLine
|
|
}
|
|
// For nodes at the same start line, narrowest-first so
|
|
// the deepest scope wins on ties.
|
|
return (i.syms[a].EndLine - i.syms[a].StartLine) <
|
|
(i.syms[b].EndLine - i.syms[b].StartLine)
|
|
})
|
|
}
|
|
|
|
// smallestEnclosing returns the narrowest symbol whose [StartLine,
|
|
// EndLine] range covers `line`, or nil when no symbol does. Lines are
|
|
// 1-based; graph nodes store the same convention. syms is sorted by
|
|
// StartLine ascending, so the scan can stop once StartLine passes line.
|
|
func (i *fileSymbolIndex) smallestEnclosing(line int) *graph.Node {
|
|
if i == nil {
|
|
return nil
|
|
}
|
|
var best *graph.Node
|
|
bestSpan := int(^uint(0) >> 1)
|
|
for _, n := range i.syms {
|
|
if n.StartLine > line {
|
|
break
|
|
}
|
|
if n.EndLine < line {
|
|
continue
|
|
}
|
|
span := n.EndLine - n.StartLine
|
|
if best == nil || span < bestSpan {
|
|
best = n
|
|
bestSpan = span
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
// find returns (symbol_id, name) for the smallest enclosing symbol
|
|
// whose [StartLine, EndLine] range covers `line`.
|
|
func (i *fileSymbolIndex) find(line int) (string, string) {
|
|
best := i.smallestEnclosing(line)
|
|
if best == nil {
|
|
return "", ""
|
|
}
|
|
return best.ID, best.Name
|
|
}
|
|
|
|
// enclosingForRange returns the symbols that enclose any line in the
|
|
// inclusive [start, end] range, choosing the smallest enclosing symbol
|
|
// at each covered line — so a range inside one function yields that
|
|
// function, while a range spanning two functions yields both. Results
|
|
// are deduplicated by node ID and returned in first-seen (top-down)
|
|
// order. A degenerate range (end < start) collapses to the single
|
|
// start line.
|
|
func (i *fileSymbolIndex) enclosingForRange(start, end int) []*graph.Node {
|
|
if i == nil {
|
|
return nil
|
|
}
|
|
if end < start {
|
|
end = start
|
|
}
|
|
seen := make(map[string]struct{})
|
|
var out []*graph.Node
|
|
for line := start; line <= end; line++ {
|
|
best := i.smallestEnclosing(line)
|
|
if best == nil {
|
|
continue
|
|
}
|
|
if _, ok := seen[best.ID]; ok {
|
|
continue
|
|
}
|
|
seen[best.ID] = struct{}{}
|
|
out = append(out, best)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// enclosingName derives the enclosing owner of a node -- the symbol
|
|
// the node is declared *inside* -- and returns its (id, name).
|
|
//
|
|
// - For a method, the owner is its receiver type, recovered from
|
|
// the EdgeMemberOf edge, or failing that from the node ID prefix
|
|
// (the ID convention is "<file>::<Owner>.<method>").
|
|
// - For a field, enum member, closure, or nested function, the
|
|
// owner is whatever EdgeMemberOf points at -- the struct, enum,
|
|
// or enclosing function.
|
|
// - For everything else (a top-level function, type, package-level
|
|
// variable) there is no enclosing owner; both return values are
|
|
// empty.
|
|
//
|
|
// A nil node or reader yields two empty strings.
|
|
func enclosingName(n *graph.Node, g graph.Reader) (id, name string) {
|
|
if n == nil {
|
|
return "", ""
|
|
}
|
|
switch n.Kind {
|
|
case graph.KindMethod, graph.KindField, graph.KindEnumMember,
|
|
graph.KindClosure:
|
|
// These kinds are always declared inside an owner.
|
|
case graph.KindFunction:
|
|
// A function is enclosed only when it is nested (a function
|
|
// literal assigned inside another function); the EdgeMemberOf
|
|
// lookup below detects that. A top-level function has none.
|
|
default:
|
|
return "", ""
|
|
}
|
|
|
|
// Primary path: the EdgeMemberOf edge records the structural
|
|
// owner directly.
|
|
if g != nil {
|
|
for _, e := range g.GetOutEdges(n.ID) {
|
|
if e.Kind != graph.EdgeMemberOf {
|
|
continue
|
|
}
|
|
if owner := g.GetNode(e.To); owner != nil {
|
|
return owner.ID, owner.Name
|
|
}
|
|
// The edge target may not be resolvable to a node (an
|
|
// unresolved owner); still surface the ID.
|
|
return e.To, graph.EnclosingShortName(e.To)
|
|
}
|
|
}
|
|
|
|
// Fallback: derive the owner from the ID convention. This
|
|
// covers method / field / enum-member / closure even when no
|
|
// EdgeMemberOf edge was materialised.
|
|
if ownerID, ownerName := graph.EnclosingFromID(n.ID, n.Kind); ownerName != "" {
|
|
return ownerID, ownerName
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// buildFileSymbolIndexForPaths builds one fileSymbolIndex per file
|
|
// path in `paths`. It is the plain-path sibling of
|
|
// buildFileSymbolIndex (which keys off astquery.Target values) --
|
|
// search_text works from trigram match paths, not AST targets, and
|
|
// needs the same enclosing-scope lookup.
|
|
func (s *Server) buildFileSymbolIndexForPaths(paths map[string]struct{}) map[string]*fileSymbolIndex {
|
|
if s.graph == nil || len(paths) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]*fileSymbolIndex, len(paths))
|
|
for _, n := range s.graph.AllNodes() {
|
|
if _, ok := paths[n.FilePath]; !ok {
|
|
continue
|
|
}
|
|
switch n.Kind {
|
|
case graph.KindFunction, graph.KindMethod, graph.KindClosure,
|
|
graph.KindType, graph.KindInterface:
|
|
idx := out[n.FilePath]
|
|
if idx == nil {
|
|
idx = &fileSymbolIndex{}
|
|
out[n.FilePath] = idx
|
|
}
|
|
idx.add(n)
|
|
}
|
|
}
|
|
for _, idx := range out {
|
|
idx.finalise()
|
|
}
|
|
return out
|
|
}
|