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

272 lines
9.0 KiB
Go

package resolver
import (
"sort"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// SynthValueRef tags a resolved value-reference read edge.
const SynthValueRef = "value-ref"
const (
// valueRefCandidateVia marks an extractor-emitted placeholder read of a
// distinctive identifier; valueRefVia marks the bound read this pass lands.
valueRefCandidateVia = "value_ref_candidate"
valueRefVia = "value_ref"
)
// ResolveValueRefs binds each captured distinctive-name value reference to the
// file-scope constant / variable it reads and re-targets the placeholder into a
// tiered EdgeReads from the reader to that constant.
//
// This closes a change-impact gap: a config constant's readers were invisible
// to blast-radius analysis — fillImpactLive follows every incoming edge except
// Defines/MemberOf, so without a read edge "change this constant → who breaks"
// missed every reader that referenced it outside a captured call/arg position.
// Beat: the read rides a provenance tier (min_tier-filterable), where a flat
// reference is not.
//
// Precision gates: only distinctive names bind (>=3 chars with an uppercase
// letter or underscore — the config-constant shape); a candidate whose name is
// shadowed by a same-file parameter, field, or inner-scope local declarator is
// dropped; a reader in a generated file is skipped; self-reads are ignored.
// Unresolved candidates are
// left as inert placeholders. Idempotent: re-targeting to the same constant is
// a no-op and graph.EvictFile drops the edges on reindex.
func ResolveValueRefs(g graph.Store) int { return resolveValueRefs(g, nil) }
// ResolveValueRefsScoped is the incremental counterpart of ResolveValueRefs: it
// resolves only the value-ref candidates that originate in the given changed
// repos, leaving an unchanged repo's already-bound reads on disk (they were
// never dropped). A nil scope resolves the whole graph, so ResolveValueRefs and
// the whole-index path stay byte-identical. Restricting the candidate set is
// binding-preserving: every candidate binds to a constant declared in its OWN
// file, so which other repos are in scope can never change a resolution.
func ResolveValueRefsScoped(g graph.Store, scope map[string]bool) int {
return resolveValueRefs(g, scope)
}
func resolveValueRefs(g graph.Store, scope map[string]bool) int {
if g == nil {
return 0
}
// Two-pass narrowing. A value-ref binds a captured read to a file-scope
// constant declared in the SAME file, so only the files that actually carry
// a candidate read need their declarators indexed. Gather the candidate
// edges first (Pass A), then build the constant/local maps for their files
// alone (Pass B) — instead of a whole-graph scan over every
// constant/variable/param/field/local node, which is the largest node
// population in the graph and was materialised twice (a flat slice plus the
// nested maps) on every whole-graph run.
candidates := valueRefCandidateEdges(g, scope)
if len(candidates) == 0 {
return 0
}
candidateFiles := make(map[string]struct{}, len(candidates))
for _, e := range candidates {
if e.FilePath != "" {
candidateFiles[e.FilePath] = struct{}{}
}
}
// constsByFile records every file-scope constant/variable declarator of a
// distinctive name (a name may have several — a try/except import, a
// `#[cfg]` const, an `#ifdef #define`); localsByFile records the
// param/field/local declarators that may shadow a read in their own scope.
constsByFile := map[string]map[string][]*graph.Node{}
localsByFile := map[string]map[string][]*graph.Node{}
for f := range candidateFiles {
for _, n := range g.GetFileNodes(f) {
if n == nil || n.FilePath == "" {
continue
}
switch n.Kind {
case graph.KindConstant, graph.KindVariable:
if !isDistinctiveValueName(n.Name) {
continue
}
m := constsByFile[n.FilePath]
if m == nil {
m = map[string][]*graph.Node{}
constsByFile[n.FilePath] = m
}
m[n.Name] = append(m[n.Name], n)
case graph.KindParam, graph.KindField, graph.KindLocal:
m := localsByFile[n.FilePath]
if m == nil {
m = map[string][]*graph.Node{}
localsByFile[n.FilePath] = m
}
m[n.Name] = append(m[n.Name], n)
}
}
}
if len(constsByFile) == 0 {
return 0
}
for _, m := range constsByFile {
for _, ns := range m {
sort.SliceStable(ns, func(i, j int) bool { return ns[i].StartLine < ns[j].StartLine })
}
}
resolved := 0
var reindex []graph.EdgeReindex
for _, e := range candidates {
name, _ := e.Meta["name"].(string)
consts := constsByFile[e.FilePath][name]
if name == "" || len(consts) == 0 {
continue
}
// Reader-scope shadow: a same-named param/field/local declared *inside
// the reading function* (its node ID nests under the reader's via a
// `.`/`#`/`:` scope separator) means the bare read more likely binds
// that local, not the constant — drop. A same-named local in an
// unrelated function does NOT shadow this read (the recall the old
// file-wide boolean census over-dropped).
if valueRefReaderShadowed(e.From, localsByFile[e.FilePath][name]) {
continue
}
if reader := g.GetNode(e.From); reader != nil && isGeneratedReader(reader) {
continue
}
// Conditional def: more than one file-scope declarator of the name
// (try/except / #[cfg] / #ifdef) is legitimate — bind the read to the
// nearest preceding declarator rather than dropping.
conditional := len(consts) > 1
target := consts[0]
if conditional {
target = nearestPrecedingDecl(consts, e.Line)
}
if target == nil || target.ID == e.From {
continue
}
if e.To == target.ID {
resolved++
continue
}
oldTo := e.To
e.To = target.ID
e.Origin = graph.OriginASTResolved
e.Confidence = 0.7
e.ConfidenceLabel = graph.ConfidenceLabelFor(graph.EdgeReads, 0.7)
e.Meta["via"] = valueRefVia
if conditional {
e.Meta["conditional_def"] = true
}
StampSynthesized(e, SynthValueRef)
reindex = append(reindex, graph.EdgeReindex{Edge: e, OldTo: oldTo})
resolved++
}
if len(reindex) > 0 {
g.ReindexEdges(reindex)
}
return resolved
}
// valueRefCandidateEdges returns the extractor-emitted placeholder read edges
// (Meta via == value_ref_candidate) the pass resolves. With a nil scope it
// scans every EdgeReads edge in the graph; with a scope it walks only the
// out-edges of the changed repos' nodes (GetRepoEdges is one backend query per
// repo), since a candidate read always originates in the repo that declared it.
func valueRefCandidateEdges(g graph.Store, scope map[string]bool) []*graph.Edge {
var out []*graph.Edge
keep := func(e *graph.Edge) {
if e == nil || e.Meta == nil {
return
}
if via, _ := e.Meta["via"].(string); via != valueRefCandidateVia {
return
}
out = append(out, e)
}
if scope == nil {
for e := range g.EdgesByKind(graph.EdgeReads) {
keep(e)
}
return out
}
for prefix := range scope {
if prefix == "" {
continue
}
for _, e := range g.GetRepoEdges(prefix) {
if e == nil || e.Kind != graph.EdgeReads {
continue
}
keep(e)
}
}
return out
}
// valueRefReaderShadowed reports whether any same-named declarator is scoped
// inside the reading function — its node ID nests under the reader's ID via a
// `.` / `#` / `:` scope separator (`f.go::Run.x`, `f.go::Run#x`). Such a
// declarator shadows a bare read in the reader's own scope; a same-named
// declarator in an unrelated function does not.
func valueRefReaderShadowed(readerID string, locals []*graph.Node) bool {
if readerID == "" {
return false
}
for _, l := range locals {
if l == nil || !strings.HasPrefix(l.ID, readerID) || len(l.ID) <= len(readerID) {
continue
}
switch l.ID[len(readerID)] {
case '.', '#', ':':
return true
}
}
return false
}
// nearestPrecedingDecl returns the conditional-def declarator with the greatest
// line at or before readLine, falling back to the first when the read precedes
// them all. decls is sorted ascending by line.
func nearestPrecedingDecl(decls []*graph.Node, readLine int) *graph.Node {
best := decls[0]
for _, d := range decls {
if d.StartLine <= readLine {
best = d
} else {
break
}
}
return best
}
// isDistinctiveValueName reports whether name has the config-constant shape:
// at least 3 characters and at least one uppercase letter or underscore. This
// keeps the value-ref binding to names unlikely to collide with an ordinary
// local (which is conventionally lowerCamelCase).
func isDistinctiveValueName(name string) bool {
if len(name) < 3 {
return false
}
for i := 0; i < len(name); i++ {
c := name[i]
if c == '_' || (c >= 'A' && c <= 'Z') {
return true
}
}
return false
}
// isGeneratedReader reports whether a node lives in a generated file, which is
// excluded from value-ref binding (its reads are machine-emitted noise).
func isGeneratedReader(n *graph.Node) bool {
if n.Meta != nil {
if gen, _ := n.Meta["generated"].(bool); gen {
return true
}
}
p := n.FilePath
return strings.Contains(p, ".pb.go") ||
strings.Contains(p, ".g.dart") ||
strings.Contains(p, "_generated.") ||
strings.Contains(p, ".generated.") ||
strings.HasSuffix(p, ".gen.go")
}