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
717 lines
19 KiB
Go
717 lines
19 KiB
Go
package cfg
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mustBuild builds a CFG and fails the test on error.
|
|
func mustBuild(t *testing.T, src, lang string) *CFG {
|
|
t.Helper()
|
|
c, err := Build([]byte(src), lang, Options{})
|
|
if err != nil {
|
|
t.Fatalf("Build(%s): %v", lang, err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// stmtByText finds the first statement whose text contains sub.
|
|
func stmtByText(t *testing.T, c *CFG, sub string) *Statement {
|
|
t.Helper()
|
|
for _, st := range c.Stmts {
|
|
if strings.Contains(st.Text, sub) {
|
|
return st
|
|
}
|
|
}
|
|
t.Fatalf("no statement containing %q; have: %v", sub, stmtTexts(c))
|
|
return nil
|
|
}
|
|
|
|
func stmtTexts(c *CFG) []string {
|
|
out := make([]string, len(c.Stmts))
|
|
for i, st := range c.Stmts {
|
|
out[i] = st.Text
|
|
}
|
|
return out
|
|
}
|
|
|
|
// hasEdge reports whether an edge with the label connects the blocks
|
|
// holding the two statements (or block IDs when from/to are ints).
|
|
func hasEdgeLabel(c *CFG, label EdgeLabel) bool {
|
|
for _, e := range c.Edges {
|
|
if e.Label == label {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func edgeBetween(c *CFG, from, to int, label EdgeLabel) bool {
|
|
for _, e := range c.Edges {
|
|
if e.From == from && e.To == to && e.Label == label {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// chainFor returns the def→use chain for (use statement, var).
|
|
func chainFor(t *testing.T, r *ReachingResult, stmt int, v string) UseChain {
|
|
t.Helper()
|
|
for _, ch := range r.Chains {
|
|
if ch.Stmt == stmt && ch.Var == v {
|
|
return ch
|
|
}
|
|
}
|
|
t.Fatalf("no chain for stmt=%d var=%q; chains: %+v", stmt, v, r.Chains)
|
|
return UseChain{}
|
|
}
|
|
|
|
func hasChain(r *ReachingResult, stmt int, v string) bool {
|
|
for _, ch := range r.Chains {
|
|
if ch.Stmt == stmt && ch.Var == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func containsInt(xs []int, x int) bool {
|
|
for _, v := range xs {
|
|
if v == x {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// construction basics
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestBuildGoIfElseDiamond(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(a int) int {
|
|
x := 1
|
|
if a > 0 {
|
|
x = 2
|
|
} else {
|
|
x = 3
|
|
}
|
|
return x
|
|
}
|
|
`, "go")
|
|
|
|
if c.FuncName != "f" {
|
|
t.Errorf("FuncName = %q, want f", c.FuncName)
|
|
}
|
|
cond := stmtByText(t, c, "a > 0")
|
|
if cond.Kind != "cond" {
|
|
t.Errorf("condition kind = %q, want cond", cond.Kind)
|
|
}
|
|
// The diamond: cond block branches true and false, both sides
|
|
// rejoin before the return.
|
|
trueTo, falseTo := -1, -1
|
|
for _, e := range c.Edges {
|
|
if e.From == cond.Block && e.Label == LabelTrue {
|
|
trueTo = e.To
|
|
}
|
|
if e.From == cond.Block && e.Label == LabelFalse {
|
|
falseTo = e.To
|
|
}
|
|
}
|
|
if trueTo < 0 || falseTo < 0 {
|
|
t.Fatalf("missing branch edges from cond block %d: %+v", cond.Block, c.Edges)
|
|
}
|
|
thenStmt := stmtByText(t, c, "x = 2")
|
|
elseStmt := stmtByText(t, c, "x = 3")
|
|
if thenStmt.Block != trueTo {
|
|
t.Errorf("then statement in block %d, want %d", thenStmt.Block, trueTo)
|
|
}
|
|
if elseStmt.Block != falseTo {
|
|
t.Errorf("else statement in block %d, want %d", elseStmt.Block, falseTo)
|
|
}
|
|
ret := stmtByText(t, c, "return x")
|
|
if ret.Block == thenStmt.Block || ret.Block == elseStmt.Block {
|
|
t.Errorf("return must live in the join block, not a branch arm")
|
|
}
|
|
if !edgeBetween(c, ret.Block, c.Exit, LabelReturn) {
|
|
t.Errorf("missing return edge to exit")
|
|
}
|
|
}
|
|
|
|
func TestBuildGoLoopBreakContinue(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(n int) int {
|
|
s := 0
|
|
for i := 0; i < n; i++ {
|
|
if i == 3 {
|
|
continue
|
|
}
|
|
if i == 7 {
|
|
break
|
|
}
|
|
s += i
|
|
}
|
|
return s
|
|
}
|
|
`, "go")
|
|
|
|
for _, want := range []EdgeLabel{LabelLoopBack, LabelBreak, LabelContinue, LabelTrue, LabelFalse} {
|
|
if !hasEdgeLabel(c, want) {
|
|
t.Errorf("missing %s edge; edges: %+v", want, c.Edges)
|
|
}
|
|
}
|
|
// continue must target the update block (i++ still runs), not
|
|
// skip it.
|
|
contStmt := stmtByText(t, c, "continue")
|
|
upd := stmtByText(t, c, "i++")
|
|
if !edgeBetween(c, contStmt.Block, upd.Block, LabelContinue) {
|
|
t.Errorf("continue should target the loop update block %d; edges: %+v", upd.Block, c.Edges)
|
|
}
|
|
// The update's defs: i (and a use of i).
|
|
if len(upd.Defs) != 1 || upd.Defs[0] != "i" {
|
|
t.Errorf("update defs = %v, want [i]", upd.Defs)
|
|
}
|
|
if len(upd.Uses) != 1 || upd.Uses[0] != "i" {
|
|
t.Errorf("update uses = %v, want [i]", upd.Uses)
|
|
}
|
|
}
|
|
|
|
func TestBuildGoSwitchFallthrough(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(x int) int {
|
|
y := 0
|
|
switch x {
|
|
case 1:
|
|
y = 1
|
|
fallthrough
|
|
case 2:
|
|
y = 2
|
|
default:
|
|
y = 3
|
|
}
|
|
return y
|
|
}
|
|
`, "go")
|
|
|
|
if !hasEdgeLabel(c, LabelCase) {
|
|
t.Fatalf("missing case edges")
|
|
}
|
|
// fallthrough: the block holding `y = 1` flows into the block
|
|
// holding `y = 2` sequentially.
|
|
s1 := stmtByText(t, c, "y = 1")
|
|
s2 := stmtByText(t, c, "y = 2")
|
|
if !edgeBetween(c, s1.Block, s2.Block, LabelSeq) {
|
|
t.Errorf("missing fallthrough seq edge %d->%d; edges: %+v", s1.Block, s2.Block, c.Edges)
|
|
}
|
|
// With a default case there must be no unmatched-subject edge.
|
|
cond := stmtByText(t, c, "x")
|
|
if cond.Kind != "cond" {
|
|
cond = c.Stmts[1]
|
|
}
|
|
for _, e := range c.Edges {
|
|
if e.From == cond.Block && e.Label == LabelFalse {
|
|
t.Errorf("switch with default must not emit a false edge")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildGoLabeledBreak(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f() int {
|
|
s := 0
|
|
outer:
|
|
for i := 0; i < 3; i++ {
|
|
for j := 0; j < 3; j++ {
|
|
if j == 2 {
|
|
break outer
|
|
}
|
|
s++
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
`, "go")
|
|
|
|
br := stmtByText(t, c, "break outer")
|
|
// The labeled break must exit the OUTER loop: its target block
|
|
// must be the block holding `return s` (outer loop_end flows
|
|
// there) — concretely, the break edge must not target the inner
|
|
// loop's end.
|
|
var breakTo = -1
|
|
for _, e := range c.Edges {
|
|
if e.From == br.Block && e.Label == LabelBreak {
|
|
breakTo = e.To
|
|
}
|
|
}
|
|
if breakTo < 0 {
|
|
t.Fatalf("no break edge from %d", br.Block)
|
|
}
|
|
ret := stmtByText(t, c, "return s")
|
|
// outer loop_end may be empty and flow to the return's block;
|
|
// accept either the return block itself or a block that reaches
|
|
// it via one seq hop.
|
|
ok := breakTo == ret.Block || edgeBetween(c, breakTo, ret.Block, LabelSeq)
|
|
if !ok {
|
|
t.Errorf("labeled break targets block %d, expected the outer loop end (return block %d)", breakTo, ret.Block)
|
|
}
|
|
}
|
|
|
|
func TestBuildGoDeferNoted(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f() {
|
|
defer cleanup()
|
|
work()
|
|
}
|
|
`, "go")
|
|
d := stmtByText(t, c, "defer cleanup()")
|
|
if d.Kind != "defer" {
|
|
t.Errorf("defer kind = %q, want defer", d.Kind)
|
|
}
|
|
w := stmtByText(t, c, "work()")
|
|
if w.Block != d.Block {
|
|
t.Errorf("defer must not split the basic block: defer in %d, work in %d", d.Block, w.Block)
|
|
}
|
|
}
|
|
|
|
func TestBuildGoInfiniteLoop(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f() {
|
|
for {
|
|
if done() {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
`, "go")
|
|
if !hasEdgeLabel(c, LabelLoopBack) || !hasEdgeLabel(c, LabelBreak) {
|
|
t.Fatalf("infinite loop needs loop_back and break edges: %+v", c.Edges)
|
|
}
|
|
}
|
|
|
|
// blockLabelByID returns a block's label.
|
|
func blockLabelByID(c *CFG, id int) string { return c.Blocks[id].Label }
|
|
|
|
// breakEdgeTarget returns the block a break statement's break edge
|
|
// targets, or -1.
|
|
func breakEdgeTarget(c *CFG, br *Statement) int {
|
|
for _, e := range c.Edges {
|
|
if e.From == br.Block && e.Label == LabelBreak {
|
|
return e.To
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// A `break` inside a Go select must exit the select, not the
|
|
// enclosing loop. With the fix the break targets the switch_end
|
|
// block; without it the break leaked to the loop / function exit and
|
|
// the statement after the select became unreachable.
|
|
func TestBuildGoSelectBreakExitsSelect(t *testing.T) {
|
|
c := mustBuild(t, `func f(ch chan int) int {
|
|
s := 0
|
|
for {
|
|
select {
|
|
case v := <-ch:
|
|
s += v
|
|
break
|
|
}
|
|
s++
|
|
}
|
|
return s
|
|
}`, "go")
|
|
br := stmtByText(t, c, "break")
|
|
to := breakEdgeTarget(c, br)
|
|
if to < 0 {
|
|
t.Fatalf("no break edge from block %d; edges: %+v", br.Block, c.Edges)
|
|
}
|
|
if got := blockLabelByID(c, to); got != "switch_end" {
|
|
t.Errorf("select break targets %q block %d, want the switch_end; edges: %+v", got, to, c.Edges)
|
|
}
|
|
// `s++` after the select must stay reachable from the select's
|
|
// merge point — the loop-carried `s` must reach the final use.
|
|
r := c.ReachingDefinitions()
|
|
inc := stmtByText(t, c, "s++")
|
|
chS := chainFor(t, r, inc.Index, "s")
|
|
if len(chS.Defs) == 0 {
|
|
t.Errorf("s++ must see a prior def of s: %v", chS.Defs)
|
|
}
|
|
}
|
|
|
|
// A labeled Go switch: `break L` must resolve to the switch end so
|
|
// the post-switch use sees the in-case definition. Before the fix the
|
|
// labeled break leaked to the function exit, so `s = 1` never reached
|
|
// `return s`.
|
|
func TestBuildGoLabeledSwitchBreak(t *testing.T) {
|
|
c := mustBuild(t, `func f(x int) int {
|
|
s := 0
|
|
L:
|
|
switch x {
|
|
case 1:
|
|
s = 1
|
|
break L
|
|
}
|
|
return s
|
|
}`, "go")
|
|
br := stmtByText(t, c, "break L")
|
|
to := breakEdgeTarget(c, br)
|
|
if to < 0 {
|
|
t.Fatalf("no break edge from block %d", br.Block)
|
|
}
|
|
if got := blockLabelByID(c, to); got != "switch_end" {
|
|
t.Errorf("labeled switch break targets %q, want switch_end; edges: %+v", got, c.Edges)
|
|
}
|
|
r := c.ReachingDefinitions()
|
|
ret := stmtByText(t, c, "return s")
|
|
ch := chainFor(t, r, ret.Index, "s")
|
|
d1 := stmtByText(t, c, "s = 1")
|
|
if !containsInt(ch.Defs, d1.Index) {
|
|
t.Errorf("in-case def `s = 1` must reach the return via the labeled break: %v", ch.Defs)
|
|
}
|
|
}
|
|
|
|
// A Go type switch `switch v := x.(type)` must define the alias v;
|
|
// every use of v inside the cases must chain back to it.
|
|
func TestBuildGoTypeSwitchAliasDefines(t *testing.T) {
|
|
c := mustBuild(t, `func f(x interface{}) int {
|
|
switch v := x.(type) {
|
|
case int:
|
|
return v
|
|
default:
|
|
return 0
|
|
}
|
|
}`, "go")
|
|
r := c.ReachingDefinitions()
|
|
use := stmtByText(t, c, "return v")
|
|
ch := chainFor(t, r, use.Index, "v")
|
|
if len(ch.Defs) == 0 {
|
|
t.Fatalf("type-switch alias v must produce a chain at its use: %v", ch.Defs)
|
|
}
|
|
def := c.Stmts[ch.Defs[0]]
|
|
if def.Kind != "cond" {
|
|
t.Errorf("v's def should be the type-switch cond statement, got kind %q (%q)", def.Kind, def.Text)
|
|
}
|
|
foundV := false
|
|
for _, d := range def.Defs {
|
|
if d == "v" {
|
|
foundV = true
|
|
}
|
|
}
|
|
if !foundV {
|
|
t.Errorf("type-switch cond must define v: %v", def.Defs)
|
|
}
|
|
// The switched expression x is a read, not a binding.
|
|
if containsString(def.Defs, "x") {
|
|
t.Errorf("the switched value x must not be a definition: %v", def.Defs)
|
|
}
|
|
}
|
|
|
|
func containsString(xs []string, x string) bool {
|
|
for _, v := range xs {
|
|
if v == x {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// reaching definitions — textbook shapes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Redefinition kills: the second assignment must be the only def
|
|
// reaching the final use.
|
|
func TestReachingRedefinitionKills(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f() int {
|
|
x := 1
|
|
x = 2
|
|
return x
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
def1 := stmtByText(t, c, "x := 1")
|
|
def2 := stmtByText(t, c, "x = 2")
|
|
ret := stmtByText(t, c, "return x")
|
|
ch := chainFor(t, r, ret.Index, "x")
|
|
if containsInt(ch.Defs, def1.Index) {
|
|
t.Errorf("killed definition %d still reaches the use: %v", def1.Index, ch.Defs)
|
|
}
|
|
if !containsInt(ch.Defs, def2.Index) {
|
|
t.Errorf("live definition %d does not reach the use: %v", def2.Index, ch.Defs)
|
|
}
|
|
}
|
|
|
|
// Branch merge unions: both arm definitions reach the post-join use.
|
|
func TestReachingBranchMergeUnion(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(a bool) int {
|
|
x := 0
|
|
if a {
|
|
x = 1
|
|
} else {
|
|
x = 2
|
|
}
|
|
return x
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
d1 := stmtByText(t, c, "x = 1")
|
|
d2 := stmtByText(t, c, "x = 2")
|
|
d0 := stmtByText(t, c, "x := 0")
|
|
ret := stmtByText(t, c, "return x")
|
|
ch := chainFor(t, r, ret.Index, "x")
|
|
if !containsInt(ch.Defs, d1.Index) || !containsInt(ch.Defs, d2.Index) {
|
|
t.Errorf("branch-arm defs must union at the join: %v (want %d and %d)", ch.Defs, d1.Index, d2.Index)
|
|
}
|
|
if containsInt(ch.Defs, d0.Index) {
|
|
t.Errorf("pre-branch def %d is killed on every path and must not reach: %v", d0.Index, ch.Defs)
|
|
}
|
|
}
|
|
|
|
// One-armed if: the initial def survives the merge alongside the arm
|
|
// def.
|
|
func TestReachingOneArmedIfKeepsBoth(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(a bool) int {
|
|
x := 0
|
|
if a {
|
|
x = 1
|
|
}
|
|
return x
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
d0 := stmtByText(t, c, "x := 0")
|
|
d1 := stmtByText(t, c, "x = 1")
|
|
ret := stmtByText(t, c, "return x")
|
|
ch := chainFor(t, r, ret.Index, "x")
|
|
if !containsInt(ch.Defs, d0.Index) || !containsInt(ch.Defs, d1.Index) {
|
|
t.Errorf("one-armed if must keep both defs at the join: %v", ch.Defs)
|
|
}
|
|
}
|
|
|
|
// Loop-carried defs: a def at the loop bottom reaches the use at the
|
|
// loop top on the next iteration.
|
|
func TestReachingLoopCarried(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(n int) int {
|
|
s := 0
|
|
for i := 0; i < n; i++ {
|
|
s = s + i
|
|
}
|
|
return s
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
d0 := stmtByText(t, c, "s := 0")
|
|
dLoop := stmtByText(t, c, "s = s + i")
|
|
// The loop body's use of s sees both the init def and its own
|
|
// previous-iteration def.
|
|
ch := chainFor(t, r, dLoop.Index, "s")
|
|
if !containsInt(ch.Defs, d0.Index) {
|
|
t.Errorf("init def must reach the first iteration: %v", ch.Defs)
|
|
}
|
|
if !containsInt(ch.Defs, dLoop.Index) {
|
|
t.Errorf("loop-carried def must reach the next iteration: %v", ch.Defs)
|
|
}
|
|
// The condition's use of i sees the init AND the increment.
|
|
cond := stmtByText(t, c, "i < n")
|
|
chI := chainFor(t, r, cond.Index, "i")
|
|
init := stmtByText(t, c, "i := 0")
|
|
inc := stmtByText(t, c, "i++")
|
|
if !containsInt(chI.Defs, init.Index) || !containsInt(chI.Defs, inc.Index) {
|
|
t.Errorf("loop condition must see init and increment defs of i: %v", chI.Defs)
|
|
}
|
|
// And the final use of s unions init + loop defs.
|
|
ret := stmtByText(t, c, "return s")
|
|
chRet := chainFor(t, r, ret.Index, "s")
|
|
if !containsInt(chRet.Defs, d0.Index) || !containsInt(chRet.Defs, dLoop.Index) {
|
|
t.Errorf("post-loop use must union zero-trip and loop defs: %v", chRet.Defs)
|
|
}
|
|
}
|
|
|
|
// Parameters are block-0 definitions reaching every unshadowed use.
|
|
func TestReachingParamsReachUses(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(a int, b int) int {
|
|
x := a + b
|
|
return x
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
assign := stmtByText(t, c, "x := a + b")
|
|
chA := chainFor(t, r, assign.Index, "a")
|
|
if len(chA.Defs) != 1 {
|
|
t.Fatalf("param a should have exactly one def: %v", chA.Defs)
|
|
}
|
|
paramStmt := c.Stmts[chA.Defs[0]]
|
|
if paramStmt.Kind != "param" {
|
|
t.Errorf("a's def should be the synthetic param statement, got kind %q", paramStmt.Kind)
|
|
}
|
|
if paramStmt.Block != c.Entry {
|
|
t.Errorf("param defs live in the entry block %d, got %d", c.Entry, paramStmt.Block)
|
|
}
|
|
}
|
|
|
|
// A use with no defining statement (package global) produces no chain.
|
|
func TestReachingGlobalsHaveNoChain(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f() int {
|
|
return globalCounter
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
ret := stmtByText(t, c, "return globalCounter")
|
|
if hasChain(r, ret.Index, "globalCounter") {
|
|
t.Errorf("global reads must not produce chains")
|
|
}
|
|
}
|
|
|
|
// Early return prunes: a def after the return in the same branch
|
|
// can't reach uses before it.
|
|
func TestReachingEarlyReturnIsolation(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(a bool) int {
|
|
x := 1
|
|
if a {
|
|
return x
|
|
}
|
|
x = 2
|
|
return x
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
d1 := stmtByText(t, c, "x := 1")
|
|
d2 := stmtByText(t, c, "x = 2")
|
|
stmts := []*Statement{}
|
|
for _, st := range c.Stmts {
|
|
if strings.Contains(st.Text, "return x") {
|
|
stmts = append(stmts, st)
|
|
}
|
|
}
|
|
if len(stmts) != 2 {
|
|
t.Fatalf("want two return statements, got %d", len(stmts))
|
|
}
|
|
early, late := stmts[0], stmts[1]
|
|
chEarly := chainFor(t, r, early.Index, "x")
|
|
if containsInt(chEarly.Defs, d2.Index) {
|
|
t.Errorf("def after the early return must not reach it: %v", chEarly.Defs)
|
|
}
|
|
if !containsInt(chEarly.Defs, d1.Index) {
|
|
t.Errorf("initial def must reach the early return: %v", chEarly.Defs)
|
|
}
|
|
chLate := chainFor(t, r, late.Index, "x")
|
|
if !containsInt(chLate.Defs, d2.Index) || containsInt(chLate.Defs, d1.Index) {
|
|
t.Errorf("late return sees only the redefinition: %v", chLate.Defs)
|
|
}
|
|
}
|
|
|
|
// Statement granularity: two uses of the same variable in different
|
|
// statements get independent chains.
|
|
func TestReachingStatementGranularity(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f() int {
|
|
x := 1
|
|
y := x
|
|
x = 2
|
|
z := x
|
|
return y + z
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
d1 := stmtByText(t, c, "x := 1")
|
|
d2 := stmtByText(t, c, "x = 2")
|
|
useY := stmtByText(t, c, "y := x")
|
|
useZ := stmtByText(t, c, "z := x")
|
|
chY := chainFor(t, r, useY.Index, "x")
|
|
chZ := chainFor(t, r, useZ.Index, "x")
|
|
if !containsInt(chY.Defs, d1.Index) || containsInt(chY.Defs, d2.Index) {
|
|
t.Errorf("y := x must see only the first def: %v", chY.Defs)
|
|
}
|
|
if !containsInt(chZ.Defs, d2.Index) || containsInt(chZ.Defs, d1.Index) {
|
|
t.Errorf("z := x must see only the second def: %v", chZ.Defs)
|
|
}
|
|
}
|
|
|
|
// Closures are opaque: assignments inside a func literal don't
|
|
// define in the enclosing frame.
|
|
func TestClosureOpaque(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f() int {
|
|
x := 1
|
|
g := func() {
|
|
x = 99
|
|
}
|
|
g()
|
|
return x
|
|
}
|
|
`, "go")
|
|
r := c.ReachingDefinitions()
|
|
ret := stmtByText(t, c, "return x")
|
|
ch := chainFor(t, r, ret.Index, "x")
|
|
d1 := stmtByText(t, c, "x := 1")
|
|
if len(ch.Defs) != 1 || ch.Defs[0] != d1.Index {
|
|
t.Errorf("closure write must not count as an enclosing-scope def: %v", ch.Defs)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// options / rendering
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestLineOffsetShiftsLines(t *testing.T) {
|
|
src := `func f() int {
|
|
x := 1
|
|
return x
|
|
}`
|
|
c, err := Build([]byte(src), "go", Options{LineOffset: 99})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st := stmtByText(t, c, "x := 1")
|
|
if st.StartLine != 101 {
|
|
t.Errorf("StartLine = %d, want 101 (snippet line 2 + offset 99)", st.StartLine)
|
|
}
|
|
}
|
|
|
|
func TestMermaidRendering(t *testing.T) {
|
|
c := mustBuild(t, `
|
|
func f(a int) int {
|
|
if a > 0 {
|
|
return 1
|
|
}
|
|
return 2
|
|
}
|
|
`, "go")
|
|
m := c.Mermaid()
|
|
if !strings.HasPrefix(m, "flowchart TD") {
|
|
t.Errorf("mermaid must start with flowchart TD: %q", m)
|
|
}
|
|
if !strings.Contains(m, "-->|true|") || !strings.Contains(m, "-->|false|") {
|
|
t.Errorf("mermaid must label branch edges: %s", m)
|
|
}
|
|
if !strings.Contains(m, "entry") || !strings.Contains(m, "exit") {
|
|
t.Errorf("mermaid must render entry/exit: %s", m)
|
|
}
|
|
}
|
|
|
|
func TestUnsupportedLanguage(t *testing.T) {
|
|
if _, err := Build([]byte("x"), "cobol", Options{}); err == nil {
|
|
t.Fatal("expected error for unsupported language")
|
|
}
|
|
if SupportedLanguage("cobol") {
|
|
t.Fatal("cobol must not be supported")
|
|
}
|
|
if !SupportedLanguage("go") || !SupportedLanguage("ruby") {
|
|
t.Fatal("go and ruby must be supported")
|
|
}
|
|
}
|
|
|
|
func TestNoFunctionInSource(t *testing.T) {
|
|
if _, err := Build([]byte("var x = 1"), "go", Options{}); err == nil {
|
|
t.Fatal("expected error when source holds no function")
|
|
}
|
|
}
|