sa3000.go (2917B)
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) (any, 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 }