sa3000.go raw
1 package sa3000
2
3 import (
4 "go/ast"
5 "go/types"
6 "go/version"
7
8 "honnef.co/go/tools/analysis/code"
9 "honnef.co/go/tools/analysis/lint"
10 "honnef.co/go/tools/analysis/report"
11
12 "golang.org/x/tools/go/analysis"
13 "golang.org/x/tools/go/analysis/passes/inspect"
14 "golang.org/x/tools/go/ast/inspector"
15 )
16
17 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
18 Analyzer: &analysis.Analyzer{
19 Name: "SA3000",
20 Run: run,
21 Requires: []*analysis.Analyzer{inspect.Analyzer},
22 },
23 Doc: &lint.RawDocumentation{
24 Title: `\'TestMain\' doesn't call \'os.Exit\', hiding test failures`,
25 Text: `Test executables (and in turn \"go test\") exit with a non-zero status
26 code if any tests failed. When specifying your own \'TestMain\' function,
27 it is your responsibility to arrange for this, by calling \'os.Exit\' with
28 the correct code. The correct code is returned by \'(*testing.M).Run\', so
29 the usual way of implementing \'TestMain\' is to end it with
30 \'os.Exit(m.Run())\'.`,
31 Since: "2017.1",
32 Severity: lint.SeverityWarning,
33 MergeIf: lint.MergeIfAny,
34 },
35 })
36
37 var Analyzer = SCAnalyzer.Analyzer
38
39 func run(pass *analysis.Pass) (interface{}, error) {
40 var (
41 fnmain ast.Node
42 callsExit bool
43 callsRun bool
44 arg types.Object
45 )
46 fn := func(node ast.Node, push bool) bool {
47 if !push {
48 if fnmain != nil && node == fnmain {
49 if !callsExit && callsRun {
50 report.Report(pass, fnmain, "TestMain should call os.Exit to set exit code")
51 }
52 fnmain = nil
53 callsExit = false
54 callsRun = false
55 arg = nil
56 }
57 return true
58 }
59
60 switch node := node.(type) {
61 case *ast.FuncDecl:
62 if fnmain != nil {
63 return true
64 }
65 if !isTestMain(pass, node) {
66 return false
67 }
68 if version.Compare(code.StdlibVersion(pass, node), "go1.15") >= 0 {
69 // Beginning with Go 1.15, the test framework will call
70 // os.Exit for us.
71 return false
72 }
73 fnmain = node
74 arg = pass.TypesInfo.ObjectOf(node.Type.Params.List[0].Names[0])
75 return true
76 case *ast.CallExpr:
77 if code.IsCallTo(pass, node, "os.Exit") {
78 callsExit = true
79 return false
80 }
81 sel, ok := node.Fun.(*ast.SelectorExpr)
82 if !ok {
83 return true
84 }
85 ident, ok := sel.X.(*ast.Ident)
86 if !ok {
87 return true
88 }
89 if arg != pass.TypesInfo.ObjectOf(ident) {
90 return true
91 }
92 if sel.Sel.Name == "Run" {
93 callsRun = true
94 return false
95 }
96 return true
97 default:
98 lint.ExhaustiveTypeSwitch(node)
99 return true
100 }
101 }
102 pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.FuncDecl)(nil), (*ast.CallExpr)(nil)}, fn)
103 return nil, nil
104 }
105
106 func isTestMain(pass *analysis.Pass, decl *ast.FuncDecl) bool {
107 if decl.Name.Name != "TestMain" {
108 return false
109 }
110 if len(decl.Type.Params.List) != 1 {
111 return false
112 }
113 arg := decl.Type.Params.List[0]
114 if len(arg.Names) != 1 {
115 return false
116 }
117 return code.IsOfPointerToTypeWithName(pass, arg.Type, "testing.M")
118 }
119