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
154 lines
5.2 KiB
Go
154 lines
5.2 KiB
Go
// Package review holds the graph-grounding post-pass that refines the
|
|
// pure-AST review rulepack (internal/astquery, Category "review").
|
|
//
|
|
// The astquery detectors are deliberately graph-agnostic: a detector's
|
|
// PostFilter only ever sees (parser.QueryResult, []byte), so the
|
|
// undecidable-from-AST-alone rules — N+1 query-in-loop and
|
|
// check-then-act on a shared map/dict — are emitted optimistically and
|
|
// then confirmed or refuted here, where a graph.Store is reachable.
|
|
//
|
|
// Grounding takes the match row (file + line + enclosing symbol) and
|
|
// consults graph metadata / resolved edges. A match that the graph
|
|
// proves is benign (e.g. a "loop query" whose enclosing function
|
|
// provably contains no loop) is dropped; everything else — including
|
|
// every decidable rule, which needs no grounding — survives.
|
|
package review
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/astquery"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// Detector names whose findings are undecidable from the AST alone and
|
|
// therefore subject to the graph-grounding refinement. Every other
|
|
// review detector is decidable and passes through untouched.
|
|
const (
|
|
detectorLoopQueryGo = "go-loop-query-call"
|
|
detectorLoopQueryPy = "py-loop-query-call"
|
|
detectorCheckActMapGo = "go-check-then-act-map"
|
|
detectorCheckActDictPy = "py-check-then-act-dict"
|
|
)
|
|
|
|
// mutatingEdgeKinds are the resolved out-edges that prove the enclosing
|
|
// symbol of a check-then-act match actually performs the "act" half —
|
|
// i.e. it writes state. Absent any of these the flagged read/check is
|
|
// not paired with a real mutation in the resolved graph and the match
|
|
// is refuted.
|
|
var mutatingEdgeKinds = map[graph.EdgeKind]struct{}{
|
|
graph.EdgeWrites: {},
|
|
graph.EdgeWritesCol: {},
|
|
graph.EdgeWritesConfig: {},
|
|
graph.EdgeAccessesField: {},
|
|
}
|
|
|
|
// GroundReviewMatches runs the graph-grounding post-pass over a set of
|
|
// review matches and returns the subset that survives. Decidable rules
|
|
// (every detector not in the undecidable set) pass through unchanged;
|
|
// the N+1 and check-then-act rows are kept only when the graph confirms
|
|
// them. A nil store grounds nothing — every row is returned as-is, so
|
|
// the feature degrades to pure-AST behaviour rather than dropping
|
|
// findings on a missing graph.
|
|
func GroundReviewMatches(g graph.Store, matches []astquery.Match) []astquery.Match {
|
|
if len(matches) == 0 {
|
|
return matches
|
|
}
|
|
out := matches[:0:0]
|
|
for _, m := range matches {
|
|
if keepReviewMatch(g, m) {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func keepReviewMatch(g graph.Store, m astquery.Match) bool {
|
|
switch m.Detector {
|
|
case detectorLoopQueryGo, detectorLoopQueryPy:
|
|
return GroundLoopCall(g, m)
|
|
case detectorCheckActMapGo, detectorCheckActDictPy:
|
|
return GroundCheckThenAct(g, m)
|
|
default:
|
|
// Decidable rule — no grounding needed.
|
|
return true
|
|
}
|
|
}
|
|
|
|
// GroundLoopCall returns true when an N+1 match should be kept: the
|
|
// enclosing symbol of the flagged call provably contains at least one
|
|
// loop body (loop_depth >= 1, stamped at index time). When the graph
|
|
// shows the enclosing function has no loop at all (loop_depth absent or
|
|
// zero) the query call cannot be inside a loop-over-collection, so the
|
|
// match is graph-refuted and dropped.
|
|
//
|
|
// A nil store or an unresolvable symbol leaves the AST verdict intact
|
|
// (keep) — grounding only ever removes a row it can affirmatively
|
|
// refute.
|
|
func GroundLoopCall(g graph.Store, m astquery.Match) bool {
|
|
if g == nil || m.SymbolID == "" {
|
|
return true
|
|
}
|
|
n := g.GetNode(m.SymbolID)
|
|
if n == nil {
|
|
return true
|
|
}
|
|
return loopDepth(n) >= 1
|
|
}
|
|
|
|
// GroundCheckThenAct returns true when a check-then-act match should be
|
|
// kept: the enclosing symbol carries a resolved mutating out-edge, so
|
|
// the "act" half (the write that follows the check) is a real mutation
|
|
// in the graph — the hallmark of a genuine read-modify-write race. When
|
|
// the graph shows the enclosing function performs no mutation at all
|
|
// the flagged shape is benign (e.g. the body only reads / logs) and the
|
|
// row is refuted.
|
|
//
|
|
// A nil store or an unresolvable symbol leaves the AST verdict intact
|
|
// (keep).
|
|
func GroundCheckThenAct(g graph.Store, m astquery.Match) bool {
|
|
if g == nil || m.SymbolID == "" {
|
|
return true
|
|
}
|
|
for _, e := range g.GetOutEdges(m.SymbolID) {
|
|
if e == nil {
|
|
continue
|
|
}
|
|
if _, ok := mutatingEdgeKinds[e.Kind]; ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// loopDepth reads the index-time loop-nesting metric off a symbol node.
|
|
// The metric is stamped only when > 0, so an absent key means zero
|
|
// loops. Meta values may be int / int64 / float64 depending on the
|
|
// backend's gob/json round-trip, so all three are accepted.
|
|
func loopDepth(n *graph.Node) int {
|
|
if n == nil || n.Meta == nil {
|
|
return 0
|
|
}
|
|
switch v := n.Meta["loop_depth"].(type) {
|
|
case int:
|
|
return v
|
|
case int64:
|
|
return int(v)
|
|
case float64:
|
|
return int(v)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// IsReviewDetector reports whether a detector name belongs to the
|
|
// review rulepack's undecidable set — exposed so the review flow can
|
|
// decide which matches still need grounding when it reuses
|
|
// pre-computed rulepack results.
|
|
func IsReviewDetector(name string) bool {
|
|
switch strings.TrimSpace(name) {
|
|
case detectorLoopQueryGo, detectorLoopQueryPy, detectorCheckActMapGo, detectorCheckActDictPy:
|
|
return true
|
|
}
|
|
return false
|
|
}
|