| // Copyright 2016 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cfg_test |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/format" |
| "go/parser" |
| "go/token" |
| "testing" |
| |
| "golang.org/x/tools/go/cfg" |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/internal/testenv" |
| ) |
| |
| const src = `package main |
| |
| import "log" |
| |
| func f1() { |
| live() |
| return |
| dead() |
| } |
| |
| func f2() { |
| for { |
| live() |
| } |
| dead() |
| } |
| |
| func f3() { |
| if true { // even known values are ignored |
| return |
| } |
| for true { // even known values are ignored |
| live() |
| } |
| for { |
| live() |
| } |
| dead() |
| } |
| |
| func f4(x int) { |
| switch x { |
| case 1: |
| live() |
| fallthrough |
| case 2: |
| live() |
| log.Fatal() |
| default: |
| panic("oops") |
| } |
| dead() |
| } |
| |
| func f4(ch chan int) { |
| select { |
| case <-ch: |
| live() |
| return |
| default: |
| live() |
| panic("oops") |
| } |
| dead() |
| } |
| |
| func f5(unknown bool) { |
| for { |
| if unknown { |
| break |
| } |
| continue |
| dead() |
| } |
| live() |
| } |
| |
| func f6(unknown bool) { |
| outer: |
| for { |
| for { |
| break outer |
| dead() |
| } |
| dead() |
| } |
| live() |
| } |
| |
| func f7() { |
| for { |
| break nosuchlabel |
| dead() |
| } |
| dead() |
| } |
| |
| func f8() { |
| select{} |
| dead() |
| } |
| |
| func f9(ch chan int) { |
| select { |
| case <-ch: |
| return |
| } |
| dead() |
| } |
| |
| func f10(ch chan int) { |
| select { |
| case <-ch: |
| return |
| dead() |
| default: |
| } |
| live() |
| } |
| |
| func f11() { |
| goto; // mustn't crash |
| dead() |
| } |
| |
| ` |
| |
| func TestDeadCode(t *testing.T) { |
| // We'll use dead code detection to verify the CFG. |
| |
| fset := token.NewFileSet() |
| f, err := parser.ParseFile(fset, "dummy.go", src, parser.Mode(0)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| for _, decl := range f.Decls { |
| if decl, ok := decl.(*ast.FuncDecl); ok { |
| g := cfg.New(decl.Body, mayReturn) |
| |
| // Print statements in unreachable blocks |
| // (in order determined by builder). |
| var buf bytes.Buffer |
| for _, b := range g.Blocks { |
| if !b.Live { |
| for _, n := range b.Nodes { |
| fmt.Fprintf(&buf, "\t%s\n", formatNode(fset, n)) |
| } |
| } |
| } |
| |
| // Check that the result contains "dead" at least once but not "live". |
| if !bytes.Contains(buf.Bytes(), []byte("dead")) || |
| bytes.Contains(buf.Bytes(), []byte("live")) { |
| t.Errorf("unexpected dead statements in function %s:\n%s", |
| decl.Name.Name, |
| &buf) |
| t.Logf("control flow graph:\n%s", g.Format(fset)) |
| } |
| } |
| } |
| } |
| |
| // TestSmoke runs the CFG builder on every FuncDecl in the standard |
| // library and x/tools. (This is all well-typed code, but it gives |
| // some coverage.) |
| func TestSmoke(t *testing.T) { |
| if testing.Short() { |
| t.Skip("skipping in short mode") |
| } |
| testenv.NeedsTool(t, "go") |
| |
| // The Mode API is just hateful. |
| // https://github.com/golang/go/issues/48226#issuecomment-1948792315 |
| mode := packages.NeedDeps | packages.NeedImports | packages.NeedSyntax | packages.NeedTypes |
| pkgs, err := packages.Load(&packages.Config{Mode: mode}, "std", "golang.org/x/tools/...") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| for _, pkg := range pkgs { |
| for _, file := range pkg.Syntax { |
| for _, decl := range file.Decls { |
| if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body != nil { |
| g := cfg.New(decl.Body, mayReturn) |
| |
| // Run a few quick sanity checks. |
| failed := false |
| for i, b := range g.Blocks { |
| errorf := func(format string, args ...any) { |
| if !failed { |
| t.Errorf("%s\n%s", pkg.Fset.Position(decl.Pos()), g.Format(pkg.Fset)) |
| failed = true |
| } |
| msg := fmt.Sprintf(format, args...) |
| t.Errorf("block %d: %s", i, msg) |
| } |
| |
| if b.Kind == cfg.KindInvalid { |
| errorf("invalid Block.Kind %v", b.Kind) |
| } |
| if b.Stmt == nil && b.Kind != cfg.KindLabel { |
| errorf("nil Block.Stmt (Kind=%v)", b.Kind) |
| } |
| if i != int(b.Index) { |
| errorf("invalid Block.Index") |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // A trivial mayReturn predicate that looks only at syntax, not types. |
| func mayReturn(call *ast.CallExpr) bool { |
| switch fun := call.Fun.(type) { |
| case *ast.Ident: |
| return fun.Name != "panic" |
| case *ast.SelectorExpr: |
| return fun.Sel.Name != "Fatal" |
| } |
| return true |
| } |
| |
| func formatNode(fset *token.FileSet, n ast.Node) string { |
| var buf bytes.Buffer |
| format.Node(&buf, fset, n) |
| // Indent secondary lines by a tab. |
| return string(bytes.Replace(buf.Bytes(), []byte("\n"), []byte("\n\t"), -1)) |
| } |