blob: 536d2fe5df793da184e9d0bb84f5095284fdc4e6 [file] [log] [blame]
// 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))
}