Files
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

237 lines
7.9 KiB
Go

package resolver
import (
"strings"
"github.com/zzet/gortex/internal/graph"
)
// scopeNode is the per-binding payload of the owner-keyed scope
// index built by bindBareNameScopeRefs. Kept as a named struct so
// the bind helpers can share the same signature.
type scopeNode struct {
id string
name string
startLine int
kind graph.NodeKind
}
// bindBareNameScopeRefs rewrites `unresolved::<bareName>` edges whose
// source is inside a function scope (or IS a function) onto the
// matching KindLocal / KindParam node that the enclosing function
// declares. Pre-#77 there was nothing to bind to — locals were
// edge-endpoint-only — so the resolver always fell through to
// `unresolved::*`. With #77's KindLocal materialisation the scope is
// now first-class and we can do the bind.
//
// Two precedence rules govern the choice when more than one candidate
// matches the name:
//
// 1. KindLocal beats KindParam — Go shadowing semantics, a local
// declared with the same name as a parameter takes over from its
// declaration line onwards.
// 2. Among KindLocal candidates the most recently declared one before
// the reference line wins (the standard "last shadow in scope"
// rule). The edge's Line field is the reference site; we filter
// candidates to StartLine <= reference line and pick the maximum
// StartLine.
//
// Ambiguous cases that don't resolve to one winner (e.g. two locals
// with the same Name on the same StartLine, or no candidate before
// the reference line) are left untouched so the downstream `unresolved`
// audit can still surface them.
//
// Scope today is Go-only — TypeScript / Python don't materialise
// locals yet, so their unresolved bare-name edges have no candidate
// to bind to. The pass naturally degenerates to a no-op for those
// languages because the candidate index will be empty for their
// owners.
func (r *Resolver) bindBareNameScopeRefs() {
// Index every KindLocal / KindParam by enclosing-function ID. Done
// once up front so the per-edge bind is an O(matching-name) walk
// rather than a graph-wide FindNodesByName.
owned := map[string][]scopeNode{}
for n := range r.graph.NodesByKind(graph.KindLocal) {
owner := enclosingFunctionForBinding(n.ID)
if owner == "" {
continue
}
owned[owner] = append(owned[owner], scopeNode{
id: n.ID, name: n.Name, startLine: n.StartLine, kind: graph.KindLocal,
})
}
for n := range r.graph.NodesByKind(graph.KindParam) {
owner := enclosingFunctionForBinding(n.ID)
if owner == "" {
continue
}
owned[owner] = append(owned[owner], scopeNode{
id: n.ID, name: n.Name, startLine: n.StartLine, kind: graph.KindParam,
})
}
if len(owned) == 0 {
return
}
var batch []graph.EdgeReindex
for e := range r.graph.EdgesByKind(graph.EdgeReads) {
if rewrote := r.tryBindBareName(e, owned); rewrote != "" {
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: rewrote})
}
}
for e := range r.graph.EdgesByKind(graph.EdgeReferences) {
if rewrote := r.tryBindBareName(e, owned); rewrote != "" {
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: rewrote})
}
}
// EdgeArgOf and EdgeValueFlow carry the same shape — `unresolved::<name>`
// is the dataflow source/target the parser couldn't bind.
for e := range r.graph.EdgesByKind(graph.EdgeArgOf) {
if rewrote := r.tryBindBareName(e, owned); rewrote != "" {
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: rewrote})
}
}
for e := range r.graph.EdgesByKind(graph.EdgeValueFlow) {
if rewrote := r.tryBindBareName(e, owned); rewrote != "" {
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: rewrote})
}
}
if len(batch) > 0 {
r.graph.ReindexEdges(batch)
}
}
// bindBareNameScopeRefsForFile is the single-file scope of
// bindBareNameScopeRefs. A bare-name reference binds to a KindLocal /
// KindParam declared by its OWN enclosing function, and that function —
// with all of its locals and params — lives entirely in the edited file.
// So the scope index is built from the file's own KindLocal / KindParam
// nodes and only the file's outgoing Read / Reference / ArgOf / ValueFlow
// edges are considered. This produces the same binds as the whole-graph
// sweep for a per-save resolve without scanning every KindLocal in the
// graph (the single largest node kind).
func (r *Resolver) bindBareNameScopeRefsForFile(filePath string) {
owned := map[string][]scopeNode{}
for _, n := range r.graph.GetFileNodes(filePath) {
if n.Kind != graph.KindLocal && n.Kind != graph.KindParam {
continue
}
owner := enclosingFunctionForBinding(n.ID)
if owner == "" {
continue
}
owned[owner] = append(owned[owner], scopeNode{
id: n.ID, name: n.Name, startLine: n.StartLine, kind: n.Kind,
})
}
if len(owned) == 0 {
return
}
var batch []graph.EdgeReindex
for _, e := range r.fileOutEdges(filePath) {
switch e.Kind {
case graph.EdgeReads, graph.EdgeReferences, graph.EdgeArgOf, graph.EdgeValueFlow:
if rewrote := r.tryBindBareName(e, owned); rewrote != "" {
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: rewrote})
}
}
}
if len(batch) > 0 {
r.graph.ReindexEdges(batch)
}
}
// tryBindBareName tries to rewrite e.To from `unresolved::<name>` to a
// matching in-scope KindLocal/KindParam ID. Returns the original To
// value when a rewrite happened (caller batches it for ReindexEdges)
// or "" when the edge was left alone.
func (r *Resolver) tryBindBareName(e *graph.Edge, owned map[string][]scopeNode) string {
if e == nil || !graph.IsUnresolvedTarget(e.To) {
return ""
}
name := graph.UnresolvedName(e.To)
if name == "" || strings.ContainsAny(name, ".*:#") {
// Not a bare identifier — leave to other passes (qualified
// names, *.method, etc.).
return ""
}
ownerID := enclosingFunctionForBinding(e.From)
if ownerID == "" {
return ""
}
candidates := owned[ownerID]
if len(candidates) == 0 {
return ""
}
chosen := pickInScopeBinding(candidates, name, e.Line)
if chosen == "" || chosen == e.To {
return ""
}
oldTo := e.To
e.To = chosen
return oldTo
}
// pickInScopeBinding implements the precedence rules:
// - prefer KindLocal over KindParam (Go shadowing),
// - among KindLocal, pick the latest StartLine that's still <= refLine,
// - if multiple candidates match the same maximum StartLine, return ""
// (ambiguous — leave the edge unresolved so the audit surfaces it).
//
// owned is the per-owner scope-node slice; name is the bare identifier
// from the edge target; refLine is the edge's line (the reference
// site). Returns the chosen ID, or "" when no unambiguous winner.
func pickInScopeBinding(owned []scopeNode, name string, refLine int) string {
var bestLocal struct {
id string
line int
dups int
}
var paramID string
for _, c := range owned {
if c.name != name {
continue
}
if c.kind == graph.KindLocal {
if refLine > 0 && c.startLine > refLine {
// Declared after the reference — can't be bound here.
continue
}
switch {
case c.startLine > bestLocal.line:
bestLocal.id = c.id
bestLocal.line = c.startLine
bestLocal.dups = 0
case c.startLine == bestLocal.line && c.id != bestLocal.id:
bestLocal.dups++
}
} else if c.kind == graph.KindParam {
if paramID != "" && paramID != c.id {
// Two params with the same name in the same function
// shouldn't happen but defensive — abstain.
paramID = ""
} else {
paramID = c.id
}
}
}
if bestLocal.id != "" && bestLocal.dups == 0 {
return bestLocal.id
}
return paramID
}
// enclosingFunctionForBinding strips the per-binding suffix added by
// the Go extractor (`#local:`, `#param:`, `#closure`, `#tparam:`) to
// recover the owner function/method ID. If `id` has no suffix it's
// returned unchanged — the caller is already a function/method node
// directly (the per-edge From is the function itself for things like
// the `external::foo` import edge inside `func Foo()`).
func enclosingFunctionForBinding(id string) string {
if i := strings.Index(id, "#"); i > 0 {
return id[:i]
}
return id
}