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
219 lines
7.8 KiB
Go
219 lines
7.8 KiB
Go
package resolver
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// bindDataflowCalleeRefs lifts the callee side of dataflow edges
|
|
// (arg_of / value_flow) from an `unresolved::` placeholder onto the real node
|
|
// the callee denotes.
|
|
//
|
|
// The main resolver's resolveEdge already binds `calls` edges to these
|
|
// callees, but it keys the candidate lookup off the edge's From node's repo —
|
|
// and a dataflow edge's From is an `unresolved::` argument placeholder with no
|
|
// node, so the lookup is scoped to the empty repo, matches nothing in a
|
|
// multi-repo graph, and the callee never lifts. materializeDataflowParams
|
|
// (which refines a resolved callee to its param node) then skips the edge
|
|
// because its target is still an `unresolved::` stub. The result was ~half of
|
|
// all arg_of edges left dangling placeholder→placeholder even when the callee
|
|
// was defined in the same file.
|
|
//
|
|
// This pass closes that gap cheaply and without touching the hot per-edge
|
|
// resolver, via three call-site-local / index lookups (O(1) per edge):
|
|
//
|
|
// - bare `unresolved::<name>` callee → the sole SAME-FILE function/method of
|
|
// that name, else the sole SAME-PACKAGE function (Go package function names
|
|
// are unique, so a same-dir function match is unambiguous). Matched by
|
|
// FilePath / dir equality, so no repo-prefix logic — identical behaviour in
|
|
// bare and prefixed graphs.
|
|
// - `unresolved::*.<method>` callee → the method the call resolver already
|
|
// bound for a `calls` / `references` edge at the SAME call site (file+line),
|
|
// reusing the receiver-type work resolveMethodCall already did there.
|
|
//
|
|
// Ordering (see runFileAttributionPassesLocked): after bindBareNameScopeRefs
|
|
// (a same-scope local/param wins over a same-file function of the same name)
|
|
// and before attributeGoBuiltins (a bare `append`/`len` argument with no
|
|
// definition falls through to builtin attribution) and before
|
|
// materializeDataflowParams (which then refines a resolved function/method
|
|
// callee to its param node).
|
|
func (r *Resolver) bindDataflowCalleeRefs() {
|
|
idx := newCalleeIndex()
|
|
for _, k := range []graph.NodeKind{graph.KindFunction, graph.KindMethod} {
|
|
for n := range r.graph.NodesByKind(k) {
|
|
if n == nil || n.Name == "" || n.FilePath == "" {
|
|
continue
|
|
}
|
|
indexName(idx.byFile, n.FilePath, n.Name, n.ID)
|
|
if k == graph.KindFunction {
|
|
indexName(idx.byDir, filepath.Dir(n.FilePath), n.Name, n.ID)
|
|
}
|
|
}
|
|
}
|
|
for _, k := range []graph.EdgeKind{graph.EdgeCalls, graph.EdgeReferences} {
|
|
for e := range r.graph.EdgesByKind(k) {
|
|
idx.indexCallSite(e)
|
|
}
|
|
}
|
|
if len(idx.byFile) == 0 && len(idx.bySite) == 0 {
|
|
return
|
|
}
|
|
var batch []graph.EdgeReindex
|
|
for _, ek := range []graph.EdgeKind{graph.EdgeArgOf, graph.EdgeValueFlow} {
|
|
for e := range r.graph.EdgesByKind(ek) {
|
|
if old := bindDataflowCalleeEdge(e, idx); old != "" {
|
|
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: old})
|
|
}
|
|
}
|
|
}
|
|
if len(batch) > 0 {
|
|
r.graph.ReindexEdges(batch)
|
|
}
|
|
}
|
|
|
|
// bindDataflowCalleeRefsForFile is the single-file scope of
|
|
// bindDataflowCalleeRefs, used on the incremental (fsnotify / edit_file)
|
|
// re-index path. It builds the same indexes restricted to the file's own
|
|
// nodes/edges (same-file), the package's functions via r.dirIndex (same-
|
|
// package — buildDirIndexes runs on the incremental resolve too, so the map is
|
|
// populated), and the file's own call sites — producing exactly the binds the
|
|
// whole-graph sweep would for the file, keeping incremental == full-index
|
|
// convergence, without scanning every function in the graph.
|
|
func (r *Resolver) bindDataflowCalleeRefsForFile(filePath string) {
|
|
idx := newCalleeIndex()
|
|
for _, n := range r.graph.GetFileNodes(filePath) {
|
|
if n == nil || n.Name == "" || n.FilePath == "" {
|
|
continue
|
|
}
|
|
if n.Kind == graph.KindFunction || n.Kind == graph.KindMethod {
|
|
indexName(idx.byFile, n.FilePath, n.Name, n.ID)
|
|
}
|
|
}
|
|
// Same-package functions: r.dirIndex[dir] carries one KindFile node per
|
|
// file in the directory, so each package file is visited exactly once.
|
|
dir := filepath.Dir(filePath)
|
|
for _, fileNode := range r.dirIndex[dir] {
|
|
for _, n := range r.graph.GetFileNodes(fileNode.FilePath) {
|
|
if n != nil && n.Kind == graph.KindFunction && n.Name != "" && n.FilePath != "" {
|
|
indexName(idx.byDir, dir, n.Name, n.ID)
|
|
}
|
|
}
|
|
}
|
|
fileEdges := r.fileOutEdges(filePath)
|
|
for _, e := range fileEdges {
|
|
if e != nil && (e.Kind == graph.EdgeCalls || e.Kind == graph.EdgeReferences) {
|
|
idx.indexCallSite(e)
|
|
}
|
|
}
|
|
var batch []graph.EdgeReindex
|
|
for _, e := range fileEdges {
|
|
if e == nil || (e.Kind != graph.EdgeArgOf && e.Kind != graph.EdgeValueFlow) {
|
|
continue
|
|
}
|
|
if old := bindDataflowCalleeEdge(e, idx); old != "" {
|
|
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: old})
|
|
}
|
|
}
|
|
if len(batch) > 0 {
|
|
r.graph.ReindexEdges(batch)
|
|
}
|
|
}
|
|
|
|
// calleeIndex holds the per-pass lookup tables bindDataflowCallee* uses.
|
|
type calleeIndex struct {
|
|
byFile map[string]map[string][]string // file -> name -> func/method ids
|
|
byDir map[string]map[string][]string // dir -> name -> function ids
|
|
bySite map[string][]string // "<file>\x00<line>" -> resolved callee ids
|
|
}
|
|
|
|
func newCalleeIndex() *calleeIndex {
|
|
return &calleeIndex{
|
|
byFile: map[string]map[string][]string{},
|
|
byDir: map[string]map[string][]string{},
|
|
bySite: map[string][]string{},
|
|
}
|
|
}
|
|
|
|
// indexCallSite records a resolved calls/references edge under its call site so
|
|
// a `*.method` dataflow callee at the same site can reuse its bound target.
|
|
func (idx *calleeIndex) indexCallSite(e *graph.Edge) {
|
|
if e == nil || e.Line <= 0 || e.FilePath == "" || graph.IsUnresolvedTarget(e.To) {
|
|
return
|
|
}
|
|
k := siteKey(e.FilePath, e.Line)
|
|
idx.bySite[k] = append(idx.bySite[k], e.To)
|
|
}
|
|
|
|
func siteKey(file string, line int) string {
|
|
return file + "\x00" + strconv.Itoa(line)
|
|
}
|
|
|
|
func indexName(m map[string]map[string][]string, key, name, id string) {
|
|
names := m[key]
|
|
if names == nil {
|
|
names = map[string][]string{}
|
|
m[key] = names
|
|
}
|
|
names[name] = append(names[name], id)
|
|
}
|
|
|
|
// bindDataflowCalleeEdge rewrites e.To from an `unresolved::` callee placeholder
|
|
// to the real node it denotes, using idx. Returns the old To value when a
|
|
// rewrite happened (for the batched reindex) or "" when the edge was left alone
|
|
// (not an unresolved target, no unambiguous match, or a shape another pass owns).
|
|
func bindDataflowCalleeEdge(e *graph.Edge, idx *calleeIndex) string {
|
|
if e == nil || !graph.IsUnresolvedTarget(e.To) {
|
|
return ""
|
|
}
|
|
name := graph.UnresolvedName(e.To)
|
|
var chosen string
|
|
switch {
|
|
case strings.HasPrefix(name, "*."):
|
|
// Method-call callee: reuse the target the call resolver bound for a
|
|
// calls/references edge at the same call site.
|
|
method := name[2:]
|
|
if method == "" || strings.ContainsAny(method, ".*:#") {
|
|
return ""
|
|
}
|
|
chosen = uniqueSiteCallee(idx.bySite[siteKey(e.FilePath, e.Line)], method)
|
|
case name == "" || strings.ContainsAny(name, ".*:#"):
|
|
// Qualified (a::b), extern, or per-binding (#...) shape — owned by
|
|
// other passes.
|
|
return ""
|
|
default:
|
|
// Bare identifier: same-file first, then same-package function.
|
|
if ids := idx.byFile[e.FilePath][name]; len(ids) == 1 {
|
|
chosen = ids[0]
|
|
} else if len(ids) == 0 {
|
|
if ids := idx.byDir[filepath.Dir(e.FilePath)][name]; len(ids) == 1 {
|
|
chosen = ids[0]
|
|
}
|
|
}
|
|
}
|
|
if chosen == "" || chosen == e.To {
|
|
return ""
|
|
}
|
|
oldTo := e.To
|
|
e.To = chosen
|
|
return oldTo
|
|
}
|
|
|
|
// uniqueSiteCallee returns the sole resolved callee at a call site whose id
|
|
// names the given method (its id ends with ".<method>" or "::<method>"), or ""
|
|
// when there is no match or more than one.
|
|
func uniqueSiteCallee(callees []string, method string) string {
|
|
var chosen string
|
|
for _, id := range callees {
|
|
if strings.HasSuffix(id, "."+method) || strings.HasSuffix(id, "::"+method) {
|
|
if chosen != "" && chosen != id {
|
|
return ""
|
|
}
|
|
chosen = id
|
|
}
|
|
}
|
|
return chosen
|
|
}
|