diff --git a/go/analysis/passes/ctrlflow/ctrlflow.go b/go/analysis/passes/ctrlflow/ctrlflow.go index 51600ffc7e..49eb50fb12 100644 --- a/go/analysis/passes/ctrlflow/ctrlflow.go +++ b/go/analysis/passes/ctrlflow/ctrlflow.go @@ -16,9 +16,11 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/cfg" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typeparams" ) var Analyzer = &analysis.Analyzer{ @@ -187,8 +189,12 @@ func (c *CFGs) callMayReturn(call *ast.CallExpr) (r bool) { return false // panic never returns } - // Is this a static call? - fn := typeutil.StaticCallee(c.pass.TypesInfo, call) + // Is this a static call? Also includes static functions + // parameterized by a type. Such functions may or may not + // return depending on the parameter type, but in some + // cases the answer is definite. We let ctrlflow figure + // that out. + fn := staticCallee(c.pass.TypesInfo, call) if fn == nil { return true // callee not statically known; be conservative } @@ -204,6 +210,50 @@ func (c *CFGs) callMayReturn(call *ast.CallExpr) (r bool) { return !c.pass.ImportObjectFact(fn, new(noReturn)) } +// staticCallee returns static function, if any, called by call. +// Effectivelly reduces to typeutil.StaticCallee. In addition, +// returns static function parameterized by a type, if any. +// +// TODO(zpavlinovic): can this be replaced by typeutil.StaticCallee +// in the future? +func staticCallee(info *types.Info, call *ast.CallExpr) *types.Func { + if fn := typeutil.StaticCallee(info, call); fn != nil { + return fn + } + return staticTypeParamCallee(info, call) +} + +// staticTypeParamCallee returns the static function in call, if any, +// expected to be parameterized by a type. +func staticTypeParamCallee(info *types.Info, call *ast.CallExpr) *types.Func { + ix := typeparams.GetIndexExprData(astutil.Unparen(call.Fun)) + if ix == nil { + return nil + } + + var obj types.Object + switch fun := ix.X.(type) { + case *ast.Ident: + obj = info.Uses[fun] // type, var, builtin, or declared func + case *ast.SelectorExpr: + if sel, ok := info.Selections[fun]; ok { + obj = sel.Obj() // method or field + } else { + obj = info.Uses[fun.Sel] // qualified identifier? + } + } + + if f, ok := obj.(*types.Func); ok && !interfaceMethod(f) { + return f + } + return nil +} + +func interfaceMethod(f *types.Func) bool { + recv := f.Type().(*types.Signature).Recv() + return recv != nil && types.IsInterface(recv.Type()) +} + var panicBuiltin = types.Universe.Lookup("panic").(*types.Builtin) func hasReachableReturn(g *cfg.CFG) bool { diff --git a/go/analysis/passes/ctrlflow/ctrlflow_test.go b/go/analysis/passes/ctrlflow/ctrlflow_test.go index 0aae7cb0ae..1503c3376c 100644 --- a/go/analysis/passes/ctrlflow/ctrlflow_test.go +++ b/go/analysis/passes/ctrlflow/ctrlflow_test.go @@ -10,13 +10,19 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/ctrlflow" + "golang.org/x/tools/internal/typeparams" ) func Test(t *testing.T) { testdata := analysistest.TestData() // load testdata/src/a/a.go - results := analysistest.Run(t, testdata, ctrlflow.Analyzer, "a") + tests := []string{"a"} + if typeparams.Enabled { + // and testdata/src/typeparams/typeparams.go when possible + tests = append(tests, "typeparams") + } + results := analysistest.Run(t, testdata, ctrlflow.Analyzer, tests...) // Perform a minimal smoke test on // the result (CFG) computed by ctrlflow. diff --git a/go/analysis/passes/ctrlflow/testdata/src/a/a.go b/go/analysis/passes/ctrlflow/testdata/src/a/a.go index a65bd74815..d2a7aec9c9 100644 --- a/go/analysis/passes/ctrlflow/testdata/src/a/a.go +++ b/go/analysis/passes/ctrlflow/testdata/src/a/a.go @@ -1,3 +1,7 @@ +// Copyright 2021 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 a // This file tests facts produced by ctrlflow. diff --git a/go/analysis/passes/ctrlflow/testdata/src/lib/lib.go b/go/analysis/passes/ctrlflow/testdata/src/lib/lib.go index c0bf7dff48..41afcc1211 100644 --- a/go/analysis/passes/ctrlflow/testdata/src/lib/lib.go +++ b/go/analysis/passes/ctrlflow/testdata/src/lib/lib.go @@ -1,3 +1,7 @@ +// Copyright 2021 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 lib func CanReturn() {} diff --git a/go/analysis/passes/ctrlflow/testdata/src/typeparams/typeparams.go b/go/analysis/passes/ctrlflow/testdata/src/typeparams/typeparams.go new file mode 100644 index 0000000000..122689199a --- /dev/null +++ b/go/analysis/passes/ctrlflow/testdata/src/typeparams/typeparams.go @@ -0,0 +1,64 @@ +// Copyright 2021 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 a + +// This file tests facts produced by ctrlflow. + +var cond bool + +var funcs = []func(){func() {}} + +func a[A any]() { // want a:"noReturn" + if cond { + funcs[0]() + b[A]() + } else { + for { + } + } +} + +func b[B any]() { // want b:"noReturn" + select {} +} + +func c[A, B any]() { // want c:"noReturn" + if cond { + a[A]() + } else { + d[A, B]() + } +} + +func d[A, B any]() { // want d:"noReturn" + b[B]() +} + +type I[T any] interface { + Id(T) T +} + +func e[T any](i I[T], t T) T { + return i.Id(t) +} + +func k[T any](i I[T], t T) T { // want k:"noReturn" + b[T]() + return i.Id(t) +} + +type T[X any] int + +func (T[X]) method1() { // want method1:"noReturn" + a[X]() +} + +func (T[X]) method2() { // (may return) + if cond { + a[X]() + } else { + funcs[0]() + } +}