sa1015.go raw

   1  package sa1015
   2  
   3  import (
   4  	"go/token"
   5  	"go/version"
   6  
   7  	"honnef.co/go/tools/analysis/code"
   8  	"honnef.co/go/tools/analysis/lint"
   9  	"honnef.co/go/tools/analysis/report"
  10  	"honnef.co/go/tools/go/ir"
  11  	"honnef.co/go/tools/go/ir/irutil"
  12  	"honnef.co/go/tools/internal/passes/buildir"
  13  
  14  	"golang.org/x/tools/go/analysis"
  15  )
  16  
  17  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
  18  	Analyzer: &analysis.Analyzer{
  19  		Name:     "SA1015",
  20  		Run:      run,
  21  		Requires: []*analysis.Analyzer{buildir.Analyzer},
  22  	},
  23  	Doc: &lint.RawDocumentation{
  24  		Title: `Using \'time.Tick\' in a way that will leak. Consider using \'time.NewTicker\', and only use \'time.Tick\' in tests, commands and endless functions`,
  25  
  26  		Text: `Before Go 1.23, \'time.Ticker\'s had to be closed to be able to be garbage
  27  collected. Since \'time.Tick\' doesn't make it possible to close the underlying
  28  ticker, using it repeatedly would leak memory.
  29  
  30  Go 1.23 fixes this by allowing tickers to be collected even if they weren't closed.`,
  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  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
  41  		if fn.Pos() == token.NoPos || version.Compare(code.StdlibVersion(pass, fn), "go1.23") >= 0 {
  42  			// Beginning with Go 1.23, the GC is able to collect unreferenced, unclosed
  43  			// tickers, which makes time.Tick safe(r) to use.
  44  			//
  45  			// When we don't have a valid position, we err on the side of false negatives.
  46  			// This shouldn't actually lead to any false negatives, as no functions
  47  			// without valid positions (such as the synthesized init function) should be
  48  			// able to use time.Tick.
  49  			continue
  50  		}
  51  
  52  		if code.IsMainLike(pass) || code.IsInTest(pass, fn) {
  53  			continue
  54  		}
  55  		for _, block := range fn.Blocks {
  56  			for _, ins := range block.Instrs {
  57  				call, ok := ins.(*ir.Call)
  58  				if !ok || !irutil.IsCallTo(call.Common(), "time.Tick") {
  59  					continue
  60  				}
  61  				if !irutil.Terminates(call.Parent()) {
  62  					continue
  63  				}
  64  				report.Report(pass, call, "using time.Tick leaks the underlying ticker, consider using it only in endless functions, tests and the main package, and use time.NewTicker here")
  65  			}
  66  		}
  67  	}
  68  	return nil, nil
  69  }
  70