blob: aada97df802b40b1cc53e950895f921576bc0fc2 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
// This file defines modernizers that use the "slices" package.
// TODO(adonovan): actually let's split them up and rename this file.
import (
"fmt"
"go/ast"
"go/types"
"slices"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
)
// The appendclipped pass offers to simplify a tower of append calls:
//
// append(append(append(base, a...), b..., c...)
//
// with a call to go1.21's slices.Concat(base, a, b, c), or simpler
// replacements such as slices.Clone(a) in degenerate cases.
//
// The base expression must denote a clipped slice (see [isClipped]
// for definition), otherwise the replacement might eliminate intended
// side effects to the base slice's array.
//
// Examples:
//
// append(append(append(x[:0:0], a...), b...), c...) -> slices.Concat(a, b, c)
// append(append(slices.Clip(a), b...) -> slices.Concat(a, b)
// append([]T{}, a...) -> slices.Clone(a)
// append([]string(nil), os.Environ()...) -> os.Environ()
//
// The fix does not always preserve nilness the of base slice when the
// addends (a, b, c) are all empty.
func appendclipped(pass *analysis.Pass) {
if pass.Pkg.Path() == "slices" {
return
}
info := pass.TypesInfo
// sliceArgs is a non-empty (reversed) list of slices to be concatenated.
simplifyAppendEllipsis := func(file *ast.File, call *ast.CallExpr, base ast.Expr, sliceArgs []ast.Expr) {
// Only appends whose base is a clipped slice can be simplified:
// We must conservatively assume an append to an unclipped slice
// such as append(y[:0], x...) is intended to have effects on y.
clipped, empty := isClippedSlice(info, base)
if !clipped {
return
}
// If the (clipped) base is empty, it may be safely ignored.
// Otherwise treat it as just another arg (the first) to Concat.
if !empty {
sliceArgs = append(sliceArgs, base)
}
slices.Reverse(sliceArgs)
// Concat of a single (non-trivial) slice degenerates to Clone.
if len(sliceArgs) == 1 {
s := sliceArgs[0]
// Special case for common but redundant clone of os.Environ().
// append(zerocap, os.Environ()...) -> os.Environ()
if scall, ok := s.(*ast.CallExpr); ok {
obj := typeutil.Callee(info, scall)
if analysisinternal.IsFunctionNamed(obj, "os", "Environ") {
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
Category: "slicesclone",
Message: "Redundant clone of os.Environ()",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Eliminate redundant clone",
TextEdits: []analysis.TextEdit{{
Pos: call.Pos(),
End: call.End(),
NewText: []byte(analysisinternal.Format(pass.Fset, s)),
}},
}},
})
return
}
}
// append(zerocap, s...) -> slices.Clone(s)
_, prefix, importEdits := analysisinternal.AddImport(info, file, "slices", "slices", "Clone", call.Pos())
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
Category: "slicesclone",
Message: "Replace append with slices.Clone",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace append with slices.Clone",
TextEdits: append(importEdits, []analysis.TextEdit{{
Pos: call.Pos(),
End: call.End(),
NewText: fmt.Appendf(nil, "%sClone(%s)", prefix, analysisinternal.Format(pass.Fset, s)),
}}...),
}},
})
return
}
// append(append(append(base, a...), b..., c...) -> slices.Concat(base, a, b, c)
//
// TODO(adonovan): simplify sliceArgs[0] further:
// - slices.Clone(s) -> s
// - s[:len(s):len(s)] -> s
// - slices.Clip(s) -> s
_, prefix, importEdits := analysisinternal.AddImport(info, file, "slices", "slices", "Concat", call.Pos())
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
Category: "slicesclone",
Message: "Replace append with slices.Concat",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace append with slices.Concat",
TextEdits: append(importEdits, []analysis.TextEdit{{
Pos: call.Pos(),
End: call.End(),
NewText: fmt.Appendf(nil, "%sConcat(%s)", prefix, formatExprs(pass.Fset, sliceArgs)),
}}...),
}},
})
}
// Mark nested calls to append so that we don't emit diagnostics for them.
skip := make(map[*ast.CallExpr]bool)
// Visit calls of form append(x, y...).
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
for curFile := range filesUsing(inspect, info, "go1.21") {
file := curFile.Node().(*ast.File)
for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
call := curCall.Node().(*ast.CallExpr)
if skip[call] {
continue
}
// Recursively unwrap ellipsis calls to append, so
// append(append(append(base, a...), b..., c...)
// yields (base, [c b a]).
base, slices := ast.Expr(call), []ast.Expr(nil) // base case: (call, nil)
again:
if call, ok := base.(*ast.CallExpr); ok {
if id, ok := call.Fun.(*ast.Ident); ok &&
call.Ellipsis.IsValid() &&
len(call.Args) == 2 &&
info.Uses[id] == builtinAppend {
// Have: append(base, s...)
base, slices = call.Args[0], append(slices, call.Args[1])
skip[call] = true
goto again
}
}
if len(slices) > 0 {
simplifyAppendEllipsis(file, call, base, slices)
}
}
}
}
// isClippedSlice reports whether e denotes a slice that is definitely
// clipped, that is, its len(s)==cap(s).
//
// In addition, it reports whether the slice is definitely empty.
//
// Examples of clipped slices:
//
// x[:0:0] (empty)
// []T(nil) (empty)
// Slice{} (empty)
// x[:len(x):len(x)] (nonempty)
// x[:k:k] (nonempty)
// slices.Clip(x) (nonempty)
func isClippedSlice(info *types.Info, e ast.Expr) (clipped, empty bool) {
switch e := e.(type) {
case *ast.SliceExpr:
// x[:0:0], x[:len(x):len(x)], x[:k:k], x[:0]
clipped = e.Slice3 && e.High != nil && e.Max != nil && equalSyntax(e.High, e.Max) // x[:k:k]
empty = e.High != nil && isZeroLiteral(e.High) // x[:0:*]
return
case *ast.CallExpr:
// []T(nil)?
if info.Types[e.Fun].IsType() &&
is[*ast.Ident](e.Args[0]) &&
info.Uses[e.Args[0].(*ast.Ident)] == builtinNil {
return true, true
}
// slices.Clip(x)?
obj := typeutil.Callee(info, e)
if analysisinternal.IsFunctionNamed(obj, "slices", "Clip") {
return true, false
}
case *ast.CompositeLit:
// Slice{}?
if len(e.Elts) == 0 {
return true, true
}
}
return false, false
}