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

     1  package s1001
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"go/types"
     8  
     9  	"github.com/amarpal/go-tools/analysis/code"
    10  	"github.com/amarpal/go-tools/analysis/edit"
    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/pattern"
    15  
    16  	"golang.org/x/tools/go/analysis"
    17  	"golang.org/x/tools/go/analysis/passes/inspect"
    18  )
    19  
    20  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    21  	Analyzer: &analysis.Analyzer{
    22  		Name:     "S1001",
    23  		Run:      run,
    24  		Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
    25  	},
    26  	Doc: &lint.Documentation{
    27  		Title: `Replace for loop with call to copy`,
    28  		Text: `
    29  Use \'copy()\' for copying elements from one slice to another. For
    30  arrays of identical size, you can use simple assignment.`,
    31  		Before: `
    32  for i, x := range src {
    33      dst[i] = x
    34  }`,
    35  		After: `copy(dst, src)`,
    36  		Since: "2017.1",
    37  		// MergeIfAll because the types of src and dst might be different under different build tags.
    38  		// You shouldn't write code like that…
    39  		MergeIf: lint.MergeIfAll,
    40  	},
    41  })
    42  
    43  var Analyzer = SCAnalyzer.Analyzer
    44  
    45  var (
    46  	checkLoopCopyQ = pattern.MustParse(`
    47  		(Or
    48  			(RangeStmt
    49  				key@(Ident _) value@(Ident _) ":=" src
    50  				[(AssignStmt (IndexExpr dst key) "=" value)])
    51  			(RangeStmt
    52  				key@(Ident _) nil ":=" src
    53  				[(AssignStmt (IndexExpr dst key) "=" (IndexExpr src key))])
    54  			(ForStmt
    55  				(AssignStmt key@(Ident _) ":=" (IntegerLiteral "0"))
    56  				(BinaryExpr key "<" (CallExpr (Symbol "len") [src]))
    57  				(IncDecStmt key "++")
    58  				[(AssignStmt (IndexExpr dst key) "=" (IndexExpr src key))]))`)
    59  )
    60  
    61  func run(pass *analysis.Pass) (interface{}, error) {
    62  	// TODO revisit once range doesn't require a structural type
    63  
    64  	isInvariant := func(k, v types.Object, node ast.Expr) bool {
    65  		if code.MayHaveSideEffects(pass, node, nil) {
    66  			return false
    67  		}
    68  		invariant := true
    69  		ast.Inspect(node, func(node ast.Node) bool {
    70  			if node, ok := node.(*ast.Ident); ok {
    71  				obj := pass.TypesInfo.ObjectOf(node)
    72  				if obj == k || obj == v {
    73  					// don't allow loop bodies like 'a[i][i] = v'
    74  					invariant = false
    75  					return false
    76  				}
    77  			}
    78  			return true
    79  		})
    80  		return invariant
    81  	}
    82  
    83  	var elType func(T types.Type) (el types.Type, isArray bool, isArrayPointer bool, ok bool)
    84  	elType = func(T types.Type) (el types.Type, isArray bool, isArrayPointer bool, ok bool) {
    85  		switch typ := T.Underlying().(type) {
    86  		case *types.Slice:
    87  			return typ.Elem(), false, false, true
    88  		case *types.Array:
    89  			return typ.Elem(), true, false, true
    90  		case *types.Pointer:
    91  			el, isArray, _, ok = elType(typ.Elem())
    92  			return el, isArray, true, ok
    93  		default:
    94  			return nil, false, false, false
    95  		}
    96  	}
    97  
    98  	fn := func(node ast.Node) {
    99  		m, ok := code.Match(pass, checkLoopCopyQ, node)
   100  		if !ok {
   101  			return
   102  		}
   103  
   104  		src := m.State["src"].(ast.Expr)
   105  		dst := m.State["dst"].(ast.Expr)
   106  
   107  		k := pass.TypesInfo.ObjectOf(m.State["key"].(*ast.Ident))
   108  		var v types.Object
   109  		if value, ok := m.State["value"]; ok {
   110  			v = pass.TypesInfo.ObjectOf(value.(*ast.Ident))
   111  		}
   112  		if !isInvariant(k, v, dst) {
   113  			return
   114  		}
   115  		if !isInvariant(k, v, src) {
   116  			// For example: 'for i := range foo()'
   117  			return
   118  		}
   119  
   120  		Tsrc := pass.TypesInfo.TypeOf(src)
   121  		Tdst := pass.TypesInfo.TypeOf(dst)
   122  		TsrcElem, TsrcArray, TsrcPointer, ok := elType(Tsrc)
   123  		if !ok {
   124  			return
   125  		}
   126  		if TsrcPointer {
   127  			Tsrc = Tsrc.Underlying().(*types.Pointer).Elem()
   128  		}
   129  		TdstElem, TdstArray, TdstPointer, ok := elType(Tdst)
   130  		if !ok {
   131  			return
   132  		}
   133  		if TdstPointer {
   134  			Tdst = Tdst.Underlying().(*types.Pointer).Elem()
   135  		}
   136  
   137  		if !types.Identical(TsrcElem, TdstElem) {
   138  			return
   139  		}
   140  
   141  		if TsrcArray && TdstArray && types.Identical(Tsrc, Tdst) {
   142  			if TsrcPointer {
   143  				src = &ast.StarExpr{
   144  					X: src,
   145  				}
   146  			}
   147  			if TdstPointer {
   148  				dst = &ast.StarExpr{
   149  					X: dst,
   150  				}
   151  			}
   152  			r := &ast.AssignStmt{
   153  				Lhs: []ast.Expr{dst},
   154  				Rhs: []ast.Expr{src},
   155  				Tok: token.ASSIGN,
   156  			}
   157  
   158  			report.Report(pass, node, "should copy arrays using assignment instead of using a loop",
   159  				report.FilterGenerated(),
   160  				report.ShortRange(),
   161  				report.Fixes(edit.Fix("replace loop with assignment", edit.ReplaceWithNode(pass.Fset, node, r))))
   162  		} else {
   163  			tv, err := types.Eval(pass.Fset, pass.Pkg, node.Pos(), "copy")
   164  			if err == nil && tv.IsBuiltin() {
   165  				to := "to"
   166  				from := "from"
   167  				src := m.State["src"].(ast.Expr)
   168  				if TsrcArray {
   169  					from = "from[:]"
   170  					src = &ast.SliceExpr{
   171  						X: src,
   172  					}
   173  				}
   174  				dst := m.State["dst"].(ast.Expr)
   175  				if TdstArray {
   176  					to = "to[:]"
   177  					dst = &ast.SliceExpr{
   178  						X: dst,
   179  					}
   180  				}
   181  
   182  				r := &ast.CallExpr{
   183  					Fun:  &ast.Ident{Name: "copy"},
   184  					Args: []ast.Expr{dst, src},
   185  				}
   186  				opts := []report.Option{
   187  					report.ShortRange(),
   188  					report.FilterGenerated(),
   189  					report.Fixes(edit.Fix("replace loop with call to copy()", edit.ReplaceWithNode(pass.Fset, node, r))),
   190  				}
   191  				report.Report(pass, node, fmt.Sprintf("should use copy(%s, %s) instead of a loop", to, from), opts...)
   192  			}
   193  		}
   194  	}
   195  	code.Preorder(pass, fn, (*ast.ForStmt)(nil), (*ast.RangeStmt)(nil))
   196  	return nil, nil
   197  }