github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/simple/s1009/s1009.go (about)

     1  package s1009
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/constant"
     7  	"go/token"
     8  	"go/types"
     9  
    10  	"github.com/amarpal/go-tools/analysis/code"
    11  	"github.com/amarpal/go-tools/analysis/facts/generated"
    12  	"github.com/amarpal/go-tools/analysis/lint"
    13  	"github.com/amarpal/go-tools/analysis/report"
    14  	"github.com/amarpal/go-tools/go/types/typeutil"
    15  	"github.com/amarpal/go-tools/knowledge"
    16  
    17  	"golang.org/x/tools/go/analysis"
    18  	"golang.org/x/tools/go/analysis/passes/inspect"
    19  )
    20  
    21  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    22  	Analyzer: &analysis.Analyzer{
    23  		Name:     "S1009",
    24  		Run:      run,
    25  		Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
    26  	},
    27  	Doc: &lint.Documentation{
    28  		Title: `Omit redundant nil check on slices`,
    29  		Text: `The \'len\' function is defined for all slices, even nil ones, which have
    30  a length of zero. It is not necessary to check if a slice is not nil
    31  before checking that its length is not zero.`,
    32  		Before:  `if x != nil && len(x) != 0 {}`,
    33  		After:   `if len(x) != 0 {}`,
    34  		Since:   "2017.1",
    35  		MergeIf: lint.MergeIfAny,
    36  	},
    37  })
    38  
    39  var Analyzer = SCAnalyzer.Analyzer
    40  
    41  // run checks for the following redundant nil-checks:
    42  //
    43  //	if x == nil || len(x) == 0 {}
    44  //	if x != nil && len(x) != 0 {}
    45  //	if x != nil && len(x) == N {} (where N != 0)
    46  //	if x != nil && len(x) > N {}
    47  //	if x != nil && len(x) >= N {} (where N != 0)
    48  func run(pass *analysis.Pass) (interface{}, error) {
    49  	isConstZero := func(expr ast.Expr) (isConst bool, isZero bool) {
    50  		_, ok := expr.(*ast.BasicLit)
    51  		if ok {
    52  			return true, code.IsIntegerLiteral(pass, expr, constant.MakeInt64(0))
    53  		}
    54  		id, ok := expr.(*ast.Ident)
    55  		if !ok {
    56  			return false, false
    57  		}
    58  		c, ok := pass.TypesInfo.ObjectOf(id).(*types.Const)
    59  		if !ok {
    60  			return false, false
    61  		}
    62  		return true, c.Val().Kind() == constant.Int && c.Val().String() == "0"
    63  	}
    64  
    65  	fn := func(node ast.Node) {
    66  		// check that expr is "x || y" or "x && y"
    67  		expr := node.(*ast.BinaryExpr)
    68  		if expr.Op != token.LOR && expr.Op != token.LAND {
    69  			return
    70  		}
    71  		eqNil := expr.Op == token.LOR
    72  
    73  		// check that x is "xx == nil" or "xx != nil"
    74  		x, ok := expr.X.(*ast.BinaryExpr)
    75  		if !ok {
    76  			return
    77  		}
    78  		if eqNil && x.Op != token.EQL {
    79  			return
    80  		}
    81  		if !eqNil && x.Op != token.NEQ {
    82  			return
    83  		}
    84  		xx, ok := x.X.(*ast.Ident)
    85  		if !ok {
    86  			return
    87  		}
    88  		if !code.IsNil(pass, x.Y) {
    89  			return
    90  		}
    91  
    92  		// check that y is "len(xx) == 0" or "len(xx) ... "
    93  		y, ok := expr.Y.(*ast.BinaryExpr)
    94  		if !ok {
    95  			return
    96  		}
    97  		if eqNil && y.Op != token.EQL { // must be len(xx) *==* 0
    98  			return
    99  		}
   100  		yx, ok := y.X.(*ast.CallExpr)
   101  		if !ok {
   102  			return
   103  		}
   104  		if !code.IsCallTo(pass, yx, "len") {
   105  			return
   106  		}
   107  		yxArg, ok := yx.Args[knowledge.Arg("len.v")].(*ast.Ident)
   108  		if !ok {
   109  			return
   110  		}
   111  		if yxArg.Name != xx.Name {
   112  			return
   113  		}
   114  
   115  		if eqNil && !code.IsIntegerLiteral(pass, y.Y, constant.MakeInt64(0)) { // must be len(x) == *0*
   116  			return
   117  		}
   118  
   119  		if !eqNil {
   120  			isConst, isZero := isConstZero(y.Y)
   121  			if !isConst {
   122  				return
   123  			}
   124  			switch y.Op {
   125  			case token.EQL:
   126  				// avoid false positive for "xx != nil && len(xx) == 0"
   127  				if isZero {
   128  					return
   129  				}
   130  			case token.GEQ:
   131  				// avoid false positive for "xx != nil && len(xx) >= 0"
   132  				if isZero {
   133  					return
   134  				}
   135  			case token.NEQ:
   136  				// avoid false positive for "xx != nil && len(xx) != <non-zero>"
   137  				if !isZero {
   138  					return
   139  				}
   140  			case token.GTR:
   141  				// ok
   142  			default:
   143  				return
   144  			}
   145  		}
   146  
   147  		// finally check that xx type is one of array, slice, map or chan
   148  		// this is to prevent false positive in case if xx is a pointer to an array
   149  		typ := pass.TypesInfo.TypeOf(xx)
   150  		ok = typeutil.All(typ, func(term *types.Term) bool {
   151  			switch term.Type().Underlying().(type) {
   152  			case *types.Slice:
   153  				return true
   154  			case *types.Map:
   155  				return true
   156  			case *types.Chan:
   157  				return true
   158  			case *types.Pointer:
   159  				return false
   160  			case *types.TypeParam:
   161  				return false
   162  			default:
   163  				lint.ExhaustiveTypeSwitch(term.Type().Underlying())
   164  				return false
   165  			}
   166  		})
   167  		if !ok {
   168  			return
   169  		}
   170  
   171  		report.Report(pass, expr, fmt.Sprintf("should omit nil check; len() for %s is defined as zero", typ), report.FilterGenerated())
   172  	}
   173  	code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
   174  	return nil, nil
   175  }