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
280 lines
8.2 KiB
Go
280 lines
8.2 KiB
Go
// Package cfg builds intra-procedural control-flow graphs from a
|
|
// single function's source text, on demand, and runs a classic
|
|
// GEN/KILL reaching-definitions fixpoint over them.
|
|
//
|
|
// The package is deliberately query-time-only: nothing here runs at
|
|
// index time and nothing touches the whole graph. A caller hands in
|
|
// one function's source (typically sliced out of a file by the
|
|
// symbol's line range), names the language, and gets back:
|
|
//
|
|
// - basic blocks, each holding the ordered statements it executes
|
|
// with their line spans and per-statement def/use variable sets;
|
|
// - labeled edges between blocks (seq / true / false / loop_back /
|
|
// break / continue / return / case / exception / finally);
|
|
// - via ReachingDefinitions, statement-granular def→use chains:
|
|
// for every variable read, the set of definitions that can reach
|
|
// it along some path.
|
|
//
|
|
// Seven languages are covered by per-language control-construct
|
|
// tables: Go, Python, JavaScript, TypeScript, Java, Rust, and Ruby.
|
|
// Parsing reuses the parser package's pooled tree-sitter parsers
|
|
// (errored parsers are closed, never pooled — see parser.ParseFile).
|
|
//
|
|
// Scope model. Definitions and uses are tracked by variable NAME
|
|
// within the function: parameters are entry-block definitions,
|
|
// assignments / declarations / augmented assigns define, identifier
|
|
// reads use. Nested function literals (closures, lambdas, inner
|
|
// defs) are treated as opaque — their bodies execute at an unknown
|
|
// later time, so neither their assignments nor their reads are
|
|
// attributed to the enclosing function's statements. Reads of names
|
|
// never defined in the function (globals, package symbols) simply
|
|
// produce no def→use chain.
|
|
package cfg
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// EdgeLabel classifies a control-flow edge between two basic blocks.
|
|
type EdgeLabel string
|
|
|
|
const (
|
|
LabelSeq EdgeLabel = "seq"
|
|
LabelTrue EdgeLabel = "true"
|
|
LabelFalse EdgeLabel = "false"
|
|
LabelLoopBack EdgeLabel = "loop_back"
|
|
LabelBreak EdgeLabel = "break"
|
|
LabelContinue EdgeLabel = "continue"
|
|
LabelReturn EdgeLabel = "return"
|
|
LabelCase EdgeLabel = "case"
|
|
LabelException EdgeLabel = "exception"
|
|
LabelFinally EdgeLabel = "finally"
|
|
)
|
|
|
|
// Statement is one executable statement (or synthetic pseudo-
|
|
// statement: parameter binding, branch condition, case label) inside
|
|
// a basic block. Lines are 1-based and already shifted by
|
|
// Options.LineOffset so they are file-absolute when the caller
|
|
// passes the function's position.
|
|
type Statement struct {
|
|
Index int `json:"index"`
|
|
Block int `json:"block"`
|
|
StartLine int `json:"start_line"`
|
|
EndLine int `json:"end_line"`
|
|
Text string `json:"text"`
|
|
Kind string `json:"kind,omitempty"`
|
|
Defs []string `json:"defs,omitempty"`
|
|
Uses []string `json:"uses,omitempty"`
|
|
}
|
|
|
|
// Block is a basic block: a maximal straight-line statement sequence
|
|
// with control transfers only at the end.
|
|
type Block struct {
|
|
ID int
|
|
Label string
|
|
Stmts []*Statement
|
|
}
|
|
|
|
// Edge is one labeled control-flow edge.
|
|
type Edge struct {
|
|
From int `json:"from"`
|
|
To int `json:"to"`
|
|
Label EdgeLabel `json:"label"`
|
|
}
|
|
|
|
// CFG is a per-function control-flow graph. Blocks[Entry] holds the
|
|
// synthetic parameter definitions; Blocks[Exit] is the empty sink
|
|
// every return/fall-off-the-end edge targets.
|
|
type CFG struct {
|
|
FuncName string
|
|
Language string
|
|
Entry int
|
|
Exit int
|
|
Blocks []*Block
|
|
Edges []Edge
|
|
Stmts []*Statement
|
|
}
|
|
|
|
// Options tunes Build.
|
|
type Options struct {
|
|
// LineOffset is added to every (1-based) snippet line so the CFG
|
|
// reports file-absolute lines. Pass the function node's
|
|
// StartLine-1 when src is sliced out of a larger file.
|
|
LineOffset int
|
|
// FuncName overrides the name discovered from the AST.
|
|
FuncName string
|
|
}
|
|
|
|
// Build parses src as one function in the given language and
|
|
// constructs its control-flow graph. src must contain (or start
|
|
// with) a single function/method definition — the first function-
|
|
// like node found in the parse tree is used. Languages whose
|
|
// methods don't parse standalone (Java, JS/TS class methods) are
|
|
// retried inside a synthetic class wrapper.
|
|
func Build(src []byte, language string, opts Options) (*CFG, error) {
|
|
spec := specFor(language)
|
|
if spec == nil {
|
|
return nil, fmt.Errorf("cfg: unsupported language %q", language)
|
|
}
|
|
prepared := src
|
|
if spec.dedent {
|
|
prepared = dedent(src)
|
|
}
|
|
|
|
tree, err := parser.ParseFile(prepared, spec.grammar())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cfg: parse: %w", err)
|
|
}
|
|
fn := findFuncRoot(tree.RootNode(), spec)
|
|
wrapOffset := 0
|
|
if fn == nil && spec.classWrap {
|
|
tree.Close()
|
|
wrapped := append([]byte("class __gortexcfg__ {\n"), prepared...)
|
|
wrapped = append(wrapped, []byte("\n}")...)
|
|
tree, err = parser.ParseFile(wrapped, spec.grammar())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cfg: parse (wrapped): %w", err)
|
|
}
|
|
fn = findFuncRoot(tree.RootNode(), spec)
|
|
wrapOffset = -1
|
|
prepared = wrapped
|
|
}
|
|
defer tree.Close()
|
|
if fn == nil {
|
|
return nil, errors.New("cfg: no function definition found in source")
|
|
}
|
|
|
|
c := &CFG{Language: spec.name, FuncName: opts.FuncName}
|
|
if c.FuncName == "" {
|
|
if nameNode := fn.ChildByFieldName("name"); nameNode != nil {
|
|
c.FuncName = nameNode.Content(prepared)
|
|
}
|
|
}
|
|
|
|
b := &builder{
|
|
spec: spec,
|
|
src: prepared,
|
|
lineOffset: opts.LineOffset + wrapOffset,
|
|
cfg: c,
|
|
edgeSeen: map[edgeKey]bool{},
|
|
}
|
|
entry := b.newBlock("entry")
|
|
exit := b.newBlock("exit")
|
|
c.Entry, c.Exit = entry.ID, exit.ID
|
|
|
|
// Parameters become block-0 definitions: one synthetic statement
|
|
// per parameter so every chain points at its own binding site.
|
|
b.cur = entry
|
|
for _, p := range spec.params(fn, prepared) {
|
|
st := b.record(p.line, p.line, p.name, "param")
|
|
st.Defs = []string{p.name}
|
|
}
|
|
|
|
body := spec.bodyOf(fn)
|
|
if body == nil {
|
|
// A bodiless declaration (interface method, abstract method)
|
|
// still yields a degenerate-but-valid CFG.
|
|
b.edge(entry, exit, LabelSeq)
|
|
return c, nil
|
|
}
|
|
|
|
first := b.newBlock("body")
|
|
b.edge(entry, first, LabelSeq)
|
|
b.cur = first
|
|
b.buildStmt(body)
|
|
if b.cur != nil {
|
|
b.edge(b.cur, exit, LabelSeq)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// findFuncRoot returns the first function-like node in breadth-first
|
|
// order, so the outermost definition wins when functions nest.
|
|
func findFuncRoot(root *sitter.Node, spec *langSpec) *sitter.Node {
|
|
if root == nil {
|
|
return nil
|
|
}
|
|
queue := []*sitter.Node{root}
|
|
for len(queue) > 0 {
|
|
n := queue[0]
|
|
queue = queue[1:]
|
|
if spec.funcKinds[n.Type()] {
|
|
return n
|
|
}
|
|
for i := 0; i < int(n.NamedChildCount()); i++ {
|
|
if c := n.NamedChild(i); c != nil {
|
|
queue = append(queue, c)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// dedent strips the longest common leading whitespace prefix from
|
|
// every non-blank line. Needed for indentation-sensitive snippets
|
|
// (a Python method sliced out of a class body would otherwise parse
|
|
// as an indentation error).
|
|
func dedent(src []byte) []byte {
|
|
lines := strings.Split(string(src), "\n")
|
|
prefix := ""
|
|
first := true
|
|
for _, ln := range lines {
|
|
if strings.TrimSpace(ln) == "" {
|
|
continue
|
|
}
|
|
indent := ln[:len(ln)-len(strings.TrimLeft(ln, " \t"))]
|
|
if first {
|
|
prefix = indent
|
|
first = false
|
|
continue
|
|
}
|
|
for !strings.HasPrefix(ln, prefix) {
|
|
prefix = prefix[:len(prefix)-1]
|
|
}
|
|
if prefix == "" {
|
|
return src
|
|
}
|
|
}
|
|
if prefix == "" {
|
|
return src
|
|
}
|
|
for i, ln := range lines {
|
|
lines[i] = strings.TrimPrefix(ln, prefix)
|
|
}
|
|
return []byte(strings.Join(lines, "\n"))
|
|
}
|
|
|
|
// StatementAt returns the statement covering the given (file-
|
|
// absolute) line that defines name, or nil. Used by the dataflow
|
|
// refinement layer to anchor graph binding nodes onto CFG
|
|
// statements.
|
|
func (c *CFG) StatementAt(line int, definedVar string) *Statement {
|
|
var best *Statement
|
|
for _, st := range c.Stmts {
|
|
if line < st.StartLine || line > st.EndLine {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, d := range st.Defs {
|
|
if d == definedVar {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
// Prefer the tightest span when statements nest (loop
|
|
// headers cover their condition line, etc.).
|
|
if best == nil || (st.EndLine-st.StartLine) < (best.EndLine-best.StartLine) {
|
|
best = st
|
|
}
|
|
}
|
|
return best
|
|
}
|