-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
go/analysis/passes/ctrlflow: an Analyzer that builds CFGs
The ctrlflow Analyzer builds a control-flow graph (see golang.org/x/tools/go/cfg) for each named and unnamed function in the package. It computes for each function whether it can never return, either because the function is an intrinsic that stops the thread (e.g. os.Exit), or because control never reaches a return statement, or because the function inevitably calls another function that never returns. For each such function it exports a noReturn fact. This change also: - adds 'inspect', another Analyzer that builds an optimized AST traversal table for use by nearly every other Analyzer. - changes analysistest.Run to return the analysis result to enable further testing. (This required changing it to analyze one package at a time, which is no less efficient, and is the typical case.) Change-Id: I877e2b2363a365a9976aa9c2719ad3fba4df2634 Reviewed-on: https://go-review.googlesource.com/c/139478 Reviewed-by: Michael Matloob <matloob@golang.org>
- Loading branch information
Showing
8 changed files
with
424 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
// Copyright 2018 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 ctrlflow is an analysis that provides a syntactic | ||
// control-flow graph (CFG) for the body of a function. | ||
// It records whether a function cannot return. | ||
// By itself, it does not report any diagnostics. | ||
package ctrlflow | ||
|
||
import ( | ||
"go/ast" | ||
"go/types" | ||
"log" | ||
"reflect" | ||
|
||
"golang.org/x/tools/go/analysis" | ||
"golang.org/x/tools/go/analysis/passes/inspect" | ||
"golang.org/x/tools/go/ast/inspector" | ||
"golang.org/x/tools/go/cfg" | ||
"golang.org/x/tools/go/types/typeutil" | ||
) | ||
|
||
var Analyzer = &analysis.Analyzer{ | ||
Name: "ctrlflow", | ||
Doc: "build a control-flow graph", | ||
Run: run, | ||
ResultType: reflect.TypeOf(new(CFGs)), | ||
FactTypes: []analysis.Fact{new(noReturn)}, | ||
Requires: []*analysis.Analyzer{inspect.Analyzer}, | ||
} | ||
|
||
// noReturn is a fact indicating that a function does not return. | ||
type noReturn struct{} | ||
|
||
func (*noReturn) AFact() {} | ||
|
||
func (*noReturn) String() string { return "noReturn" } | ||
|
||
// A CFGs holds the control-flow graphs | ||
// for all the functions of the current package. | ||
type CFGs struct { | ||
defs map[*ast.Ident]types.Object // from Pass.TypesInfo.Defs | ||
funcDecls map[*types.Func]*declInfo | ||
funcLits map[*ast.FuncLit]*litInfo | ||
pass *analysis.Pass // transient; nil after construction | ||
} | ||
|
||
// CFGs has two maps: funcDecls for named functions and funcLits for | ||
// unnamed ones. Unlike funcLits, the funcDecls map is not keyed by its | ||
// syntax node, *ast.FuncDecl, because callMayReturn needs to do a | ||
// look-up by *types.Func, and you can get from an *ast.FuncDecl to a | ||
// *types.Func but not the other way. | ||
|
||
type declInfo struct { | ||
decl *ast.FuncDecl | ||
cfg *cfg.CFG // iff decl.Body != nil | ||
started bool // to break cycles | ||
noReturn bool | ||
} | ||
|
||
type litInfo struct { | ||
cfg *cfg.CFG | ||
noReturn bool | ||
} | ||
|
||
// FuncDecl returns the control-flow graph for a named function. | ||
// It returns nil if decl.Body==nil. | ||
func (c *CFGs) FuncDecl(decl *ast.FuncDecl) *cfg.CFG { | ||
if decl.Body == nil { | ||
return nil | ||
} | ||
fn := c.defs[decl.Name].(*types.Func) | ||
return c.funcDecls[fn].cfg | ||
} | ||
|
||
// FuncLit returns the control-flow graph for a literal function. | ||
func (c *CFGs) FuncLit(lit *ast.FuncLit) *cfg.CFG { | ||
return c.funcLits[lit].cfg | ||
} | ||
|
||
func run(pass *analysis.Pass) (interface{}, error) { | ||
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) | ||
|
||
// Because CFG construction consumes and produces noReturn | ||
// facts, CFGs for exported FuncDecls must be built before 'run' | ||
// returns; we cannot construct them lazily. | ||
// (We could build CFGs for FuncLits lazily, | ||
// but the benefit is marginal.) | ||
|
||
// Pass 1. Map types.Funcs to ast.FuncDecls in this package. | ||
funcDecls := make(map[*types.Func]*declInfo) // functions and methods | ||
funcLits := make(map[*ast.FuncLit]*litInfo) | ||
|
||
var decls []*types.Func // keys(funcDecls), in order | ||
var lits []*ast.FuncLit // keys(funcLits), in order | ||
|
||
nodeFilter := []ast.Node{ | ||
(*ast.FuncDecl)(nil), | ||
(*ast.FuncLit)(nil), | ||
} | ||
inspect.Preorder(nodeFilter, func(n ast.Node) { | ||
switch n := n.(type) { | ||
case *ast.FuncDecl: | ||
fn := pass.TypesInfo.Defs[n.Name].(*types.Func) | ||
funcDecls[fn] = &declInfo{decl: n} | ||
decls = append(decls, fn) | ||
|
||
case *ast.FuncLit: | ||
funcLits[n] = new(litInfo) | ||
lits = append(lits, n) | ||
} | ||
}) | ||
|
||
c := &CFGs{ | ||
defs: pass.TypesInfo.Defs, | ||
funcDecls: funcDecls, | ||
funcLits: funcLits, | ||
pass: pass, | ||
} | ||
|
||
// Pass 2. Build CFGs. | ||
|
||
// Build CFGs for named functions. | ||
// Cycles in the static call graph are broken | ||
// arbitrarily but deterministically. | ||
// We create noReturn facts as discovered. | ||
for _, fn := range decls { | ||
c.buildDecl(fn, funcDecls[fn]) | ||
} | ||
|
||
// Build CFGs for literal functions. | ||
// These aren't relevant to facts (since they aren't named) | ||
// but are required for the CFGs.FuncLit API. | ||
for _, lit := range lits { | ||
li := funcLits[lit] | ||
if li.cfg == nil { | ||
li.cfg = cfg.New(lit.Body, c.callMayReturn) | ||
if !hasReachableReturn(li.cfg) { | ||
li.noReturn = true | ||
} | ||
} | ||
} | ||
|
||
// All CFGs are now built. | ||
c.pass = nil | ||
|
||
return c, nil | ||
} | ||
|
||
// di.cfg may be nil on return. | ||
func (c *CFGs) buildDecl(fn *types.Func, di *declInfo) { | ||
// buildDecl may call itself recursively for the same function, | ||
// because cfg.New is passed the callMayReturn method, which | ||
// builds the CFG of the callee, leading to recursion. | ||
// The buildDecl call tree thus resembles the static call graph. | ||
// We mark each node when we start working on it to break cycles. | ||
|
||
if !di.started { // break cycle | ||
di.started = true | ||
|
||
if isIntrinsicNoReturn(fn) { | ||
di.noReturn = true | ||
} | ||
if di.decl.Body != nil { | ||
di.cfg = cfg.New(di.decl.Body, c.callMayReturn) | ||
if !hasReachableReturn(di.cfg) { | ||
di.noReturn = true | ||
} | ||
} | ||
if di.noReturn { | ||
c.pass.ExportObjectFact(fn, new(noReturn)) | ||
} | ||
|
||
// debugging | ||
if false { | ||
log.Printf("CFG for %s:\n%s (noreturn=%t)\n", fn, di.cfg.Format(c.pass.Fset), di.noReturn) | ||
} | ||
} | ||
} | ||
|
||
// callMayReturn reports whether the called function may return. | ||
// It is passed to the CFG builder. | ||
func (c *CFGs) callMayReturn(call *ast.CallExpr) (r bool) { | ||
if id, ok := call.Fun.(*ast.Ident); ok && c.pass.TypesInfo.Uses[id] == panicBuiltin { | ||
return false // panic never returns | ||
} | ||
|
||
// Is this a static call? | ||
fn := typeutil.StaticCallee(c.pass.TypesInfo, call) | ||
if fn == nil { | ||
return true // callee not statically known; be conservative | ||
} | ||
|
||
// Function or method declared in this package? | ||
if di, ok := c.funcDecls[fn]; ok { | ||
c.buildDecl(fn, di) | ||
return !di.noReturn | ||
} | ||
|
||
// Not declared in this package. | ||
// Is there a fact from another package? | ||
return !c.pass.ImportObjectFact(fn, new(noReturn)) | ||
} | ||
|
||
var panicBuiltin = types.Universe.Lookup("panic").(*types.Builtin) | ||
|
||
func hasReachableReturn(g *cfg.CFG) bool { | ||
for _, b := range g.Blocks { | ||
if b.Live && b.Return() != nil { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// isIntrinsicNoReturn reports whether a function intrinsically never | ||
// returns because it stops execution of the calling thread. | ||
// It is the base case in the recursion. | ||
func isIntrinsicNoReturn(fn *types.Func) bool { | ||
// Add functions here as the need arises, but don't allocate memory. | ||
path, name := fn.Pkg().Path(), fn.Name() | ||
return path == "syscall" && (name == "Exit" || name == "ExitProcess" || name == "ExitThread") || | ||
path == "runtime" && name == "Goexit" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package ctrlflow_test | ||
|
||
import ( | ||
"go/ast" | ||
"testing" | ||
|
||
"golang.org/x/tools/go/analysis/analysistest" | ||
"golang.org/x/tools/go/analysis/passes/ctrlflow" | ||
) | ||
|
||
func Test(t *testing.T) { | ||
testdata := analysistest.TestData() | ||
|
||
// load testdata/src/a/a.go | ||
pass, result := analysistest.Run(t, testdata, ctrlflow.Analyzer, "a") | ||
|
||
// Perform a minimal smoke test on | ||
// the result (CFG) computed by ctrlflow. | ||
if result != nil { | ||
cfgs := result.(*ctrlflow.CFGs) | ||
|
||
for _, decl := range pass.Files[0].Decls { | ||
if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body != nil { | ||
if cfgs.FuncDecl(decl) == nil { | ||
t.Errorf("%s: no CFG for func %s", | ||
pass.Fset.Position(decl.Pos()), decl.Name.Name) | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.