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