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