src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }