Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/slices.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modernize
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/types"
    11  	"slices"
    12  	"strconv"
    13  
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/go/analysis/passes/inspect"
    16  	"golang.org/x/tools/go/types/typeutil"
    17  	"golang.org/x/tools/internal/analysis/analyzerutil"
    18  	"golang.org/x/tools/internal/astutil"
    19  	"golang.org/x/tools/internal/refactor"
    20  	"golang.org/x/tools/internal/typesinternal"
    21  	"golang.org/x/tools/internal/versions"
    22  )
    23  
    24  // Warning: this analyzer is not safe to enable by default.
    25  var AppendClippedAnalyzer = &analysis.Analyzer{
    26  	Name:     "appendclipped",
    27  	Doc:      analyzerutil.MustExtractDoc(doc, "appendclipped"),
    28  	Requires: []*analysis.Analyzer{inspect.Analyzer},
    29  	Run:      appendclipped,
    30  	URL:      "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#appendclipped",
    31  }
    32  
    33  // The appendclipped pass offers to simplify a tower of append calls:
    34  //
    35  //	append(append(append(base, a...), b..., c...)
    36  //
    37  // with a call to go1.21's slices.Concat(base, a, b, c), or simpler
    38  // replacements such as slices.Clone(a) in degenerate cases.
    39  //
    40  // We offer bytes.Clone in preference to slices.Clone where
    41  // appropriate, if the package already imports "bytes";
    42  // their behaviors are identical.
    43  //
    44  // The base expression must denote a clipped slice (see [isClipped]
    45  // for definition), otherwise the replacement might eliminate intended
    46  // side effects to the base slice's array.
    47  //
    48  // Examples:
    49  //
    50  //	append(append(append(x[:0:0], a...), b...), c...) -> slices.Concat(a, b, c)
    51  //	append(append(slices.Clip(a), b...)               -> slices.Concat(a, b)
    52  //	append([]T{}, a...)                               -> slices.Clone(a)
    53  //	append([]string(nil), os.Environ()...)            -> os.Environ()
    54  //
    55  // The fix does not always preserve nilness the of base slice when the
    56  // addends (a, b, c) are all empty (see #73557).
    57  func appendclipped(pass *analysis.Pass) (any, error) {
    58  	// Skip the analyzer in packages where its
    59  	// fixes would create an import cycle.
    60  	if within(pass, "slices", "bytes", "runtime") {
    61  		return nil, nil
    62  	}
    63  
    64  	info := pass.TypesInfo
    65  
    66  	// sliceArgs is a non-empty (reversed) list of slices to be concatenated.
    67  	simplifyAppendEllipsis := func(file *ast.File, call *ast.CallExpr, base ast.Expr, sliceArgs []ast.Expr) {
    68  		// Only appends whose base is a clipped slice can be simplified:
    69  		// We must conservatively assume an append to an unclipped slice
    70  		// such as append(y[:0], x...) is intended to have effects on y.
    71  		clipped, empty := clippedSlice(info, base)
    72  		if clipped == nil {
    73  			return
    74  		}
    75  
    76  		// If any slice arg has a different type from the base
    77  		// (and thus the result) don't offer a fix, to avoid
    78  		// changing the return type, e.g:
    79  		//
    80  		//     type S []int
    81  		//   - x := append([]int(nil), S{}...) // x : []int
    82  		//   + x := slices.Clone(S{})          // x : S
    83  		//
    84  		// We could do better by inserting an explicit generic
    85  		// instantiation:
    86  		//
    87  		//   x := slices.Clone[[]int](S{})
    88  		//
    89  		// but this is often unnecessary and unwanted, such as
    90  		// when the value is used an in assignment context that
    91  		// provides an explicit type:
    92  		//
    93  		//   var x []int = slices.Clone(S{})
    94  		baseType := info.TypeOf(base)
    95  		for _, arg := range sliceArgs {
    96  			if !types.Identical(info.TypeOf(arg), baseType) {
    97  				return
    98  			}
    99  		}
   100  
   101  		// If the (clipped) base is empty, it may be safely ignored.
   102  		// Otherwise treat it (or its unclipped subexpression, if possible)
   103  		// as just another arg (the first) to Concat.
   104  		//
   105  		// TODO(adonovan): not so fast! If all the operands
   106  		// are empty, then the nilness of base matters, because
   107  		// append preserves nilness whereas Concat does not (#73557).
   108  		if !empty {
   109  			sliceArgs = append(sliceArgs, clipped)
   110  		}
   111  		slices.Reverse(sliceArgs)
   112  
   113  		// TODO(adonovan): simplify sliceArgs[0] further: slices.Clone(s) -> s
   114  
   115  		// Concat of a single (non-trivial) slice degenerates to Clone.
   116  		if len(sliceArgs) == 1 {
   117  			s := sliceArgs[0]
   118  
   119  			// Special case for common but redundant clone of os.Environ().
   120  			// append(zerocap, os.Environ()...) -> os.Environ()
   121  			if scall, ok := s.(*ast.CallExpr); ok {
   122  				obj := typeutil.Callee(info, scall)
   123  				if typesinternal.IsFunctionNamed(obj, "os", "Environ") {
   124  					pass.Report(analysis.Diagnostic{
   125  						Pos:     call.Pos(),
   126  						End:     call.End(),
   127  						Message: "Redundant clone of os.Environ()",
   128  						SuggestedFixes: []analysis.SuggestedFix{{
   129  							Message: "Eliminate redundant clone",
   130  							TextEdits: []analysis.TextEdit{{
   131  								Pos:     call.Pos(),
   132  								End:     call.End(),
   133  								NewText: []byte(astutil.Format(pass.Fset, s)),
   134  							}},
   135  						}},
   136  					})
   137  					return
   138  				}
   139  			}
   140  
   141  			// If the slice type is []byte, and the file imports
   142  			// "bytes" but not "slices", prefer the (behaviorally
   143  			// identical) bytes.Clone for local consistency.
   144  			// https://go.dev/issue/70815#issuecomment-2671572984
   145  			fileImports := func(path string) bool {
   146  				return slices.ContainsFunc(file.Imports, func(spec *ast.ImportSpec) bool {
   147  					value, _ := strconv.Unquote(spec.Path.Value)
   148  					return value == path
   149  				})
   150  			}
   151  			clonepkg := cond(
   152  				types.Identical(info.TypeOf(call), byteSliceType) &&
   153  					!fileImports("slices") && fileImports("bytes"),
   154  				"bytes",
   155  				"slices")
   156  
   157  			// append(zerocap, s...) -> slices.Clone(s) or bytes.Clone(s)
   158  			//
   159  			// This is unsound if s is empty and its nilness
   160  			// differs from zerocap (#73557).
   161  			prefix, importEdits := refactor.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos())
   162  			message := fmt.Sprintf("Replace append with %s.Clone", clonepkg)
   163  			pass.Report(analysis.Diagnostic{
   164  				Pos:     call.Pos(),
   165  				End:     call.End(),
   166  				Message: message,
   167  				SuggestedFixes: []analysis.SuggestedFix{{
   168  					Message: message,
   169  					TextEdits: append(importEdits, []analysis.TextEdit{{
   170  						Pos:     call.Pos(),
   171  						End:     call.End(),
   172  						NewText: fmt.Appendf(nil, "%sClone(%s)", prefix, astutil.Format(pass.Fset, s)),
   173  					}}...),
   174  				}},
   175  			})
   176  			return
   177  		}
   178  
   179  		// append(append(append(base, a...), b..., c...) -> slices.Concat(base, a, b, c)
   180  		//
   181  		// This is unsound if all slices are empty and base is non-nil (#73557).
   182  		prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", "Concat", call.Pos())
   183  		pass.Report(analysis.Diagnostic{
   184  			Pos:     call.Pos(),
   185  			End:     call.End(),
   186  			Message: "Replace append with slices.Concat",
   187  			SuggestedFixes: []analysis.SuggestedFix{{
   188  				Message: "Replace append with slices.Concat",
   189  				TextEdits: append(importEdits, []analysis.TextEdit{{
   190  					Pos:     call.Pos(),
   191  					End:     call.End(),
   192  					NewText: fmt.Appendf(nil, "%sConcat(%s)", prefix, formatExprs(pass.Fset, sliceArgs)),
   193  				}}...),
   194  			}},
   195  		})
   196  	}
   197  
   198  	// Mark nested calls to append so that we don't emit diagnostics for them.
   199  	skip := make(map[*ast.CallExpr]bool)
   200  
   201  	// Visit calls of form append(x, y...).
   202  	for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
   203  		file := curFile.Node().(*ast.File)
   204  
   205  		for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
   206  			call := curCall.Node().(*ast.CallExpr)
   207  			if skip[call] {
   208  				continue
   209  			}
   210  
   211  			// Recursively unwrap ellipsis calls to append, so
   212  			//   append(append(append(base, a...), b..., c...)
   213  			// yields (base, [c b a]).
   214  			base, slices := ast.Expr(call), []ast.Expr(nil) // base case: (call, nil)
   215  		again:
   216  			if call, ok := base.(*ast.CallExpr); ok {
   217  				if id, ok := call.Fun.(*ast.Ident); ok &&
   218  					call.Ellipsis.IsValid() &&
   219  					len(call.Args) == 2 &&
   220  					info.Uses[id] == builtinAppend {
   221  
   222  					// Have: append(base, s...)
   223  					base, slices = call.Args[0], append(slices, call.Args[1])
   224  					skip[call] = true
   225  					goto again
   226  				}
   227  			}
   228  
   229  			if len(slices) > 0 {
   230  				simplifyAppendEllipsis(file, call, base, slices)
   231  			}
   232  		}
   233  	}
   234  	return nil, nil
   235  }
   236  
   237  // clippedSlice returns res != nil if e denotes a slice that is
   238  // definitely clipped, that is, its len(s)==cap(s).
   239  //
   240  // The value of res is either the same as e or is a subexpression of e
   241  // that denotes the same slice but without the clipping operation.
   242  //
   243  // In addition, it reports whether the slice is definitely empty.
   244  //
   245  // Examples of clipped slices:
   246  //
   247  //	x[:0:0]				(empty)
   248  //	[]T(nil)			(empty)
   249  //	Slice{}				(empty)
   250  //	x[:len(x):len(x)]		(nonempty)  res=x
   251  //	x[:k:k]	 	         	(nonempty)
   252  //	slices.Clip(x)			(nonempty)  res=x
   253  //
   254  // TODO(adonovan): Add a check that the expression x has no side effects in
   255  // case x[:len(x):len(x)] -> x. Now the program behavior may change.
   256  func clippedSlice(info *types.Info, e ast.Expr) (res ast.Expr, empty bool) {
   257  	switch e := e.(type) {
   258  	case *ast.SliceExpr:
   259  		// x[:0:0], x[:len(x):len(x)], x[:k:k]
   260  		if e.Slice3 && e.High != nil && e.Max != nil && astutil.EqualSyntax(e.High, e.Max) { // x[:k:k]
   261  			res = e
   262  			empty = isZeroIntConst(info, e.High) // x[:0:0]
   263  			if call, ok := e.High.(*ast.CallExpr); ok &&
   264  				typeutil.Callee(info, call) == builtinLen &&
   265  				astutil.EqualSyntax(call.Args[0], e.X) {
   266  				res = e.X // x[:len(x):len(x)] -> x
   267  			}
   268  			return
   269  		}
   270  		return
   271  
   272  	case *ast.CallExpr:
   273  		// []T(nil)?
   274  		if info.Types[e.Fun].IsType() &&
   275  			is[*ast.Ident](e.Args[0]) &&
   276  			info.Uses[e.Args[0].(*ast.Ident)] == builtinNil {
   277  			return e, true
   278  		}
   279  
   280  		// slices.Clip(x)?
   281  		obj := typeutil.Callee(info, e)
   282  		if typesinternal.IsFunctionNamed(obj, "slices", "Clip") {
   283  			return e.Args[0], false // slices.Clip(x) -> x
   284  		}
   285  
   286  	case *ast.CompositeLit:
   287  		// Slice{}?
   288  		if len(e.Elts) == 0 {
   289  			return e, true
   290  		}
   291  	}
   292  	return nil, false
   293  }
   294  

View as plain text