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  }