blob: ca862863c9e54ce672244f020ca39ff948f182f8 [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
import (
"go/ast"
"go/constant"
"go/token"
"go/types"
"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/internal/analysisinternal"
)
// slices.Delete is not equivalent to append(s[:i], [j:]...):
// it clears the vacated array slots; see #73686.
// Until we either fix it or revise our safety goals,
// we disable this analyzer for now.
//
// Its former documentation in doc.go was:
//
// - slicesdelete: replace append(s[:i], s[i+1]...) by
// slices.Delete(s, i, i+1), added in go1.21.
var EnableSlicesDelete = false
// The slicesdelete pass attempts to replace instances of append(s[:i], s[i+k:]...)
// with slices.Delete(s, i, i+k) where k is some positive constant.
// Other variations that will also have suggested replacements include:
// append(s[:i-1], s[i:]...) and append(s[:i+k1], s[i+k2:]) where k2 > k1.
func slicesdelete(pass *analysis.Pass) {
if !EnableSlicesDelete {
return
}
// Skip the analyzer in packages where its
// fixes would create an import cycle.
if within(pass, "slices", "runtime") {
return
}
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
info := pass.TypesInfo
report := func(file *ast.File, call *ast.CallExpr, slice1, slice2 *ast.SliceExpr) {
insert := func(pos token.Pos, text string) analysis.TextEdit {
return analysis.TextEdit{Pos: pos, End: pos, NewText: []byte(text)}
}
isIntExpr := func(e ast.Expr) bool {
return types.Identical(types.Default(info.TypeOf(e)), builtinInt.Type())
}
isIntShadowed := func() bool {
scope := pass.TypesInfo.Scopes[file].Innermost(call.Lparen)
if _, obj := scope.LookupParent("int", call.Lparen); obj != builtinInt {
return true // int type is shadowed
}
return false
}
_, prefix, edits := analysisinternal.AddImport(info, file, "slices", "slices", "Delete", call.Pos())
// append's indices may be any integer type; slices.Delete requires int.
// Insert int conversions as needed (and if possible).
if isIntShadowed() && (!isIntExpr(slice1.High) || !isIntExpr(slice2.Low)) {
return
}
if !isIntExpr(slice1.High) {
edits = append(edits,
insert(slice1.High.Pos(), "int("),
insert(slice1.High.End(), ")"),
)
}
if !isIntExpr(slice2.Low) {
edits = append(edits,
insert(slice2.Low.Pos(), "int("),
insert(slice2.Low.End(), ")"),
)
}
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
Category: "slicesdelete",
Message: "Replace append with slices.Delete",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace append with slices.Delete",
TextEdits: append(edits, []analysis.TextEdit{
// Change name of called function.
{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
NewText: []byte(prefix + "Delete"),
},
// Delete ellipsis.
{
Pos: call.Ellipsis,
End: call.Ellipsis + token.Pos(len("...")), // delete ellipsis
},
// Remove second slice variable name.
{
Pos: slice2.X.Pos(),
End: slice2.X.End(),
},
// Insert after first slice variable name.
{
Pos: slice1.X.End(),
NewText: []byte(", "),
},
// Remove brackets and colons.
{
Pos: slice1.Lbrack,
End: slice1.High.Pos(),
},
{
Pos: slice1.Rbrack,
End: slice1.Rbrack + 1,
},
{
Pos: slice2.Lbrack,
End: slice2.Lbrack + 1,
},
{
Pos: slice2.Low.End(),
End: slice2.Rbrack + 1,
},
}...),
}},
})
}
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 id, ok := call.Fun.(*ast.Ident); ok && len(call.Args) == 2 {
// Verify we have append with two slices and ... operator,
// the first slice has no low index and second slice has no
// high index, and not a three-index slice.
if call.Ellipsis.IsValid() && info.Uses[id] == builtinAppend {
slice1, ok1 := call.Args[0].(*ast.SliceExpr)
slice2, ok2 := call.Args[1].(*ast.SliceExpr)
if ok1 && slice1.Low == nil && !slice1.Slice3 &&
ok2 && slice2.High == nil && !slice2.Slice3 &&
equalSyntax(slice1.X, slice2.X) && noEffects(info, slice1.X) &&
increasingSliceIndices(info, slice1.High, slice2.Low) {
// Have append(s[:a], s[b:]...) where we can verify a < b.
report(file, call, slice1, slice2)
}
}
}
}
}
}
// Given two slice indices a and b, returns true if we can verify that a < b.
// It recognizes certain forms such as i+k1 < i+k2 where k1 < k2.
func increasingSliceIndices(info *types.Info, a, b ast.Expr) bool {
// Given an expression of the form i±k, returns (i, k)
// where k is a signed constant. Otherwise it returns (e, 0).
split := func(e ast.Expr) (ast.Expr, constant.Value) {
if binary, ok := e.(*ast.BinaryExpr); ok && (binary.Op == token.SUB || binary.Op == token.ADD) {
// Negate constants if operation is subtract instead of add
if k := info.Types[binary.Y].Value; k != nil {
return binary.X, constant.UnaryOp(binary.Op, k, 0) // i ± k
}
}
return e, constant.MakeInt64(0)
}
// Handle case where either a or b is a constant
ak := info.Types[a].Value
bk := info.Types[b].Value
if ak != nil || bk != nil {
return ak != nil && bk != nil && constant.Compare(ak, token.LSS, bk)
}
ai, ak := split(a)
bi, bk := split(b)
return equalSyntax(ai, bi) && constant.Compare(ak, token.LSS, bk)
}