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  					return first(strconv.Unquote(spec.Path.Value)) == path
   148  				})
   149  			}
   150  			clonepkg := cond(
   151  				types.Identical(info.TypeOf(call), byteSliceType) &&
   152  					!fileImports("slices") && fileImports("bytes"),
   153  				"bytes",
   154  				"slices")
   155  
   156  			// append(zerocap, s...) -> slices.Clone(s) or bytes.Clone(s)
   157  			//
   158  			// This is unsound if s is empty and its nilness
   159  			// differs from zerocap (#73557).
   160  			prefix, importEdits := refactor.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos())
   161  			message := fmt.Sprintf("Replace append with %s.Clone", clonepkg)
   162  			pass.Report(analysis.Diagnostic{
   163  				Pos:     call.Pos(),
   164  				End:     call.End(),
   165  				Message: message,
   166  				SuggestedFixes: []analysis.SuggestedFix{{
   167  					Message: message,
   168  					TextEdits: append(importEdits, []analysis.TextEdit{{
   169  						Pos:     call.Pos(),
   170  						End:     call.End(),
   171  						NewText: fmt.Appendf(nil, "%sClone(%s)", prefix, astutil.Format(pass.Fset, s)),
   172  					}}...),
   173  				}},
   174  			})
   175  			return
   176  		}
   177  
   178  		// append(append(append(base, a...), b..., c...) -> slices.Concat(base, a, b, c)
   179  		//
   180  		// This is unsound if all slices are empty and base is non-nil (#73557).
   181  		prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", "Concat", call.Pos())
   182  		pass.Report(analysis.Diagnostic{
   183  			Pos:     call.Pos(),
   184  			End:     call.End(),
   185  			Message: "Replace append with slices.Concat",
   186  			SuggestedFixes: []analysis.SuggestedFix{{
   187  				Message: "Replace append with slices.Concat",
   188  				TextEdits: append(importEdits, []analysis.TextEdit{{
   189  					Pos:     call.Pos(),
   190  					End:     call.End(),
   191  					NewText: fmt.Appendf(nil, "%sConcat(%s)", prefix, formatExprs(pass.Fset, sliceArgs)),
   192  				}}...),
   193  			}},
   194  		})
   195  	}
   196  
   197  	// Mark nested calls to append so that we don't emit diagnostics for them.
   198  	skip := make(map[*ast.CallExpr]bool)
   199  
   200  	// Visit calls of form append(x, y...).
   201  	for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
   202  		file := curFile.Node().(*ast.File)
   203  
   204  		for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
   205  			call := curCall.Node().(*ast.CallExpr)
   206  			if skip[call] {
   207  				continue
   208  			}
   209  
   210  			// Recursively unwrap ellipsis calls to append, so
   211  			//   append(append(append(base, a...), b..., c...)
   212  			// yields (base, [c b a]).
   213  			base, slices := ast.Expr(call), []ast.Expr(nil) // base case: (call, nil)
   214  		again:
   215  			if call, ok := base.(*ast.CallExpr); ok {
   216  				if id, ok := call.Fun.(*ast.Ident); ok &&
   217  					call.Ellipsis.IsValid() &&
   218  					len(call.Args) == 2 &&
   219  					info.Uses[id] == builtinAppend {
   220  
   221  					// Have: append(base, s...)
   222  					base, slices = call.Args[0], append(slices, call.Args[1])
   223  					skip[call] = true
   224  					goto again
   225  				}
   226  			}
   227  
   228  			if len(slices) > 0 {
   229  				simplifyAppendEllipsis(file, call, base, slices)
   230  			}
   231  		}
   232  	}
   233  	return nil, nil
   234  }
   235  
   236  // clippedSlice returns res != nil if e denotes a slice that is
   237  // definitely clipped, that is, its len(s)==cap(s).
   238  //
   239  // The value of res is either the same as e or is a subexpression of e
   240  // that denotes the same slice but without the clipping operation.
   241  //
   242  // In addition, it reports whether the slice is definitely empty.
   243  //
   244  // Examples of clipped slices:
   245  //
   246  //	x[:0:0]				(empty)
   247  //	[]T(nil)			(empty)
   248  //	Slice{}				(empty)
   249  //	x[:len(x):len(x)]		(nonempty)  res=x
   250  //	x[:k:k]	 	         	(nonempty)
   251  //	slices.Clip(x)			(nonempty)  res=x
   252  //
   253  // TODO(adonovan): Add a check that the expression x has no side effects in
   254  // case x[:len(x):len(x)] -> x. Now the program behavior may change.
   255  func clippedSlice(info *types.Info, e ast.Expr) (res ast.Expr, empty bool) {
   256  	switch e := e.(type) {
   257  	case *ast.SliceExpr:
   258  		// x[:0:0], x[:len(x):len(x)], x[:k:k]
   259  		if e.Slice3 && e.High != nil && e.Max != nil && astutil.EqualSyntax(e.High, e.Max) { // x[:k:k]
   260  			res = e
   261  			empty = isZeroIntConst(info, e.High) // x[:0:0]
   262  			if call, ok := e.High.(*ast.CallExpr); ok &&
   263  				typeutil.Callee(info, call) == builtinLen &&
   264  				astutil.EqualSyntax(call.Args[0], e.X) {
   265  				res = e.X // x[:len(x):len(x)] -> x
   266  			}
   267  			return
   268  		}
   269  		return
   270  
   271  	case *ast.CallExpr:
   272  		// []T(nil)?
   273  		if info.Types[e.Fun].IsType() &&
   274  			is[*ast.Ident](e.Args[0]) &&
   275  			info.Uses[e.Args[0].(*ast.Ident)] == builtinNil {
   276  			return e, true
   277  		}
   278  
   279  		// slices.Clip(x)?
   280  		obj := typeutil.Callee(info, e)
   281  		if typesinternal.IsFunctionNamed(obj, "slices", "Clip") {
   282  			return e.Args[0], false // slices.Clip(x) -> x
   283  		}
   284  
   285  	case *ast.CompositeLit:
   286  		// Slice{}?
   287  		if len(e.Elts) == 0 {
   288  			return e, true
   289  		}
   290  	}
   291  	return nil, false
   292  }
   293  

View as plain text