chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
package dataflow
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zzet/gortex/internal/cfg"
|
||||
"github.com/zzet/gortex/internal/graph"
|
||||
)
|
||||
|
||||
// Refinement markers stamped on EdgeStep.Refined. A confirmed hop
|
||||
// has a CFG-verified reaching-definition chain from the source
|
||||
// binding to the statement that defines the target binding; a pruned
|
||||
// hop is one the chain analysis disproves (the source's definition
|
||||
// is killed on every path before the target's defining statement).
|
||||
const (
|
||||
RefinedConfirmed = "confirmed_intraprocedural"
|
||||
RefinedPruned = "pruned"
|
||||
)
|
||||
|
||||
// prunedPenalty scales a path's confidence for every hop the
|
||||
// reaching-definitions analysis disproves, so stale edges sink in
|
||||
// the ranking instead of silently disappearing.
|
||||
const prunedPenalty = 0.25
|
||||
|
||||
// defaultRefinerCapacity bounds how many per-function CFGs one
|
||||
// refiner will hold. Refiners are per-call; the cap keeps a
|
||||
// pathological many-function path from accumulating parse trees.
|
||||
//
|
||||
// A taint sweep reuses one refiner across every (source, sink) pair,
|
||||
// and refinement runs per pair before the findings are ranked and
|
||||
// capped — so a function on several candidate paths must survive in
|
||||
// the cache between pairs or it gets re-parsed. The working set of a
|
||||
// single flow_between walk is bounded by DefaultMaxPaths distinct
|
||||
// paths of at most DefaultMaxDepth hops; sizing the cache to cover
|
||||
// that union keeps a broad pattern sweep from thrashing the FIFO,
|
||||
// while still bounding transient memory (the refiner is discarded at
|
||||
// the end of the call).
|
||||
const defaultRefinerCapacity = DefaultMaxPaths * DefaultMaxDepth
|
||||
|
||||
// FuncSource is one function's source text plus the file-absolute
|
||||
// line its first byte sits on.
|
||||
type FuncSource struct {
|
||||
Src []byte
|
||||
StartLine int
|
||||
}
|
||||
|
||||
// SourceResolver fetches the source of a function/method node. The
|
||||
// MCP layer supplies an overlay-aware reader; tests can return
|
||||
// source from memory.
|
||||
type SourceResolver func(fn *graph.Node) (FuncSource, error)
|
||||
|
||||
// Refiner upgrades value_flow hops whose endpoints are bindings of
|
||||
// the same function using statement-granular reaching-definition
|
||||
// chains. CFGs are built lazily — only for functions that actually
|
||||
// appear on a candidate path — and cached with FIFO eviction. Not
|
||||
// safe for concurrent use; construct one per query.
|
||||
type Refiner struct {
|
||||
g graph.Store
|
||||
resolve SourceResolver
|
||||
cap int
|
||||
entries map[string]*refEntry
|
||||
order []string
|
||||
}
|
||||
|
||||
// refEntry caches one function's analysis; a nil graph marks a
|
||||
// negative entry (unsupported language, unreadable source, parse
|
||||
// failure) so the failure isn't retried per hop.
|
||||
type refEntry struct {
|
||||
c *cfg.CFG
|
||||
r *cfg.ReachingResult
|
||||
}
|
||||
|
||||
// NewRefiner builds a refiner over the graph with the given source
|
||||
// resolver. capacity <= 0 selects the default.
|
||||
func NewRefiner(g graph.Store, resolve SourceResolver, capacity int) *Refiner {
|
||||
if capacity <= 0 {
|
||||
capacity = defaultRefinerCapacity
|
||||
}
|
||||
return &Refiner{
|
||||
g: g,
|
||||
resolve: resolve,
|
||||
cap: capacity,
|
||||
entries: make(map[string]*refEntry, capacity),
|
||||
}
|
||||
}
|
||||
|
||||
// refinePaths stamps Refined markers on the value_flow hops it can
|
||||
// judge and rescales path confidence for pruned hops. Returns true
|
||||
// when any confidence changed (callers re-rank).
|
||||
func (r *Refiner) refinePaths(paths []Path) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
changed := false
|
||||
for pi := range paths {
|
||||
for si := range paths[pi].Edges {
|
||||
step := &paths[pi].Edges[si]
|
||||
if graph.EdgeKind(step.Kind) != graph.EdgeValueFlow {
|
||||
continue
|
||||
}
|
||||
switch r.refineStep(step) {
|
||||
case RefinedConfirmed:
|
||||
step.Refined = RefinedConfirmed
|
||||
case RefinedPruned:
|
||||
step.Refined = RefinedPruned
|
||||
paths[pi].Confidence *= prunedPenalty
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// refineStep judges one hop. Returns "" when the hop is out of scope
|
||||
// (endpoints not bindings of the same function, unsupported
|
||||
// language) or the CFG can't anchor both endpoints — unmarked hops
|
||||
// keep their coarse-edge semantics.
|
||||
func (r *Refiner) refineStep(step *EdgeStep) string {
|
||||
fromOwner, fromName, ok := splitBindingID(step.From)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
toOwner, toName, ok := splitBindingID(step.To)
|
||||
if !ok || fromOwner != toOwner || fromName == "" || toName == "" {
|
||||
return ""
|
||||
}
|
||||
ent := r.entryFor(fromOwner)
|
||||
if ent == nil || ent.c == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
defStmt := r.bindingDefStmt(ent, step.From, fromName)
|
||||
if defStmt == nil {
|
||||
return ""
|
||||
}
|
||||
toNode := r.g.GetNode(step.To)
|
||||
if toNode == nil || toNode.StartLine == 0 {
|
||||
return ""
|
||||
}
|
||||
useStmt := ent.c.StatementAt(toNode.StartLine, toName)
|
||||
if useStmt == nil || useStmt.Index == defStmt.Index {
|
||||
return ""
|
||||
}
|
||||
// The hop claims `toName`'s definition consumes `fromName`. If
|
||||
// the CFG statement doesn't even read fromName the extraction
|
||||
// disagrees with the graph edge — stay unmarked rather than
|
||||
// judging on mismatched evidence.
|
||||
reads := false
|
||||
for _, u := range useStmt.Uses {
|
||||
if u == fromName {
|
||||
reads = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !reads {
|
||||
return ""
|
||||
}
|
||||
for _, ch := range ent.r.ChainsFor(useStmt.Index) {
|
||||
if ch.Var != fromName {
|
||||
continue
|
||||
}
|
||||
for _, d := range ch.Defs {
|
||||
if d == defStmt.Index {
|
||||
return RefinedConfirmed
|
||||
}
|
||||
}
|
||||
}
|
||||
// fromName is read at the target statement, but the specific
|
||||
// definition this hop starts from never reaches it — every path
|
||||
// kills it first.
|
||||
return RefinedPruned
|
||||
}
|
||||
|
||||
// bindingDefStmt anchors a binding node onto its defining CFG
|
||||
// statement: params map to the synthetic entry-block param
|
||||
// statements, locals to the statement covering their binding line.
|
||||
func (r *Refiner) bindingDefStmt(ent *refEntry, id, name string) *cfg.Statement {
|
||||
if strings.Contains(id, "#param:") {
|
||||
for _, st := range ent.c.Stmts {
|
||||
if st.Kind != "param" {
|
||||
continue
|
||||
}
|
||||
for _, d := range st.Defs {
|
||||
if d == name {
|
||||
return st
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
node := r.g.GetNode(id)
|
||||
if node == nil || node.StartLine == 0 {
|
||||
return nil
|
||||
}
|
||||
return ent.c.StatementAt(node.StartLine, name)
|
||||
}
|
||||
|
||||
// entryFor returns the cached analysis for a function, building it
|
||||
// on first sight. Failures cache negatively.
|
||||
func (r *Refiner) entryFor(ownerID string) *refEntry {
|
||||
if ent, ok := r.entries[ownerID]; ok {
|
||||
return ent
|
||||
}
|
||||
ent := &refEntry{}
|
||||
r.insert(ownerID, ent)
|
||||
|
||||
fn := r.g.GetNode(ownerID)
|
||||
if fn == nil || (fn.Kind != graph.KindFunction && fn.Kind != graph.KindMethod) {
|
||||
return ent
|
||||
}
|
||||
if !cfg.SupportedLanguage(fn.Language) || fn.StartLine == 0 || fn.EndLine == 0 {
|
||||
return ent
|
||||
}
|
||||
src, err := r.resolve(fn)
|
||||
if err != nil || len(src.Src) == 0 {
|
||||
return ent
|
||||
}
|
||||
c, err := cfg.Build(src.Src, fn.Language, cfg.Options{
|
||||
LineOffset: src.StartLine - 1,
|
||||
FuncName: fn.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return ent
|
||||
}
|
||||
ent.c = c
|
||||
ent.r = c.ReachingDefinitions()
|
||||
return ent
|
||||
}
|
||||
|
||||
func (r *Refiner) insert(key string, ent *refEntry) {
|
||||
if len(r.order) >= r.cap {
|
||||
oldest := r.order[0]
|
||||
r.order = r.order[1:]
|
||||
delete(r.entries, oldest)
|
||||
}
|
||||
r.entries[key] = ent
|
||||
r.order = append(r.order, key)
|
||||
}
|
||||
|
||||
// splitBindingID decomposes the dataflow binding ID forms emitted at
|
||||
// extraction time into the owning function ID and the variable name.
|
||||
// Both binding forms may carry a position/offset suffix after `@`:
|
||||
//
|
||||
// - locals: `<owner>#local:<name>@+<offsetFromOwnerStartLine>`
|
||||
// - params: `<owner>#param:<name>` (Go) or
|
||||
// `<owner>#param:<name>@<position>` (Python / TypeScript / Rust /
|
||||
// Java / C#, which disambiguate duplicate names by position).
|
||||
//
|
||||
// The `@…` suffix is stripped from both so the bare variable name
|
||||
// matches the CFG's def/use sets; without that the param branch
|
||||
// silently left every non-Go param hop unrefined.
|
||||
func splitBindingID(id string) (owner, name string, ok bool) {
|
||||
if i := strings.Index(id, "#local:"); i > 0 {
|
||||
return id[:i], trimBindingSuffix(id[i+len("#local:"):]), true
|
||||
}
|
||||
if i := strings.Index(id, "#param:"); i > 0 {
|
||||
return id[:i], trimBindingSuffix(id[i+len("#param:"):]), true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// trimBindingSuffix drops the `@<position>` / `@+<offset>`
|
||||
// disambiguator a binding ID carries after its name.
|
||||
func trimBindingSuffix(rest string) string {
|
||||
if j := strings.IndexByte(rest, '@'); j >= 0 {
|
||||
return rest[:j]
|
||||
}
|
||||
return rest
|
||||
}
|
||||
Reference in New Issue
Block a user