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

     1  package sa5012
     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/lint"
    11  	"github.com/amarpal/go-tools/analysis/report"
    12  	"github.com/amarpal/go-tools/go/ir"
    13  	"github.com/amarpal/go-tools/go/ir/irutil"
    14  	"github.com/amarpal/go-tools/go/types/typeutil"
    15  	"github.com/amarpal/go-tools/internal/passes/buildir"
    16  
    17  	"golang.org/x/tools/go/analysis"
    18  )
    19  
    20  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    21  	Analyzer: &analysis.Analyzer{
    22  		Name:      "SA5012",
    23  		Run:       run,
    24  		FactTypes: []analysis.Fact{new(evenElements)},
    25  		Requires:  []*analysis.Analyzer{buildir.Analyzer},
    26  	},
    27  	Doc: &lint.Documentation{
    28  		Title: "Passing odd-sized slice to function expecting even size",
    29  		Text: `Some functions that take slices as parameters expect the slices to have an even number of elements. 
    30  Often, these functions treat elements in a slice as pairs. 
    31  For example, \'strings.NewReplacer\' takes pairs of old and new strings, 
    32  and calling it with an odd number of elements would be an error.`,
    33  		Since:    "2020.2",
    34  		Severity: lint.SeverityError,
    35  		MergeIf:  lint.MergeIfAny,
    36  	},
    37  })
    38  
    39  var Analyzer = SCAnalyzer.Analyzer
    40  
    41  type evenElements struct{}
    42  
    43  func (evenElements) AFact() {}
    44  
    45  func (evenElements) String() string { return "needs even elements" }
    46  
    47  func findSliceLength(v ir.Value) int {
    48  	// TODO(dh): VRP would help here
    49  
    50  	v = irutil.Flatten(v)
    51  	val := func(v ir.Value) int {
    52  		if v, ok := v.(*ir.Const); ok {
    53  			return int(v.Int64())
    54  		}
    55  		return -1
    56  	}
    57  	switch v := v.(type) {
    58  	case *ir.Slice:
    59  		low := 0
    60  		high := -1
    61  		if v.Low != nil {
    62  			low = val(v.Low)
    63  		}
    64  		if v.High != nil {
    65  			high = val(v.High)
    66  		} else {
    67  			switch vv := v.X.(type) {
    68  			case *ir.Alloc:
    69  				high = int(typeutil.Dereference(vv.Type()).Underlying().(*types.Array).Len())
    70  			case *ir.Slice:
    71  				high = findSliceLength(vv)
    72  			}
    73  		}
    74  		if low == -1 || high == -1 {
    75  			return -1
    76  		}
    77  		return high - low
    78  	default:
    79  		return -1
    80  	}
    81  }
    82  
    83  func flagSliceLens(pass *analysis.Pass) {
    84  	var tag evenElements
    85  
    86  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
    87  		for _, b := range fn.Blocks {
    88  			for _, instr := range b.Instrs {
    89  				call, ok := instr.(ir.CallInstruction)
    90  				if !ok {
    91  					continue
    92  				}
    93  				callee := call.Common().StaticCallee()
    94  				if callee == nil {
    95  					continue
    96  				}
    97  				for argi, arg := range call.Common().Args {
    98  					if callee.Signature.Recv() != nil {
    99  						if argi == 0 {
   100  							continue
   101  						}
   102  						argi--
   103  					}
   104  
   105  					_, ok := arg.Type().Underlying().(*types.Slice)
   106  					if !ok {
   107  						continue
   108  					}
   109  					param := callee.Signature.Params().At(argi)
   110  					if !pass.ImportObjectFact(param, &tag) {
   111  						continue
   112  					}
   113  
   114  					// TODO handle stubs
   115  
   116  					// we know the argument has to have even length.
   117  					// now let's try to find its length
   118  					if n := findSliceLength(arg); n > -1 && n%2 != 0 {
   119  						src := call.Source().(*ast.CallExpr).Args[argi]
   120  						sig := call.Common().Signature()
   121  						var label string
   122  						if argi == sig.Params().Len()-1 && sig.Variadic() {
   123  							label = "variadic argument"
   124  						} else {
   125  							label = "argument"
   126  						}
   127  						// Note that param.Name() is guaranteed to not
   128  						// be empty, otherwise the function couldn't
   129  						// have enforced its length.
   130  						report.Report(pass, src, fmt.Sprintf("%s %q is expected to have even number of elements, but has %d elements", label, param.Name(), n))
   131  					}
   132  				}
   133  			}
   134  		}
   135  	}
   136  }
   137  
   138  func findSliceLenChecks(pass *analysis.Pass) {
   139  	// mark all function parameters that have to be of even length
   140  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
   141  		for _, b := range fn.Blocks {
   142  			// all paths go through this block
   143  			if !b.Dominates(fn.Exit) {
   144  				continue
   145  			}
   146  
   147  			// if foo % 2 != 0
   148  			ifi, ok := b.Control().(*ir.If)
   149  			if !ok {
   150  				continue
   151  			}
   152  			cmp, ok := ifi.Cond.(*ir.BinOp)
   153  			if !ok {
   154  				continue
   155  			}
   156  			var needle uint64
   157  			switch cmp.Op {
   158  			case token.NEQ:
   159  				// look for != 0
   160  				needle = 0
   161  			case token.EQL:
   162  				// look for == 1
   163  				needle = 1
   164  			default:
   165  				continue
   166  			}
   167  
   168  			rem, ok1 := cmp.X.(*ir.BinOp)
   169  			k, ok2 := cmp.Y.(*ir.Const)
   170  			if ok1 != ok2 {
   171  				continue
   172  			}
   173  			if !ok1 {
   174  				rem, ok1 = cmp.Y.(*ir.BinOp)
   175  				k, ok2 = cmp.X.(*ir.Const)
   176  			}
   177  			if !ok1 || !ok2 || rem.Op != token.REM || k.Value.Kind() != constant.Int || k.Uint64() != needle {
   178  				continue
   179  			}
   180  			k, ok = rem.Y.(*ir.Const)
   181  			if !ok || k.Value.Kind() != constant.Int || k.Uint64() != 2 {
   182  				continue
   183  			}
   184  
   185  			// if len(foo) % 2 != 0
   186  			call, ok := rem.X.(*ir.Call)
   187  			if !ok || !irutil.IsCallTo(call.Common(), "len") {
   188  				continue
   189  			}
   190  
   191  			// we're checking the length of a parameter that is a slice
   192  			// TODO(dh): support parameters that have flown through sigmas and phis
   193  			param, ok := call.Call.Args[0].(*ir.Parameter)
   194  			if !ok {
   195  				continue
   196  			}
   197  			if !typeutil.All(param.Type(), typeutil.IsSlice) {
   198  				continue
   199  			}
   200  
   201  			// if len(foo) % 2 != 0 then panic
   202  			if _, ok := b.Succs[0].Control().(*ir.Panic); !ok {
   203  				continue
   204  			}
   205  
   206  			pass.ExportObjectFact(param.Object(), new(evenElements))
   207  		}
   208  	}
   209  }
   210  
   211  func findIndirectSliceLenChecks(pass *analysis.Pass) {
   212  	seen := map[*ir.Function]struct{}{}
   213  
   214  	var doFunction func(fn *ir.Function)
   215  	doFunction = func(fn *ir.Function) {
   216  		if _, ok := seen[fn]; ok {
   217  			return
   218  		}
   219  		seen[fn] = struct{}{}
   220  
   221  		for _, b := range fn.Blocks {
   222  			// all paths go through this block
   223  			if !b.Dominates(fn.Exit) {
   224  				continue
   225  			}
   226  
   227  			for _, instr := range b.Instrs {
   228  				call, ok := instr.(*ir.Call)
   229  				if !ok {
   230  					continue
   231  				}
   232  				callee := call.Call.StaticCallee()
   233  				if callee == nil {
   234  					continue
   235  				}
   236  
   237  				if callee.Pkg == fn.Pkg || callee.Pkg == nil {
   238  					doFunction(callee)
   239  				}
   240  
   241  				for argi, arg := range call.Call.Args {
   242  					if callee.Signature.Recv() != nil {
   243  						if argi == 0 {
   244  							continue
   245  						}
   246  						argi--
   247  					}
   248  
   249  					// TODO(dh): support parameters that have flown through length-preserving instructions
   250  					param, ok := arg.(*ir.Parameter)
   251  					if !ok {
   252  						continue
   253  					}
   254  					if !typeutil.All(param.Type(), typeutil.IsSlice) {
   255  						continue
   256  					}
   257  
   258  					// We can't use callee.Params to look up the
   259  					// parameter, because Params is not populated for
   260  					// external functions. In our modular analysis.
   261  					// any function in any package that isn't the
   262  					// current package is considered "external", as it
   263  					// has been loaded from export data only.
   264  					sigParams := callee.Signature.Params()
   265  
   266  					if !pass.ImportObjectFact(sigParams.At(argi), new(evenElements)) {
   267  						continue
   268  					}
   269  					pass.ExportObjectFact(param.Object(), new(evenElements))
   270  				}
   271  			}
   272  		}
   273  	}
   274  
   275  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
   276  		doFunction(fn)
   277  	}
   278  }
   279  
   280  func run(pass *analysis.Pass) (interface{}, error) {
   281  	findSliceLenChecks(pass)
   282  	findIndirectSliceLenChecks(pass)
   283  	flagSliceLens(pass)
   284  
   285  	return nil, nil
   286  }