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

97 lines
3.1 KiB
Go

package languages
import (
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// captureValueRefCandidates records a value-reference candidate read for every
// distinctive identifier in the parse tree that names a file-scope constant or
// variable this extractor just emitted, attributed to its enclosing function.
// The resolver's ResolveValueRefs then binds (or shadow-prunes) each candidate
// into a tiered EdgeReads, so a config constant's blast radius reaches its
// readers — closing a change-impact gap that plain call/arg extraction misses.
//
// It is grammar-agnostic: it keys on `identifier` leaf nodes (the value-position
// token in nearly every tree-sitter grammar) and pre-filters to names the file
// actually declares, so it never emits a candidate that cannot bind. Call it
// once at the end of an extractor's Extract with the parse-tree root.
func captureValueRefCandidates(result *parser.ExtractionResult, root *sitter.Node, filePath string, src []byte) {
if root == nil || result == nil {
return
}
consts := map[string]bool{}
for _, n := range result.Nodes {
if n == nil || n.FilePath != filePath {
continue
}
if n.Kind != graph.KindConstant && n.Kind != graph.KindVariable {
continue
}
if isDistinctiveConstName(n.Name) {
consts[n.Name] = true
}
}
if len(consts) == 0 {
return
}
funcRanges := buildFuncRanges(result)
if len(funcRanges) == 0 {
return
}
seen := map[string]bool{}
walkNodes(root, func(n *sitter.Node) {
if !isValueRefIdentNode(n.Type()) {
return
}
name := n.Content(src)
if !consts[name] {
return
}
line := int(n.StartPoint().Row) + 1
fromID := findEnclosingFunc(funcRanges, line)
if fromID == "" || fromID == filePath {
return // not inside a function (e.g. the declaration itself)
}
key := fromID + "\x00" + name
if seen[key] {
return
}
seen[key] = true
result.Edges = append(result.Edges, &graph.Edge{
From: fromID, To: "unresolved::valueref::" + name, Kind: graph.EdgeReads,
FilePath: filePath, Line: line, Origin: graph.OriginSpeculative,
Meta: map[string]any{"via": "value_ref_candidate", "name": name},
})
})
}
// isValueRefIdentNode reports whether a node type is a value-position
// identifier leaf the constant-read scan should consider. Beyond the common
// `identifier`, Kotlin and Swift use `simple_identifier` and PHP a bare `name`,
// so a constant read in those grammars is captured too.
func isValueRefIdentNode(t string) bool {
switch t {
case "identifier", "simple_identifier", "name", "constant":
return true
}
return false
}
// isDistinctiveConstName mirrors the resolver's distinctive-name gate: at least
// 3 characters with an uppercase letter or underscore — the config-constant
// shape, unlikely to collide with an ordinary lowerCamelCase local.
func isDistinctiveConstName(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
}