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

568 lines
15 KiB
Go

package cfg
import (
"strings"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// maxStmtText caps the recorded statement text so giant one-liners
// don't bloat tool responses.
const maxStmtText = 120
// frame is one entry of the break/continue resolution stack. Loops
// push a frame with both targets; switch statements in languages
// where `break` exits the switch push a frame with only breakTo.
type frame struct {
label string
continueTo *Block
breakTo *Block
isLoop bool
}
// builder drives CFG construction. cur is the block receiving the
// next statement; nil means the current position is past a
// terminator (return/break/…) — the next statement starts a fresh,
// unreachable block so its defs/uses still surface.
type builder struct {
spec *langSpec
src []byte
lineOffset int
cfg *CFG
cur *Block
frames []frame
pendingLabel string
pendingFallthrough bool
edgeSeen map[edgeKey]bool
}
type edgeKey struct {
from, to int
label EdgeLabel
}
func (b *builder) newBlock(label string) *Block {
bl := &Block{ID: len(b.cfg.Blocks), Label: label}
b.cfg.Blocks = append(b.cfg.Blocks, bl)
return bl
}
func (b *builder) edge(from, to *Block, label EdgeLabel) {
if from == nil || to == nil {
return
}
k := edgeKey{from.ID, to.ID, label}
if b.edgeSeen[k] {
return
}
b.edgeSeen[k] = true
b.cfg.Edges = append(b.cfg.Edges, Edge{From: from.ID, To: to.ID, Label: label})
}
// moveTo links cur to bl sequentially and makes bl current.
func (b *builder) moveTo(bl *Block) {
if b.cur != nil {
b.edge(b.cur, bl, LabelSeq)
}
b.cur = bl
}
// ensureCur guarantees a current block, opening an unreachable one
// when the previous statement terminated control flow.
func (b *builder) ensureCur() {
if b.cur == nil {
b.cur = b.newBlock("unreachable")
}
}
func (b *builder) pushFrame(f frame) { b.frames = append(b.frames, f) }
func (b *builder) popFrame() { b.frames = b.frames[:len(b.frames)-1] }
// takeLabel consumes the label set by an enclosing labeled
// statement, if any.
func (b *builder) takeLabel() string {
l := b.pendingLabel
b.pendingLabel = ""
return l
}
// record appends a synthetic statement with explicit position/text.
func (b *builder) record(startLine, endLine int, text, kind string) *Statement {
st := &Statement{
Index: len(b.cfg.Stmts),
Block: b.cur.ID,
StartLine: startLine + b.lineOffset,
EndLine: endLine + b.lineOffset,
Text: text,
Kind: kind,
}
b.cfg.Stmts = append(b.cfg.Stmts, st)
b.cur.Stmts = append(b.cur.Stmts, st)
return st
}
// recordNode appends a statement positioned at n without running
// def/use extraction (callers fill Defs/Uses themselves).
func (b *builder) recordNode(n *sitter.Node, kind string) *Statement {
return b.record(int(n.StartPoint().Row)+1, int(n.EndPoint().Row)+1, stmtText(n, b.src), kind)
}
// addStmt appends a statement for n with def/use extraction.
func (b *builder) addStmt(n *sitter.Node, kind string) *Statement {
if n == nil {
return nil
}
st := b.recordNode(n, kind)
st.Defs, st.Uses = extractDefUse(b.spec, b.src, n, false)
return st
}
// leaf records n as a plain statement in the current block.
func (b *builder) leaf(n *sitter.Node, kind string) {
b.ensureCur()
b.addStmt(n, kind)
}
// stmtText renders the statement's first source line, trimmed and
// capped; multi-line statements get an ellipsis.
func stmtText(n *sitter.Node, src []byte) string {
text := n.Content(src)
if i := strings.IndexByte(text, '\n'); i >= 0 {
text = text[:i] + " …"
}
text = strings.TrimSpace(text)
if len(text) > maxStmtText {
text = text[:maxStmtText] + "…"
}
return text
}
// buildStmt processes one statement node: control constructs are
// consumed by the language dispatch table, everything else is a leaf.
func (b *builder) buildStmt(n *sitter.Node) {
if n == nil {
return
}
if n.Type() == "comment" {
return
}
if b.spec.dispatch(b, n) {
return
}
b.leaf(n, "")
}
// buildSeq processes every named child of n as a statement.
func (b *builder) buildSeq(n *sitter.Node) {
for i := 0; i < int(n.NamedChildCount()); i++ {
if c := n.NamedChild(i); c != nil {
b.buildStmt(c)
}
}
}
// ---------------------------------------------------------------------------
// if / else
// ---------------------------------------------------------------------------
// buildIf wires the classic diamond. alt may itself be an if (else-
// if chains) — the recursive buildStmt handles it through dispatch.
func (b *builder) buildIf(init, cond, then, alt *sitter.Node) {
b.ensureCur()
if init != nil {
b.buildStmt(init)
}
if cond != nil {
b.addStmt(cond, "cond")
}
head := b.cur
after := b.newBlock("if_end")
thenBlock := b.newBlock("then")
b.edge(head, thenBlock, LabelTrue)
b.cur = thenBlock
if then != nil {
b.buildStmt(then)
}
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
if alt != nil {
elseBlock := b.newBlock("else")
b.edge(head, elseBlock, LabelFalse)
b.cur = elseBlock
b.buildStmt(alt)
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
} else {
b.edge(head, after, LabelFalse)
}
b.cur = after
}
// buildIfChain handles grammars that stack elif/else clauses as
// sibling `alternative` fields (Python) instead of nesting them.
func (b *builder) buildIfChain(cond, cons *sitter.Node, alts []*sitter.Node) {
b.ensureCur()
if cond != nil {
b.addStmt(cond, "cond")
}
head := b.cur
after := b.newBlock("if_end")
thenBlock := b.newBlock("then")
b.edge(head, thenBlock, LabelTrue)
b.cur = thenBlock
if cons != nil {
b.buildStmt(cons)
}
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
if len(alts) == 0 {
b.edge(head, after, LabelFalse)
b.cur = after
return
}
elseBlock := b.newBlock("else")
b.edge(head, elseBlock, LabelFalse)
b.cur = elseBlock
first := alts[0]
if first.Type() == "elif_clause" {
b.buildIfChain(first.ChildByFieldName("condition"), first.ChildByFieldName("consequence"), alts[1:])
} else {
// else_clause terminates the chain.
b.buildStmt(first)
}
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
b.cur = after
}
// ---------------------------------------------------------------------------
// loops
// ---------------------------------------------------------------------------
// loopParts feeds buildLoop. Exactly one of {cond, headerStmt,
// infinite} shapes the header:
// - cond: pre/post-test condition loop (while / for / do-while)
// - headerStmt: for-in/range loop; the header statement defines the
// loop variables and reads the iterable. When
// headerStmtOnlyHeaderFields is set the node also contains the
// body, so def/use extraction is restricted to the header fields.
// - infinite: no condition (Go `for {}`, Rust `loop {}`).
type loopParts struct {
init, cond, update, body *sitter.Node
headerStmt *sitter.Node
headerStmtOnlyHeaderFields bool
postTest bool
infinite bool
// elseNode is a Python for/while-else clause. It runs only on a
// normal (non-break) loop exit, so the builder routes the header's
// False edge through the else block while `break` jumps past it to
// a dedicated join block. Nil for every other language.
elseNode *sitter.Node
}
func (b *builder) buildLoop(p loopParts) {
b.ensureCur()
if p.init != nil {
b.buildStmt(p.init)
}
label := b.takeLabel()
if p.postTest {
bodyBlock := b.newBlock("loop_body")
b.moveTo(bodyBlock)
header := b.newBlock("loop_header")
after := b.newBlock("loop_end")
b.pushFrame(frame{label: label, continueTo: header, breakTo: after, isLoop: true})
if p.body != nil {
b.buildStmt(p.body)
}
b.popFrame()
if b.cur != nil {
b.edge(b.cur, header, LabelSeq)
}
b.cur = header
if p.cond != nil {
b.addStmt(p.cond, "cond")
}
b.edge(header, bodyBlock, LabelLoopBack)
b.edge(header, after, LabelFalse)
b.cur = after
return
}
header := b.newBlock("loop_header")
b.moveTo(header)
if p.headerStmt != nil {
b.addLoopHeaderStmt(p)
} else if p.cond != nil {
b.addStmt(p.cond, "cond")
}
after := b.newBlock("loop_end")
bodyBlock := b.newBlock("loop_body")
// With a for/while-else clause, the normal exit (header False) runs
// the else before reaching the join, but `break` must skip it.
// Route break edges at a separate join block; without an else the
// join is the loop_end itself so behaviour is unchanged.
breakTarget := after
if p.elseNode != nil {
breakTarget = b.newBlock("loop_join")
}
if p.infinite {
b.edge(header, bodyBlock, LabelSeq)
} else {
b.edge(header, bodyBlock, LabelTrue)
b.edge(header, after, LabelFalse)
}
var updateBlock *Block
contTarget := header
if p.update != nil {
updateBlock = b.newBlock("loop_update")
contTarget = updateBlock
}
b.pushFrame(frame{label: label, continueTo: contTarget, breakTo: breakTarget, isLoop: true})
b.cur = bodyBlock
if p.body != nil {
b.buildStmt(p.body)
}
b.popFrame()
if updateBlock != nil {
if b.cur != nil {
b.edge(b.cur, updateBlock, LabelSeq)
}
b.cur = updateBlock
b.buildStmt(p.update)
if b.cur != nil {
b.edge(b.cur, header, LabelLoopBack)
}
} else if b.cur != nil {
b.edge(b.cur, header, LabelLoopBack)
}
b.cur = after
if p.elseNode != nil {
// The else clause runs on the False (no-break) exit; break
// edges already bypass it by targeting the join directly.
b.buildStmt(p.elseNode)
if b.cur != nil {
b.edge(b.cur, breakTarget, LabelSeq)
}
b.cur = breakTarget
}
}
// addLoopHeaderStmt records the for-in header: loop variables are
// definitions, the iterable is a use. When the header node embeds
// the body (Python for, JS for-in, Java enhanced-for, Rust for, Ruby
// for) only the header fields are inspected.
func (b *builder) addLoopHeaderStmt(p loopParts) {
n := p.headerStmt
if !p.headerStmtOnlyHeaderFields {
b.addStmt(n, "loop")
return
}
defsNode, usesNode := forInHeaderFields(n)
startLine := int(n.StartPoint().Row) + 1
endLine := startLine
if usesNode != nil {
endLine = int(usesNode.EndPoint().Row) + 1
}
text := stmtText(n, b.src)
st := b.record(startLine, endLine, text, "loop")
if defsNode != nil {
defs, _ := extractDefUse(b.spec, b.src, defsNode, true)
st.Defs = defs
}
if usesNode != nil {
_, uses := extractDefUse(b.spec, b.src, usesNode, false)
st.Uses = uses
}
}
// forInHeaderFields probes the field-name pairs the supported
// grammars use for "<vars> in <iterable>" headers.
func forInHeaderFields(n *sitter.Node) (defs, uses *sitter.Node) {
if l := n.ChildByFieldName("left"); l != nil {
return l, n.ChildByFieldName("right")
}
if p := n.ChildByFieldName("pattern"); p != nil {
return p, n.ChildByFieldName("value")
}
if name := n.ChildByFieldName("name"); name != nil {
return name, n.ChildByFieldName("value")
}
return nil, nil
}
// ---------------------------------------------------------------------------
// jumps
// ---------------------------------------------------------------------------
// findFrame resolves a break/continue target. continue skips frames
// without a continue target (switches); a label restricts the match.
func (b *builder) findFrame(label string, needContinue bool) *frame {
for i := len(b.frames) - 1; i >= 0; i-- {
f := &b.frames[i]
if needContinue && f.continueTo == nil {
continue
}
if label != "" && f.label != label {
continue
}
return f
}
return nil
}
func (b *builder) buildBreak(n *sitter.Node, label string) {
b.ensureCur()
b.addStmt(n, "break")
if f := b.findFrame(label, false); f != nil {
b.edge(b.cur, f.breakTo, LabelBreak)
} else {
// break outside any loop/switch — treat as function exit so
// the flow graph stays connected.
b.edge(b.cur, b.cfg.Blocks[b.cfg.Exit], LabelBreak)
}
b.cur = nil
}
func (b *builder) buildContinue(n *sitter.Node, label string) {
b.ensureCur()
b.addStmt(n, "continue")
if f := b.findFrame(label, true); f != nil {
b.edge(b.cur, f.continueTo, LabelContinue)
} else {
b.edge(b.cur, b.cfg.Blocks[b.cfg.Exit], LabelContinue)
}
b.cur = nil
}
// buildReturn handles return/raise/throw: the statement reads its
// expression and control transfers to the exit block.
func (b *builder) buildReturn(n *sitter.Node, kind string, label EdgeLabel) {
b.ensureCur()
b.addStmt(n, kind)
b.edge(b.cur, b.cfg.Blocks[b.cfg.Exit], label)
b.cur = nil
}
// ---------------------------------------------------------------------------
// try / except / finally
// ---------------------------------------------------------------------------
// handlerPart is one catch/except/rescue clause. headerNode carries
// the exception filter and binding; headerDefs marks the node as a
// pure binding (its identifiers are definitions, e.g. `catch (e)`).
type handlerPart struct {
headerNode *sitter.Node
headerDefs bool
bodyNode *sitter.Node
}
// tryParts feeds buildTry. The protected body is either one block
// node or an explicit statement list (Ruby's method-level rescue).
type tryParts struct {
bodyNode *sitter.Node
bodyStmts []*sitter.Node
handlers []handlerPart
elseNode *sitter.Node
finallyNode *sitter.Node
}
// buildTry wires the protected region: every block created while
// building the body gets an exception edge to every handler — the
// conservative may-throw model (an exception can surface at any
// point of the region, so handler entry merges the region's defs).
// The region opens with an empty marker block so the region's IN
// state also reaches the handlers (an exception can fire before the
// first protected statement completes). Within one basic block the
// model stays block-granular: a def made and re-killed inside the
// same region block is not separately visible to the handler.
func (b *builder) buildTry(p tryParts) {
b.ensureCur()
tryBlock := b.newBlock("try")
b.moveTo(tryBlock)
regionStart := tryBlock.ID
bodyBlock := b.newBlock("try_body")
b.moveTo(bodyBlock)
if p.bodyNode != nil {
b.buildStmt(p.bodyNode)
}
for _, st := range p.bodyStmts {
b.buildStmt(st)
}
tryEnd := b.cur
regionEnd := len(b.cfg.Blocks)
after := b.newBlock("try_end")
handlerEnds := make([]*Block, 0, len(p.handlers))
for _, h := range p.handlers {
hb := b.newBlock("handler")
for id := regionStart; id < regionEnd; id++ {
b.edge(b.cfg.Blocks[id], hb, LabelException)
}
b.cur = hb
if h.headerNode != nil {
st := b.recordNode(h.headerNode, "catch")
st.Defs, st.Uses = extractDefUse(b.spec, b.src, h.headerNode, h.headerDefs)
}
if h.bodyNode != nil {
b.buildStmt(h.bodyNode)
}
handlerEnds = append(handlerEnds, b.cur)
}
mainEnd := tryEnd
if p.elseNode != nil && tryEnd != nil {
eb := b.newBlock("try_else")
b.edge(tryEnd, eb, LabelSeq)
b.cur = eb
b.buildStmt(p.elseNode)
mainEnd = b.cur
}
if p.finallyNode != nil {
fb := b.newBlock("finally")
if mainEnd != nil {
b.edge(mainEnd, fb, LabelSeq)
}
for _, he := range handlerEnds {
if he != nil {
b.edge(he, fb, LabelFinally)
}
}
// An exception that matches no handler (or a handler-less
// try/finally) still runs the finalizer on its way out, so
// the protected region always feeds the finally directly.
for id := regionStart; id < regionEnd; id++ {
b.edge(b.cfg.Blocks[id], fb, LabelException)
}
b.cur = fb
b.buildStmt(p.finallyNode)
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
} else {
if mainEnd != nil {
b.edge(mainEnd, after, LabelSeq)
}
for _, he := range handlerEnds {
if he != nil {
b.edge(he, after, LabelSeq)
}
}
}
b.cur = after
}