|
| 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 | +// This file defines modernizers that use the "slices" package. |
| 8 | + |
| 9 | +import ( |
| 10 | + "fmt" |
| 11 | + "go/ast" |
| 12 | + "go/token" |
| 13 | + "go/types" |
| 14 | + "slices" |
| 15 | + |
| 16 | + "golang.org/x/tools/go/analysis" |
| 17 | + "golang.org/x/tools/go/analysis/passes/inspect" |
| 18 | + "golang.org/x/tools/go/ast/inspector" |
| 19 | + "golang.org/x/tools/internal/analysisinternal" |
| 20 | + "golang.org/x/tools/internal/astutil/cursor" |
| 21 | + "golang.org/x/tools/internal/versions" |
| 22 | +) |
| 23 | + |
| 24 | +// The appendclipped pass offers to simplify a tower of append calls: |
| 25 | +// |
| 26 | +// append(append(append(base, a...), b..., c...) |
| 27 | +// |
| 28 | +// with a call to go1.21's slices.Concat(base, a, b, c), or simpler |
| 29 | +// replacements such as slices.Clone(a) in degenerate cases. |
| 30 | +// |
| 31 | +// The base expression must denote a clipped slice (see [isClipped] |
| 32 | +// for definition), otherwise the replacement might eliminate intended |
| 33 | +// side effects to the base slice's array. |
| 34 | +// |
| 35 | +// Examples: |
| 36 | +// |
| 37 | +// append(append(append(x[:0:0], a...), b...), c...) -> slices.Concat(a, b, c) |
| 38 | +// append(append(slices.Clip(a), b...) -> slices.Concat(a, b) |
| 39 | +// append([]T{}, a...) -> slices.Clone(a) |
| 40 | +// append([]string(nil), os.Environ()...) -> os.Environ() |
| 41 | +// |
| 42 | +// The fix does not always preserve nilness the of base slice when the |
| 43 | +// addends (a, b, c) are all empty. |
| 44 | +func appendclipped(pass *analysis.Pass) { |
| 45 | + if pass.Pkg.Path() == "slices" { |
| 46 | + return |
| 47 | + } |
| 48 | + |
| 49 | + // sliceArgs is a non-empty (reversed) list of slices to be concatenated. |
| 50 | + simplifyAppendEllipsis := func(call *ast.CallExpr, base ast.Expr, sliceArgs []ast.Expr) { |
| 51 | + // Only appends whose base is a clipped slice can be simplified: |
| 52 | + // We must conservatively assume an append to an unclipped slice |
| 53 | + // such as append(y[:0], x...) is intended to have effects on y. |
| 54 | + clipped, empty := isClippedSlice(pass.TypesInfo, base) |
| 55 | + if !clipped { |
| 56 | + return |
| 57 | + } |
| 58 | + |
| 59 | + // If the (clipped) base is empty, it may be safely ignored. |
| 60 | + // Otherwise treat it as just another arg (the first) to Concat. |
| 61 | + if !empty { |
| 62 | + sliceArgs = append(sliceArgs, base) |
| 63 | + } |
| 64 | + slices.Reverse(sliceArgs) |
| 65 | + |
| 66 | + // Concat of a single (non-trivial) slice degenerates to Clone. |
| 67 | + if len(sliceArgs) == 1 { |
| 68 | + s := sliceArgs[0] |
| 69 | + |
| 70 | + // Special case for common but redundant clone of os.Environ(). |
| 71 | + // append(zerocap, os.Environ()...) -> os.Environ() |
| 72 | + if scall, ok := s.(*ast.CallExpr); ok && |
| 73 | + isQualifiedIdent(pass.TypesInfo, scall.Fun, "os", "Environ") { |
| 74 | + |
| 75 | + pass.Report(analysis.Diagnostic{ |
| 76 | + Pos: call.Pos(), |
| 77 | + End: call.End(), |
| 78 | + Category: "slicesclone", |
| 79 | + Message: "Redundant clone of os.Environ()", |
| 80 | + SuggestedFixes: []analysis.SuggestedFix{{ |
| 81 | + Message: "Eliminate redundant clone", |
| 82 | + TextEdits: []analysis.TextEdit{{ |
| 83 | + Pos: call.Pos(), |
| 84 | + End: call.End(), |
| 85 | + NewText: formatNode(pass.Fset, s), |
| 86 | + }}, |
| 87 | + }}, |
| 88 | + }) |
| 89 | + return |
| 90 | + } |
| 91 | + |
| 92 | + // append(zerocap, s...) -> slices.Clone(s) |
| 93 | + file := enclosingFile(pass, call.Pos()) |
| 94 | + slicesName, importEdits := analysisinternal.AddImport(pass.TypesInfo, file, call.Pos(), "slices", "slices") |
| 95 | + pass.Report(analysis.Diagnostic{ |
| 96 | + Pos: call.Pos(), |
| 97 | + End: call.End(), |
| 98 | + Category: "slicesclone", |
| 99 | + Message: "Replace append with slices.Clone", |
| 100 | + SuggestedFixes: []analysis.SuggestedFix{{ |
| 101 | + Message: "Replace append with slices.Clone", |
| 102 | + TextEdits: append(importEdits, []analysis.TextEdit{{ |
| 103 | + Pos: call.Pos(), |
| 104 | + End: call.End(), |
| 105 | + NewText: []byte(fmt.Sprintf("%s.Clone(%s)", slicesName, formatNode(pass.Fset, s))), |
| 106 | + }}...), |
| 107 | + }}, |
| 108 | + }) |
| 109 | + return |
| 110 | + } |
| 111 | + |
| 112 | + // append(append(append(base, a...), b..., c...) -> slices.Concat(base, a, b, c) |
| 113 | + // |
| 114 | + // TODO(adonovan): simplify sliceArgs[0] further: |
| 115 | + // - slices.Clone(s) -> s |
| 116 | + // - s[:len(s):len(s)] -> s |
| 117 | + // - slices.Clip(s) -> s |
| 118 | + file := enclosingFile(pass, call.Pos()) |
| 119 | + slicesName, importEdits := analysisinternal.AddImport(pass.TypesInfo, file, call.Pos(), "slices", "slices") |
| 120 | + pass.Report(analysis.Diagnostic{ |
| 121 | + Pos: call.Pos(), |
| 122 | + End: call.End(), |
| 123 | + Category: "slicesclone", |
| 124 | + Message: "Replace append with slices.Concat", |
| 125 | + SuggestedFixes: []analysis.SuggestedFix{{ |
| 126 | + Message: "Replace append with slices.Concat", |
| 127 | + TextEdits: append(importEdits, []analysis.TextEdit{{ |
| 128 | + Pos: call.Pos(), |
| 129 | + End: call.End(), |
| 130 | + NewText: []byte(fmt.Sprintf("%s.Concat(%s)", slicesName, formatExprs(pass.Fset, sliceArgs))), |
| 131 | + }}...), |
| 132 | + }}, |
| 133 | + }) |
| 134 | + } |
| 135 | + |
| 136 | + // Mark nested calls to append so that we don't emit diagnostics for them. |
| 137 | + skip := make(map[*ast.CallExpr]bool) |
| 138 | + |
| 139 | + // Visit calls of form append(x, y...). |
| 140 | + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) |
| 141 | + filter := []ast.Node{(*ast.File)(nil), (*ast.CallExpr)(nil)} |
| 142 | + cursor.Root(inspect).Inspect(filter, func(cur cursor.Cursor, push bool) (descend bool) { |
| 143 | + if push { |
| 144 | + switch n := cur.Node().(type) { |
| 145 | + case *ast.File: |
| 146 | + if versions.Before(pass.TypesInfo.FileVersions[n], "go1.21") { |
| 147 | + return false |
| 148 | + } |
| 149 | + |
| 150 | + case *ast.CallExpr: |
| 151 | + call := n |
| 152 | + if skip[call] { |
| 153 | + return true |
| 154 | + } |
| 155 | + |
| 156 | + // Recursively unwrap ellipsis calls to append, so |
| 157 | + // append(append(append(base, a...), b..., c...) |
| 158 | + // yields (base, [c b a]). |
| 159 | + base, slices := ast.Expr(call), []ast.Expr(nil) // base case: (call, nil) |
| 160 | + again: |
| 161 | + if call, ok := base.(*ast.CallExpr); ok { |
| 162 | + if id, ok := call.Fun.(*ast.Ident); ok && |
| 163 | + call.Ellipsis.IsValid() && |
| 164 | + len(call.Args) == 2 && |
| 165 | + pass.TypesInfo.Uses[id] == builtinAppend { |
| 166 | + |
| 167 | + // Have: append(base, s...) |
| 168 | + base, slices = call.Args[0], append(slices, call.Args[1]) |
| 169 | + skip[call] = true |
| 170 | + goto again |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + if len(slices) > 0 { |
| 175 | + simplifyAppendEllipsis(call, base, slices) |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + return true |
| 180 | + }) |
| 181 | +} |
| 182 | + |
| 183 | +// isClippedSlice reports whether e denotes a slice that is definitely |
| 184 | +// clipped, that is, its len(s)==cap(s). |
| 185 | +// |
| 186 | +// In addition, it reports whether the slice is definitely empty. |
| 187 | +// |
| 188 | +// Examples of clipped slices: |
| 189 | +// |
| 190 | +// x[:0:0] (empty) |
| 191 | +// []T(nil) (empty) |
| 192 | +// Slice{} (empty) |
| 193 | +// x[:len(x):len(x)] (nonempty) |
| 194 | +// x[:k:k] (nonempty) |
| 195 | +// slices.Clip(x) (nonempty) |
| 196 | +func isClippedSlice(info *types.Info, e ast.Expr) (clipped, empty bool) { |
| 197 | + switch e := e.(type) { |
| 198 | + case *ast.SliceExpr: |
| 199 | + // x[:0:0], x[:len(x):len(x)], x[:k:k], x[:0] |
| 200 | + isZeroLiteral := func(e ast.Expr) bool { |
| 201 | + lit, ok := e.(*ast.BasicLit) |
| 202 | + return ok && lit.Kind == token.INT && lit.Value == "0" |
| 203 | + } |
| 204 | + clipped = e.Slice3 && e.High != nil && e.Max != nil && equalSyntax(e.High, e.Max) // x[:k:k] |
| 205 | + empty = e.High != nil && isZeroLiteral(e.High) // x[:0:*] |
| 206 | + return |
| 207 | + |
| 208 | + case *ast.CallExpr: |
| 209 | + // []T(nil)? |
| 210 | + if info.Types[e.Fun].IsType() && |
| 211 | + is[*ast.Ident](e.Args[0]) && |
| 212 | + info.Uses[e.Args[0].(*ast.Ident)] == builtinNil { |
| 213 | + return true, true |
| 214 | + } |
| 215 | + |
| 216 | + // slices.Clip(x)? |
| 217 | + if isQualifiedIdent(info, e.Fun, "slices", "Clip") { |
| 218 | + return true, false |
| 219 | + } |
| 220 | + |
| 221 | + case *ast.CompositeLit: |
| 222 | + // Slice{}? |
| 223 | + if len(e.Elts) == 0 { |
| 224 | + return true, true |
| 225 | + } |
| 226 | + } |
| 227 | + return false, false |
| 228 | +} |
| 229 | + |
| 230 | +var ( |
| 231 | + builtinAppend = types.Universe.Lookup("append") |
| 232 | + builtinNil = types.Universe.Lookup("nil") |
| 233 | +) |
0 commit comments