| // 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) |
| } |