gopls/internal/analysis/modernize: appendclipped: unclip
The appendclipped pass must ascertain that the first argument
to append(x, y...) is clipped, so that we don't eliminate
possible intended side effects on x, but in some cases:
- append(x[:len(x):len(x)], y...)
- append(slices.Clip(x), y...)
we can further simplify the first argument to its unclipped
version (just x in both cases), so that the result is:
slices.Concat(x, y)
+ test
Fixes golang/go#71296
Change-Id: I89cc4350b5dbd57c88c35c0b4459b23347814441
Reviewed-on: https://go-review.googlesource.com/c/tools/+/647796
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/analysis/modernize/modernize.go b/gopls/internal/analysis/modernize/modernize.go
index 861194e..0f7b58e 100644
--- a/gopls/internal/analysis/modernize/modernize.go
+++ b/gopls/internal/analysis/modernize/modernize.go
@@ -130,6 +130,7 @@
builtinAppend = types.Universe.Lookup("append")
builtinBool = types.Universe.Lookup("bool")
builtinFalse = types.Universe.Lookup("false")
+ builtinLen = types.Universe.Lookup("len")
builtinMake = types.Universe.Lookup("make")
builtinNil = types.Universe.Lookup("nil")
builtinTrue = types.Universe.Lookup("true")
diff --git a/gopls/internal/analysis/modernize/slices.go b/gopls/internal/analysis/modernize/slices.go
index aada97d..bdab9de 100644
--- a/gopls/internal/analysis/modernize/slices.go
+++ b/gopls/internal/analysis/modernize/slices.go
@@ -52,18 +52,21 @@
// 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 {
+ clipped, empty := clippedSlice(info, base)
+ if clipped == nil {
return
}
// If the (clipped) base is empty, it may be safely ignored.
- // Otherwise treat it as just another arg (the first) to Concat.
+ // Otherwise treat it (or its unclipped subexpression, if possible)
+ // as just another arg (the first) to Concat.
if !empty {
- sliceArgs = append(sliceArgs, base)
+ sliceArgs = append(sliceArgs, clipped)
}
slices.Reverse(sliceArgs)
+ // TODO(adonovan): simplify sliceArgs[0] further: slices.Clone(s) -> s
+
// Concat of a single (non-trivial) slice degenerates to Clone.
if len(sliceArgs) == 1 {
s := sliceArgs[0]
@@ -111,11 +114,6 @@
}
// 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(),
@@ -172,25 +170,36 @@
}
}
-// isClippedSlice reports whether e denotes a slice that is definitely
-// clipped, that is, its len(s)==cap(s).
+// clippedSlice returns res != nil if 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.
+// The value of res is either the same as e or is a subexpression of e
+// that denotes the same slice but without the clipping operation.
+//
+// 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[:len(x):len(x)] (nonempty) res=x
// x[:k:k] (nonempty)
-// slices.Clip(x) (nonempty)
-func isClippedSlice(info *types.Info, e ast.Expr) (clipped, empty bool) {
+// slices.Clip(x) (nonempty) res=x
+func clippedSlice(info *types.Info, e ast.Expr) (res ast.Expr, 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:*]
+ // x[:0:0], x[:len(x):len(x)], x[:k:k]
+ if e.Slice3 && e.High != nil && e.Max != nil && equalSyntax(e.High, e.Max) { // x[:k:k]
+ res = e
+ empty = isZeroLiteral(e.High) // x[:0:0]
+ if call, ok := e.High.(*ast.CallExpr); ok &&
+ typeutil.Callee(info, call) == builtinLen &&
+ equalSyntax(call.Args[0], e.X) {
+ res = e.X // x[:len(x):len(x)] -> x
+ }
+ return
+ }
return
case *ast.CallExpr:
@@ -198,20 +207,20 @@
if info.Types[e.Fun].IsType() &&
is[*ast.Ident](e.Args[0]) &&
info.Uses[e.Args[0].(*ast.Ident)] == builtinNil {
- return true, true
+ return e, true
}
// slices.Clip(x)?
obj := typeutil.Callee(info, e)
if analysisinternal.IsFunctionNamed(obj, "slices", "Clip") {
- return true, false
+ return e.Args[0], false // slices.Clip(x) -> x
}
case *ast.CompositeLit:
// Slice{}?
if len(e.Elts) == 0 {
- return true, true
+ return e, true
}
}
- return false, false
+ return nil, false
}
diff --git a/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go.golden b/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go.golden
index 5d6761b..6352d52 100644
--- a/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go.golden
@@ -20,7 +20,7 @@
print(slices.Concat(Bytes{1, 2, 3}, Bytes{4, 5, 6})) // want "Replace append with slices.Concat"
print(slices.Concat(s, other, other)) // want "Replace append with slices.Concat"
print(slices.Concat(os.Environ(), other, other)) // want "Replace append with slices.Concat"
- print(slices.Concat(other[:len(other):len(other)], s, other)) // want "Replace append with slices.Concat"
- print(slices.Concat(slices.Clip(other), s, other)) // want "Replace append with slices.Concat"
+ print(slices.Concat(other, s, other)) // want "Replace append with slices.Concat"
+ print(slices.Concat(other, s, other)) // want "Replace append with slices.Concat"
print(append(append(append(other[:0], s...), other...), other...)) // nope: intent may be to mutate other
}