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

1288 lines
39 KiB
Go

package cfg
import (
"strings"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
golang "github.com/zzet/gortex/internal/parser/tsitter/golang"
javalang "github.com/zzet/gortex/internal/parser/tsitter/java"
jslang "github.com/zzet/gortex/internal/parser/tsitter/javascript"
pylang "github.com/zzet/gortex/internal/parser/tsitter/python"
rubylang "github.com/zzet/gortex/internal/parser/tsitter/ruby"
rustlang "github.com/zzet/gortex/internal/parser/tsitter/rust"
tsxlang "github.com/zzet/gortex/internal/parser/tsitter/tsx"
tslang "github.com/zzet/gortex/internal/parser/tsitter/typescript"
)
// assignMode says whether an assignment-shaped construct also reads
// its targets before writing them.
type assignMode int
const (
augNever assignMode = iota // plain assignment / declaration
augAlways // augmented assign (x += 1): def + use
augIfOp // augmented iff the operator token isn't bare "="
)
// assignRule describes one assignment-shaped node kind: which field
// holds the write targets and whether the targets are also read.
type assignRule struct {
lhsField string
mode assignMode
}
// updateRule describes increment/decrement-shaped nodes whose single
// target is both read and written. An empty field means "first named
// child".
type updateRule struct {
field string
}
// param is one parameter binding discovered in a function header.
type param struct {
name string
line int
}
// langSpec is the per-language table driving CFG construction and
// def/use extraction. The shared builder owns all block/edge
// mechanics; the spec only names the AST shapes.
type langSpec struct {
name string
grammar func() *sitter.Language
classWrap bool // retry parse inside `class __gortexcfg__ { … }`
dedent bool // strip common indentation before parsing
funcKinds map[string]bool
// dispatch consumes control constructs; returning false makes the
// builder record the node as a leaf statement.
dispatch func(b *builder, n *sitter.Node) bool
identKinds map[string]bool
assigns map[string]assignRule
updates map[string]updateRule
skipFields map[string]map[string]bool
skipKinds map[string]bool
nestedFuncs map[string]bool
patternContainers map[string]bool
paramSkipFields map[string]bool
paramSkipKinds map[string]bool
}
// bodyOf locates a function node's body, falling back from the
// `body` field to the language's known body node kind (Ruby methods
// carry an unfielded body_statement in some grammar shapes).
func (s *langSpec) bodyOf(fn *sitter.Node) *sitter.Node {
if b := fn.ChildByFieldName("body"); b != nil {
return b
}
if s.name == "ruby" {
return childOfType(fn, "body_statement")
}
return nil
}
// params collects the function's parameter bindings (including the
// Go receiver) as block-0 definitions.
func (s *langSpec) params(fn *sitter.Node, src []byte) []param {
var out []param
collect := func(container *sitter.Node) {
if container == nil {
return
}
var walk func(n *sitter.Node)
walk = func(n *sitter.Node) {
if s.identKinds[n.Type()] {
name := n.Content(src)
if name != "" && name != "_" {
out = append(out, param{name: name, line: int(n.StartPoint().Row) + 1})
}
return
}
if s.paramSkipKinds[n.Type()] {
return
}
cnt := int(n.ChildCount())
for i := 0; i < cnt; i++ {
c := n.Child(i)
if c == nil || !c.IsNamed() {
continue
}
if f := n.FieldNameForChild(i); f != "" && s.paramSkipFields[f] {
continue
}
walk(c)
}
}
walk(container)
}
if s.name == "go" {
collect(fn.ChildByFieldName("receiver"))
}
if p := fn.ChildByFieldName("parameters"); p != nil {
collect(p)
} else if p := fn.ChildByFieldName("parameter"); p != nil {
// JS arrow functions with a single unparenthesized parameter.
collect(p)
}
return out
}
// specFor maps a graph language label to its spec. Returns nil for
// languages without a control-construct table.
func specFor(language string) *langSpec {
switch strings.ToLower(language) {
case "go", "golang":
return goSpec
case "python", "py":
return pySpec
case "javascript", "js", "jsx":
return jsSpec
case "typescript", "ts":
return tsSpec
case "tsx":
return tsxSpec
case "java":
return javaSpec
case "rust", "rs":
return rustSpec
case "ruby", "rb":
return rubySpec
}
return nil
}
// SupportedLanguage reports whether Build can construct a CFG for
// the given graph language label.
func SupportedLanguage(language string) bool { return specFor(language) != nil }
// childOfType returns the first named child with the given type.
func childOfType(n *sitter.Node, kind string) *sitter.Node {
for i := 0; i < int(n.NamedChildCount()); i++ {
c := n.NamedChild(i)
if c != nil && c.Type() == kind {
return c
}
}
return nil
}
// childrenByField returns every child stored under the given field
// name, in order. tree-sitter allows repeated fields (Python's
// if_statement stacks elif/else clauses under `alternative`).
func childrenByField(n *sitter.Node, field string) []*sitter.Node {
var out []*sitter.Node
cnt := int(n.ChildCount())
for i := 0; i < cnt; i++ {
if n.FieldNameForChild(i) == field {
if c := n.Child(i); c != nil {
out = append(out, c)
}
}
}
return out
}
// ---------------------------------------------------------------------------
// Go
// ---------------------------------------------------------------------------
var goSpec = &langSpec{
name: "go",
grammar: golang.GetLanguage,
funcKinds: map[string]bool{
"function_declaration": true, "method_declaration": true, "func_literal": true,
},
identKinds: map[string]bool{"identifier": true},
assigns: map[string]assignRule{
"short_var_declaration": {lhsField: "left", mode: augNever},
"assignment_statement": {lhsField: "left", mode: augIfOp},
"var_spec": {lhsField: "name", mode: augNever},
"const_spec": {lhsField: "name", mode: augNever},
"range_clause": {lhsField: "left", mode: augNever},
"receive_statement": {lhsField: "left", mode: augNever},
},
updates: map[string]updateRule{
"inc_statement": {}, "dec_statement": {},
},
skipFields: map[string]map[string]bool{
"selector_expression": {"field": true},
"keyed_element": {"key": true},
},
skipKinds: map[string]bool{
"field_identifier": true, "type_identifier": true, "package_identifier": true,
"label_name": true,
},
nestedFuncs: map[string]bool{"func_literal": true},
patternContainers: map[string]bool{"expression_list": true},
paramSkipFields: map[string]bool{"type": true},
paramSkipKinds: map[string]bool{"type_identifier": true, "qualified_type": true},
dispatch: goDispatch,
}
func goDispatch(b *builder, n *sitter.Node) bool {
switch n.Type() {
case "block", "statement_list":
b.buildSeq(n)
case "if_statement":
b.buildIf(n.ChildByFieldName("initializer"), n.ChildByFieldName("condition"),
n.ChildByFieldName("consequence"), n.ChildByFieldName("alternative"))
case "for_statement":
body := n.ChildByFieldName("body")
if fc := childOfType(n, "for_clause"); fc != nil {
b.buildLoop(loopParts{
init: fc.ChildByFieldName("initializer"),
cond: fc.ChildByFieldName("condition"),
update: fc.ChildByFieldName("update"),
body: body,
})
return true
}
if rc := childOfType(n, "range_clause"); rc != nil {
b.buildLoop(loopParts{headerStmt: rc, body: body})
return true
}
// `for cond { … }` — the condition is the lone named child
// that isn't the body; bare `for { … }` has none.
var cond *sitter.Node
for i := 0; i < int(n.NamedChildCount()); i++ {
c := n.NamedChild(i)
if c != nil && !c.Equal(body) && c.Type() != "comment" {
cond = c
break
}
}
b.buildLoop(loopParts{cond: cond, body: body, infinite: cond == nil})
case "expression_switch_statement", "type_switch_statement", "select_statement":
b.buildGoSwitch(n)
case "labeled_statement":
if lbl := childOfType(n, "label_name"); lbl != nil {
b.pendingLabel = lbl.Content(b.src)
}
for i := 0; i < int(n.NamedChildCount()); i++ {
c := n.NamedChild(i)
if c != nil && c.Type() != "label_name" {
b.buildStmt(c)
}
}
b.pendingLabel = ""
case "break_statement":
b.buildBreak(n, goJumpLabel(n, b.src))
case "continue_statement":
b.buildContinue(n, goJumpLabel(n, b.src))
case "return_statement":
b.buildReturn(n, "return", LabelReturn)
case "fallthrough_statement":
b.leaf(n, "fallthrough")
b.pendingFallthrough = true
case "defer_statement":
// Defers run at function exit; they do not alter intra-block
// flow, so the statement is recorded in place and flagged.
b.leaf(n, "defer")
case "go_statement":
b.leaf(n, "go")
default:
return false
}
return true
}
// goJumpLabel extracts the optional label off a break/continue.
func goJumpLabel(n *sitter.Node, src []byte) string {
if lbl := childOfType(n, "label_name"); lbl != nil {
return lbl.Content(src)
}
return ""
}
// buildGoSwitch covers expression/type switches and select: every
// case is dispatched from the head, cases do not fall through unless
// an explicit fallthrough statement was seen.
func (b *builder) buildGoSwitch(n *sitter.Node) {
b.ensureCur()
if init := n.ChildByFieldName("initializer"); init != nil {
b.buildStmt(init)
}
if alias := n.ChildByFieldName("alias"); alias != nil {
// Type switch `switch v := x.(type)`: the alias binds v as a
// definition, the switched value field is a read. (The grammar
// always exposes `value` for the subject, so the binding lives
// in the alias field, not value.)
st := b.recordNode(alias, "cond")
st.Defs, _ = extractDefUse(b.spec, b.src, alias, true)
if v := n.ChildByFieldName("value"); v != nil {
_, st.Uses = extractDefUse(b.spec, b.src, v, false)
}
} else if v := n.ChildByFieldName("value"); v != nil {
b.addStmt(v, "cond")
}
head := b.cur
after := b.newBlock("switch_end")
// Go `break` (labeled or bare) exits the switch/select, not the
// enclosing loop. Push a break-only frame (no continue target, so
// `continue` still skips past it to the loop) and consume any
// pending label from an enclosing labeled statement.
b.pushFrame(frame{label: b.takeLabel(), breakTo: after})
defer b.popFrame()
hasDefault := false
var pendingFT *Block
for i := 0; i < int(n.NamedChildCount()); i++ {
c := n.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "expression_case", "type_case", "communication_case", "default_case":
default:
continue
}
caseBlock := b.newBlock("case")
b.edge(head, caseBlock, LabelCase)
if pendingFT != nil {
b.edge(pendingFT, caseBlock, LabelSeq)
pendingFT = nil
}
b.cur = caseBlock
if c.Type() == "default_case" {
hasDefault = true
} else if c.Type() == "communication_case" {
if comm := c.ChildByFieldName("communication"); comm != nil {
b.addStmt(comm, "case")
}
} else if v := c.ChildByFieldName("value"); v != nil {
b.addStmt(v, "case")
} else if tn := c.ChildByFieldName("type"); tn != nil {
b.addStmt(tn, "case")
}
if sl := childOfType(c, "statement_list"); sl != nil {
b.buildSeq(sl)
}
if b.pendingFallthrough {
pendingFT = b.cur
b.pendingFallthrough = false
b.cur = nil
}
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
}
if !hasDefault {
b.edge(head, after, LabelFalse)
}
b.cur = after
}
// ---------------------------------------------------------------------------
// Python
// ---------------------------------------------------------------------------
var pySpec = &langSpec{
name: "python",
grammar: pylang.GetLanguage,
dedent: true,
funcKinds: map[string]bool{
"function_definition": true,
},
identKinds: map[string]bool{"identifier": true},
assigns: map[string]assignRule{
"assignment": {lhsField: "left", mode: augNever},
"augmented_assignment": {lhsField: "left", mode: augAlways},
"named_expression": {lhsField: "name", mode: augNever},
"as_pattern": {lhsField: "alias", mode: augNever},
},
updates: map[string]updateRule{},
skipFields: map[string]map[string]bool{
"attribute": {"attribute": true},
"keyword_argument": {"name": true},
},
skipKinds: map[string]bool{},
nestedFuncs: map[string]bool{
"function_definition": true, "lambda": true, "class_definition": true,
},
patternContainers: map[string]bool{
"pattern_list": true, "tuple_pattern": true, "list_pattern": true,
"as_pattern_target": true,
},
paramSkipFields: map[string]bool{"type": true, "value": true},
paramSkipKinds: map[string]bool{"type": true},
dispatch: pyDispatch,
}
func pyDispatch(b *builder, n *sitter.Node) bool {
switch n.Type() {
case "block":
b.buildSeq(n)
case "if_statement":
b.buildIfChain(n.ChildByFieldName("condition"), n.ChildByFieldName("consequence"),
childrenByField(n, "alternative"))
case "elif_clause":
// Reached only via buildIfChain recursion fallback.
b.buildIfChain(n.ChildByFieldName("condition"), n.ChildByFieldName("consequence"), nil)
case "else_clause":
if body := n.ChildByFieldName("body"); body != nil {
b.buildStmt(body)
} else {
b.buildSeq(n)
}
case "while_statement":
b.buildLoop(loopParts{
cond: n.ChildByFieldName("condition"),
body: n.ChildByFieldName("body"),
elseNode: n.ChildByFieldName("alternative"),
})
case "for_statement":
b.buildLoop(loopParts{
headerStmt: n,
headerStmtOnlyHeaderFields: true,
body: n.ChildByFieldName("body"),
elseNode: n.ChildByFieldName("alternative"),
})
case "try_statement":
b.buildPyTry(n)
case "with_statement":
// `with` introduces bindings but no branching beyond the
// (ignored) exception path already modeled by try blocks.
if cl := childOfType(n, "with_clause"); cl != nil {
b.leaf(cl, "with")
}
if body := n.ChildByFieldName("body"); body != nil {
b.buildStmt(body)
}
case "break_statement":
b.buildBreak(n, "")
case "continue_statement":
b.buildContinue(n, "")
case "return_statement":
b.buildReturn(n, "return", LabelReturn)
case "raise_statement":
b.buildReturn(n, "throw", LabelException)
case "match_statement":
b.buildPyMatch(n)
default:
return false
}
return true
}
// buildPyTry maps try/except/else/finally onto the generic try
// builder.
func (b *builder) buildPyTry(n *sitter.Node) {
p := tryParts{bodyNode: n.ChildByFieldName("body")}
for i := 0; i < int(n.NamedChildCount()); i++ {
c := n.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "except_clause", "except_group_clause":
h := handlerPart{bodyNode: childOfType(c, "block")}
if v := c.ChildByFieldName("value"); v != nil {
h.headerNode = v
}
p.handlers = append(p.handlers, h)
case "else_clause":
p.elseNode = childOfType(c, "block")
case "finally_clause":
p.finallyNode = childOfType(c, "block")
}
}
b.buildTry(p)
}
// buildPyMatch maps structural pattern matching onto the switch
// shape: every case is dispatched from the subject, no fallthrough.
func (b *builder) buildPyMatch(n *sitter.Node) {
b.ensureCur()
if subj := n.ChildByFieldName("subject"); subj != nil {
b.addStmt(subj, "cond")
}
head := b.cur
after := b.newBlock("match_end")
// The case clauses live inside the match's body block (stored
// under the `alternative` field), not as direct children of the
// match_statement, so descend into the body before scanning.
scope := n.ChildByFieldName("body")
if scope == nil {
scope = n
}
matchedAll := false
for i := 0; i < int(scope.NamedChildCount()); i++ {
c := scope.NamedChild(i)
if c == nil || c.Type() != "case_clause" {
continue
}
caseBlock := b.newBlock("case")
b.edge(head, caseBlock, LabelCase)
b.cur = caseBlock
// The case pattern binds names (capture patterns) — record it
// as a definition; the guard, when present, reads.
if pat := childOfType(c, "case_pattern"); pat != nil {
st := b.recordNode(pat, "case")
st.Defs, _ = extractDefUse(b.spec, b.src, pat, true)
if g := c.ChildByFieldName("guard"); g != nil {
_, st.Uses = extractDefUse(b.spec, b.src, g, false)
}
if isPyWildcardCase(pat) {
matchedAll = true
}
}
if body := c.ChildByFieldName("consequence"); body != nil {
b.buildStmt(body)
}
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
}
if !matchedAll {
b.edge(head, after, LabelFalse)
}
b.cur = after
}
// isPyWildcardCase reports whether a case_pattern is the catch-all
// `case _`: the grammar emits an empty case_pattern (no children) for
// the wildcard, so the subsequent arms are unreachable.
func isPyWildcardCase(pat *sitter.Node) bool {
return pat.NamedChildCount() == 0
}
// ---------------------------------------------------------------------------
// JavaScript / TypeScript
// ---------------------------------------------------------------------------
func jsLikeSpec(name string, grammar func() *sitter.Language) *langSpec {
return &langSpec{
name: name,
grammar: grammar,
classWrap: true,
funcKinds: map[string]bool{
"function_declaration": true, "function_expression": true, "function": true,
"generator_function_declaration": true, "generator_function": true,
"method_definition": true, "arrow_function": true,
},
identKinds: map[string]bool{
"identifier": true, "shorthand_property_identifier_pattern": true,
},
assigns: map[string]assignRule{
"variable_declarator": {lhsField: "name", mode: augNever},
"assignment_expression": {lhsField: "left", mode: augNever},
"augmented_assignment_expression": {lhsField: "left", mode: augAlways},
"assignment_pattern": {lhsField: "left", mode: augNever},
},
updates: map[string]updateRule{
"update_expression": {field: "argument"},
},
skipFields: map[string]map[string]bool{
"member_expression": {"property": true},
"pair": {"key": true},
"pair_pattern": {"key": true},
},
skipKinds: map[string]bool{
"property_identifier": true, "statement_identifier": true,
"type_annotation": true, "type_identifier": true, "predefined_type": true,
},
nestedFuncs: map[string]bool{
"function_declaration": true, "function_expression": true, "function": true,
"generator_function_declaration": true, "generator_function": true,
"method_definition": true, "arrow_function": true, "class_declaration": true,
"class": true,
},
patternContainers: map[string]bool{
"array_pattern": true, "object_pattern": true, "pair_pattern": true,
"rest_pattern": true,
},
paramSkipFields: map[string]bool{"type": true, "value": true, "right": true},
paramSkipKinds: map[string]bool{"type_annotation": true},
dispatch: jsDispatch,
}
}
var (
jsSpec = jsLikeSpec("javascript", jslang.GetLanguage)
tsSpec = jsLikeSpec("typescript", tslang.GetLanguage)
tsxSpec = jsLikeSpec("tsx", tsxlang.GetLanguage)
)
func jsDispatch(b *builder, n *sitter.Node) bool {
switch n.Type() {
case "statement_block":
b.buildSeq(n)
case "if_statement":
b.buildIf(nil, n.ChildByFieldName("condition"),
n.ChildByFieldName("consequence"), n.ChildByFieldName("alternative"))
case "else_clause":
for i := 0; i < int(n.NamedChildCount()); i++ {
if c := n.NamedChild(i); c != nil {
b.buildStmt(c)
}
}
case "while_statement":
b.buildLoop(loopParts{cond: n.ChildByFieldName("condition"), body: n.ChildByFieldName("body")})
case "do_statement":
b.buildLoop(loopParts{cond: n.ChildByFieldName("condition"), body: n.ChildByFieldName("body"), postTest: true})
case "for_statement":
b.buildLoop(loopParts{
init: n.ChildByFieldName("initializer"),
cond: n.ChildByFieldName("condition"),
update: n.ChildByFieldName("increment"),
body: n.ChildByFieldName("body"),
})
case "for_in_statement":
b.buildLoop(loopParts{headerStmt: n, headerStmtOnlyHeaderFields: true, body: n.ChildByFieldName("body")})
case "switch_statement":
b.buildJsSwitch(n)
case "try_statement":
p := tryParts{bodyNode: n.ChildByFieldName("body")}
if h := n.ChildByFieldName("handler"); h != nil {
p.handlers = append(p.handlers, handlerPart{
headerNode: h.ChildByFieldName("parameter"),
headerDefs: true,
bodyNode: h.ChildByFieldName("body"),
})
}
if f := n.ChildByFieldName("finalizer"); f != nil {
p.finallyNode = f.ChildByFieldName("body")
}
b.buildTry(p)
case "labeled_statement":
if lbl := n.ChildByFieldName("label"); lbl != nil {
b.pendingLabel = lbl.Content(b.src)
}
if body := n.ChildByFieldName("body"); body != nil {
b.buildStmt(body)
}
b.pendingLabel = ""
case "break_statement":
b.buildBreak(n, fieldText(n, "label", b.src))
case "continue_statement":
b.buildContinue(n, fieldText(n, "label", b.src))
case "return_statement":
b.buildReturn(n, "return", LabelReturn)
case "throw_statement":
b.buildReturn(n, "throw", LabelException)
default:
return false
}
return true
}
// buildJsSwitch models C-style fallthrough: consecutive cases chain
// unless a break/return terminated the previous one; break targets
// the switch end. Java arrow rules (`case 1 -> …`) never fall
// through, so they bypass the fallthrough chaining and edge straight
// to the switch end.
func (b *builder) buildJsSwitch(n *sitter.Node) {
b.ensureCur()
if v := n.ChildByFieldName("value"); v != nil {
b.addStmt(v, "cond")
} else if v := n.ChildByFieldName("condition"); v != nil {
b.addStmt(v, "cond")
}
head := b.cur
after := b.newBlock("switch_end")
b.pushFrame(frame{breakTo: after})
hasDefault := false
var prevEnd *Block
body := n.ChildByFieldName("body")
if body == nil {
body = n
}
for i := 0; i < int(body.NamedChildCount()); i++ {
c := body.NamedChild(i)
if c == nil {
continue
}
var isDefault, isArrowRule bool
switch c.Type() {
case "switch_case":
case "switch_default":
isDefault = true
case "switch_block_statement_group":
isDefault = javaGroupIsDefault(c)
case "switch_rule":
// Arrow form: `case 1 -> { … }`. No fallthrough.
isDefault = javaGroupIsDefault(c)
isArrowRule = true
default:
continue
}
caseBlock := b.newBlock("case")
b.edge(head, caseBlock, LabelCase)
if prevEnd != nil && !isArrowRule {
b.edge(prevEnd, caseBlock, LabelSeq)
}
b.cur = caseBlock
if isDefault {
hasDefault = true
}
b.buildCaseGroupBody(c)
if isArrowRule {
// Implicit break: the rule's end leaves the switch directly
// and does not chain into the next rule.
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
prevEnd = nil
continue
}
prevEnd = b.cur
}
b.popFrame()
if prevEnd != nil {
b.edge(prevEnd, after, LabelSeq)
}
if !hasDefault {
b.edge(head, after, LabelFalse)
}
b.cur = after
}
// buildCaseGroupBody emits the case-label match values then the
// case's statements. Works for JS switch_case/switch_default (value
// field + repeated body fields) and Java switch_block_statement_group
// (switch_label children followed by statements).
func (b *builder) buildCaseGroupBody(c *sitter.Node) {
if v := c.ChildByFieldName("value"); v != nil {
b.addStmt(v, "case")
}
cnt := int(c.ChildCount())
for i := 0; i < cnt; i++ {
ch := c.Child(i)
if ch == nil || !ch.IsNamed() {
continue
}
f := c.FieldNameForChild(i)
if f == "value" {
continue
}
if ch.Type() == "switch_label" {
if ch.NamedChildCount() > 0 {
b.addStmt(ch, "case")
}
continue
}
if f == "body" || f == "" {
b.buildStmt(ch)
}
}
}
// javaGroupIsDefault reports whether a Java case group carries the
// `default` label (a switch_label with no children).
func javaGroupIsDefault(c *sitter.Node) bool {
for i := 0; i < int(c.NamedChildCount()); i++ {
ch := c.NamedChild(i)
if ch != nil && ch.Type() == "switch_label" && ch.NamedChildCount() == 0 {
return true
}
}
return false
}
func fieldText(n *sitter.Node, field string, src []byte) string {
if c := n.ChildByFieldName(field); c != nil {
return c.Content(src)
}
return ""
}
// ---------------------------------------------------------------------------
// Java
// ---------------------------------------------------------------------------
var javaSpec = &langSpec{
name: "java",
grammar: javalang.GetLanguage,
classWrap: true,
funcKinds: map[string]bool{
"method_declaration": true, "constructor_declaration": true,
},
identKinds: map[string]bool{"identifier": true},
assigns: map[string]assignRule{
"variable_declarator": {lhsField: "name", mode: augNever},
"assignment_expression": {lhsField: "left", mode: augIfOp},
"resource": {lhsField: "name", mode: augNever},
},
updates: map[string]updateRule{
"update_expression": {},
},
skipFields: map[string]map[string]bool{
"field_access": {"field": true},
"method_invocation": {"name": true},
"method_reference": {},
},
skipKinds: map[string]bool{
"type_identifier": true, "integral_type": true, "floating_point_type": true,
"boolean_type": true, "void_type": true, "generic_type": true,
"annotation": true, "marker_annotation": true, "modifiers": true,
},
nestedFuncs: map[string]bool{
"lambda_expression": true, "class_declaration": true, "anonymous_class_body": true,
},
patternContainers: map[string]bool{},
paramSkipFields: map[string]bool{"type": true, "dimensions": true},
paramSkipKinds: map[string]bool{
"type_identifier": true, "integral_type": true, "floating_point_type": true,
"boolean_type": true, "generic_type": true, "annotation": true,
"marker_annotation": true, "modifiers": true,
},
dispatch: javaDispatch,
}
func javaDispatch(b *builder, n *sitter.Node) bool {
switch n.Type() {
case "block":
b.buildSeq(n)
case "if_statement":
b.buildIf(nil, n.ChildByFieldName("condition"),
n.ChildByFieldName("consequence"), n.ChildByFieldName("alternative"))
case "while_statement":
b.buildLoop(loopParts{cond: n.ChildByFieldName("condition"), body: n.ChildByFieldName("body")})
case "do_statement":
b.buildLoop(loopParts{cond: n.ChildByFieldName("condition"), body: n.ChildByFieldName("body"), postTest: true})
case "for_statement":
b.buildLoop(loopParts{
init: n.ChildByFieldName("init"),
cond: n.ChildByFieldName("condition"),
update: n.ChildByFieldName("update"),
body: n.ChildByFieldName("body"),
})
case "enhanced_for_statement":
b.buildLoop(loopParts{headerStmt: n, headerStmtOnlyHeaderFields: true, body: n.ChildByFieldName("body")})
case "switch_expression", "switch_statement":
b.buildJsSwitch(n)
case "try_statement", "try_with_resources_statement":
p := tryParts{bodyNode: n.ChildByFieldName("body")}
if res := n.ChildByFieldName("resources"); res != nil {
b.ensureCur()
b.addStmt(res, "with")
}
for i := 0; i < int(n.NamedChildCount()); i++ {
c := n.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "catch_clause":
h := handlerPart{bodyNode: c.ChildByFieldName("body"), headerDefs: true}
if fp := childOfType(c, "catch_formal_parameter"); fp != nil {
h.headerNode = fp
}
p.handlers = append(p.handlers, h)
case "finally_clause":
p.finallyNode = childOfType(c, "block")
}
}
b.buildTry(p)
case "labeled_statement":
// (identifier) ':' statement — no fields in this grammar.
first := n.NamedChild(0)
if first != nil && first.Type() == "identifier" {
b.pendingLabel = first.Content(b.src)
}
for i := 1; i < int(n.NamedChildCount()); i++ {
if c := n.NamedChild(i); c != nil {
b.buildStmt(c)
}
}
b.pendingLabel = ""
case "break_statement":
b.buildBreak(n, javaJumpLabel(n, b.src))
case "continue_statement":
b.buildContinue(n, javaJumpLabel(n, b.src))
case "return_statement":
b.buildReturn(n, "return", LabelReturn)
case "throw_statement":
b.buildReturn(n, "throw", LabelException)
case "synchronized_statement":
if l := n.ChildByFieldName("lock"); l != nil {
b.ensureCur()
b.addStmt(l, "")
}
if body := n.ChildByFieldName("body"); body != nil {
b.buildStmt(body)
}
default:
return false
}
return true
}
func javaJumpLabel(n *sitter.Node, src []byte) string {
if c := n.NamedChild(0); c != nil && c.Type() == "identifier" {
return c.Content(src)
}
return ""
}
// ---------------------------------------------------------------------------
// Rust
// ---------------------------------------------------------------------------
var rustSpec = &langSpec{
name: "rust",
grammar: rustlang.GetLanguage,
funcKinds: map[string]bool{
"function_item": true, "closure_expression": true,
},
identKinds: map[string]bool{"identifier": true},
assigns: map[string]assignRule{
"let_declaration": {lhsField: "pattern", mode: augNever},
"assignment_expression": {lhsField: "left", mode: augNever},
"compound_assignment_expr": {lhsField: "left", mode: augAlways},
"let_condition": {lhsField: "pattern", mode: augNever},
},
updates: map[string]updateRule{},
skipFields: map[string]map[string]bool{
"field_expression": {"field": true},
"tuple_struct_pattern": {"type": true},
"struct_pattern": {"type": true},
"field_initializer": {"field": true},
// A match arm's guard hangs off the pattern node as the
// `condition` field; it reads variables, it doesn't bind them,
// so the binding walk must skip it.
"match_pattern": {"condition": true},
},
skipKinds: map[string]bool{
"type_identifier": true, "primitive_type": true, "field_identifier": true,
"scoped_identifier": true, "scoped_type_identifier": true, "lifetime": true,
"type_arguments": true, "label": true,
},
nestedFuncs: map[string]bool{
"closure_expression": true, "function_item": true,
},
patternContainers: map[string]bool{
"tuple_pattern": true, "tuple_struct_pattern": true, "struct_pattern": true,
"slice_pattern": true, "reference_pattern": true, "mut_pattern": true,
"field_pattern": true, "or_pattern": true, "match_pattern": true,
},
paramSkipFields: map[string]bool{"type": true},
paramSkipKinds: map[string]bool{"type_identifier": true, "primitive_type": true, "reference_type": true},
dispatch: rustDispatch,
}
func rustDispatch(b *builder, n *sitter.Node) bool {
switch n.Type() {
case "block":
b.buildSeq(n)
case "expression_statement":
// Unwrap so control-flow expressions in statement position
// (if/while/match/loop) reach the cases below.
if c := n.NamedChild(0); c != nil {
b.buildStmt(c)
return true
}
return false
case "if_expression":
b.buildIf(nil, n.ChildByFieldName("condition"),
n.ChildByFieldName("consequence"), n.ChildByFieldName("alternative"))
case "else_clause":
for i := 0; i < int(n.NamedChildCount()); i++ {
if c := n.NamedChild(i); c != nil {
b.buildStmt(c)
}
}
case "while_expression":
b.pendingLabel = rustLoopOwnLabel(n, b.src)
b.buildLoop(loopParts{cond: n.ChildByFieldName("condition"), body: n.ChildByFieldName("body")})
case "loop_expression":
b.pendingLabel = rustLoopOwnLabel(n, b.src)
b.buildLoop(loopParts{body: n.ChildByFieldName("body"), infinite: true})
case "for_expression":
b.pendingLabel = rustLoopOwnLabel(n, b.src)
b.buildLoop(loopParts{headerStmt: n, headerStmtOnlyHeaderFields: true, body: n.ChildByFieldName("body")})
case "match_expression":
b.buildRustMatch(n)
case "break_expression":
b.buildBreak(n, rustLoopLabel(n, b.src))
case "continue_expression":
b.buildContinue(n, rustLoopLabel(n, b.src))
case "return_expression":
b.buildReturn(n, "return", LabelReturn)
default:
return false
}
return true
}
// rustLoopLabel extracts the label off a break/continue expression
// (`break 'outer`). The vendored grammar exposes the label as a child
// of node type `label` whose first identifier is the lifetime name.
func rustLoopLabel(n *sitter.Node, src []byte) string {
return rustLabelName(childOfType(n, "label"), src)
}
// rustLoopOwnLabel extracts the label a loop declares for itself
// (`'outer: loop { … }`). The label is a `label` child of the loop
// expression, ahead of the body.
func rustLoopOwnLabel(n *sitter.Node, src []byte) string {
return rustLabelName(childOfType(n, "label"), src)
}
// rustLabelName normalises a `label` node to its bare name (no
// leading `'`). The label identifier is the node's first child; fall
// back to the node text for grammar shapes that inline it.
func rustLabelName(lbl *sitter.Node, src []byte) string {
if lbl == nil {
return ""
}
text := lbl.Content(src)
if id := childOfType(lbl, "identifier"); id != nil {
text = id.Content(src)
}
return strings.TrimPrefix(strings.TrimSpace(text), "'")
}
// buildRustMatch dispatches each arm off the subject; arms never
// fall through and the match is exhaustive, so there is no
// unmatched-subject edge.
func (b *builder) buildRustMatch(n *sitter.Node) {
b.ensureCur()
if v := n.ChildByFieldName("value"); v != nil {
b.addStmt(v, "cond")
}
head := b.cur
after := b.newBlock("match_end")
body := n.ChildByFieldName("body")
if body == nil {
b.cur = after
return
}
for i := 0; i < int(body.NamedChildCount()); i++ {
arm := body.NamedChild(i)
if arm == nil || arm.Type() != "match_arm" {
continue
}
caseBlock := b.newBlock("case")
b.edge(head, caseBlock, LabelCase)
b.cur = caseBlock
if pat := arm.ChildByFieldName("pattern"); pat != nil {
// Arm patterns bind names; the guard (if any) reads. The
// guard lives on the pattern node as its `condition` field
// (skipped by the binding walk via skipFields), so the
// asDef pass over the pattern yields only the captures.
st := b.recordNode(pat, "case")
defs, _ := extractDefUse(b.spec, b.src, pat, true)
st.Defs = defs
if g := pat.ChildByFieldName("condition"); g != nil {
_, uses := extractDefUse(b.spec, b.src, g, false)
st.Uses = uses
}
}
if v := arm.ChildByFieldName("value"); v != nil {
b.buildStmt(v)
}
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
}
b.cur = after
}
// ---------------------------------------------------------------------------
// Ruby
// ---------------------------------------------------------------------------
var rubySpec = &langSpec{
name: "ruby",
grammar: rubylang.GetLanguage,
funcKinds: map[string]bool{
"method": true, "singleton_method": true,
},
identKinds: map[string]bool{"identifier": true},
assigns: map[string]assignRule{
"assignment": {lhsField: "left", mode: augNever},
"operator_assignment": {lhsField: "left", mode: augAlways},
},
updates: map[string]updateRule{},
skipFields: map[string]map[string]bool{
"call": {"method": true},
},
skipKinds: map[string]bool{
"constant": true, "instance_variable": true, "class_variable": true,
"global_variable": true, "symbol": true, "hash_key_symbol": true,
},
nestedFuncs: map[string]bool{
"method": true, "singleton_method": true, "lambda": true,
"do_block": true, "block": true, "class": true, "module": true,
},
patternContainers: map[string]bool{
"left_assignment_list": true, "destructured_left_assignment": true,
"rest_assignment": true,
},
paramSkipFields: map[string]bool{"value": true},
paramSkipKinds: map[string]bool{},
dispatch: rubyDispatch,
}
func rubyDispatch(b *builder, n *sitter.Node) bool {
switch n.Type() {
case "body_statement", "begin":
b.buildRubyBody(n)
case "then", "do", "else", "ensure":
b.buildSeq(n)
case "if", "unless":
b.buildIf(nil, n.ChildByFieldName("condition"),
n.ChildByFieldName("consequence"), n.ChildByFieldName("alternative"))
case "elsif":
b.buildIf(nil, n.ChildByFieldName("condition"),
n.ChildByFieldName("consequence"), n.ChildByFieldName("alternative"))
case "if_modifier", "unless_modifier":
b.buildIf(nil, n.ChildByFieldName("condition"), n.ChildByFieldName("body"), nil)
case "while", "until":
b.buildLoop(loopParts{cond: n.ChildByFieldName("condition"), body: n.ChildByFieldName("body")})
case "while_modifier", "until_modifier":
b.buildLoop(loopParts{cond: n.ChildByFieldName("condition"), body: n.ChildByFieldName("body")})
case "for":
b.buildLoop(loopParts{headerStmt: n, headerStmtOnlyHeaderFields: true, body: n.ChildByFieldName("body")})
case "case":
b.buildRubyCase(n)
case "call":
blk := n.ChildByFieldName("block")
if blk == nil {
blk = childOfType(n, "do_block")
}
if blk == nil {
blk = childOfType(n, "block")
}
if blk == nil {
return false // plain call — leaf statement
}
b.buildRubyBlockCall(n, blk)
case "break":
b.buildBreak(n, "")
case "next", "redo":
b.buildContinue(n, "")
case "return":
b.buildReturn(n, "return", LabelReturn)
case "raise", "throw":
b.buildReturn(n, "throw", LabelException)
default:
return false
}
return true
}
// buildRubyBody handles statement sequences that may carry method-
// level or begin-level rescue/ensure/else clauses.
func (b *builder) buildRubyBody(n *sitter.Node) {
var mainStmts []*sitter.Node
var p tryParts
hasClauses := false
for i := 0; i < int(n.NamedChildCount()); i++ {
c := n.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "rescue":
hasClauses = true
h := handlerPart{}
if v := c.ChildByFieldName("variable"); v != nil {
h.headerNode = v
h.headerDefs = true
} else if ex := c.ChildByFieldName("exceptions"); ex != nil {
h.headerNode = ex
}
h.bodyNode = c.ChildByFieldName("body")
p.handlers = append(p.handlers, h)
case "ensure":
hasClauses = true
p.finallyNode = c
case "else":
hasClauses = true
p.elseNode = c
default:
mainStmts = append(mainStmts, c)
}
}
if !hasClauses {
for _, st := range mainStmts {
b.buildStmt(st)
}
return
}
p.bodyStmts = mainStmts
b.buildTry(p)
}
// buildRubyCase dispatches each `when` off the subject; clauses
// never fall through.
func (b *builder) buildRubyCase(n *sitter.Node) {
b.ensureCur()
if v := n.ChildByFieldName("value"); v != nil {
b.addStmt(v, "cond")
}
head := b.cur
after := b.newBlock("case_end")
hasElse := false
for i := 0; i < int(n.NamedChildCount()); i++ {
c := n.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "when", "in_clause":
caseBlock := b.newBlock("case")
b.edge(head, caseBlock, LabelCase)
b.cur = caseBlock
if pat := c.ChildByFieldName("pattern"); pat != nil {
b.addStmt(pat, "case")
}
if body := c.ChildByFieldName("body"); body != nil {
b.buildStmt(body)
}
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
case "else":
hasElse = true
elseBlock := b.newBlock("case")
b.edge(head, elseBlock, LabelCase)
b.cur = elseBlock
b.buildSeq(c)
if b.cur != nil {
b.edge(b.cur, after, LabelSeq)
}
}
}
if !hasElse {
b.edge(head, after, LabelFalse)
}
b.cur = after
}
// buildRubyBlockCall models `receiver.each do |x| … end` as a loop:
// the call is the header (reads receiver + args), the block body may
// run zero or more times, and `next`/`break` behave like loop
// continue/break.
func (b *builder) buildRubyBlockCall(call, blk *sitter.Node) {
b.ensureCur()
header := b.newBlock("block_call")
b.moveTo(header)
// Uses come from the call minus the block body (the spec's
// nestedFuncs already exclude do_block/block subtrees).
b.addStmt(call, "loop")
after := b.newBlock("block_end")
bodyBlock := b.newBlock("block_body")
b.edge(header, bodyBlock, LabelTrue)
b.edge(header, after, LabelFalse)
b.pushFrame(frame{label: b.takeLabel(), continueTo: header, breakTo: after, isLoop: true})
b.cur = bodyBlock
if params := blk.ChildByFieldName("parameters"); params != nil {
st := b.recordNode(params, "param")
defs, _ := extractDefUse(b.spec, b.src, params, true)
st.Defs = defs
}
if body := blk.ChildByFieldName("body"); body != nil {
// The block body is a body_statement; build it directly so
// rescue clauses inside the block still work.
b.buildStmt(body)
}
if b.cur != nil {
b.edge(b.cur, header, LabelLoopBack)
}
b.popFrame()
b.cur = after
}