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
272 lines
9.0 KiB
Go
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")
|
|
}
|