go/analysis/passes/modernize: add slicesclip analyzer The new analyzer suggests replacing full slice expressions of the form x[:len(x):len(x)], which clip a slice's capacity to its length, with the equivalent and more readable slices.Clip(x), added in Go 1.21. Fixes golang/go#80438 Change-Id: I1590af6f415b603f4bf737fb3ecd3f7f75aa3219 Reviewed-on: https://go-review.googlesource.com/c/tools/+/805980 Auto-Submit: Alan Donovan <adonovan@google.com> Reviewed-by: Alex Putman <aputman@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/go/analysis/passes/modernize/doc.go b/go/analysis/passes/modernize/doc.go index e441f8d..954bc96 100644 --- a/go/analysis/passes/modernize/doc.go +++ b/go/analysis/passes/modernize/doc.go
@@ -384,6 +384,22 @@ for i, v := range slices.Backward(s) { ... } +# Analyzer slicesclip + +slicesclip: replace three-index slice expressions with slices.Clip + +The slicesclip analyzer suggests replacing a full slice expression of +the form + + x[:len(x):len(x)] + +which clips the capacity of a slice to its length, with the simpler +and more readable + + slices.Clip(x) + +added in Go 1.21. + # Analyzer slicescontains slicescontains: replace loops with slices.Contains or slices.ContainsFunc
diff --git a/go/analysis/passes/modernize/export_test.go b/go/analysis/passes/modernize/export_test.go index 1ca7539..b873454 100644 --- a/go/analysis/passes/modernize/export_test.go +++ b/go/analysis/passes/modernize/export_test.go
@@ -10,5 +10,6 @@ ImportCommentAnalyzer = importCommentAnalyzer ReflectTypeAssertAnalyzer = reflectTypeAssertAnalyzer SlicesBackwardAnalyzer = slicesBackwardAnalyzer + SlicesClipAnalyzer = slicesClipAnalyzer UnsafeFuncsAnalyzer = unsafeFuncsAnalyzer )
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go index 0483a37..9e950d7 100644 --- a/go/analysis/passes/modernize/modernize.go +++ b/go/analysis/passes/modernize/modernize.go
@@ -48,6 +48,7 @@ reflectTypeAssertAnalyzer, // awaiting public symbol ReflectTypeForAnalyzer, slicesBackwardAnalyzer, // awaiting public symbol + slicesClipAnalyzer, // awaiting public symbol SlicesContainsAnalyzer, SlicesSortAnalyzer, StdIteratorsAnalyzer,
diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go index c0d2edd..37060dc 100644 --- a/go/analysis/passes/modernize/modernize_test.go +++ b/go/analysis/passes/modernize/modernize_test.go
@@ -109,6 +109,10 @@ RunWithSuggestedFixes(t, TestData(), modernize.SlicesBackwardAnalyzer, "slicesbackward") } +func TestSlicesClip(t *testing.T) { + RunWithSuggestedFixes(t, TestData(), modernize.SlicesClipAnalyzer, "slicesclip") +} + func TestSlicesContains(t *testing.T) { RunWithSuggestedFixes(t, TestData(), modernize.SlicesContainsAnalyzer, "slicescontains") }
diff --git a/go/analysis/passes/modernize/slicesclip.go b/go/analysis/passes/modernize/slicesclip.go new file mode 100644 index 0000000..b08cbc6 --- /dev/null +++ b/go/analysis/passes/modernize/slicesclip.go
@@ -0,0 +1,81 @@ +// Copyright 2026 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 ( + "fmt" + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" +) + +var slicesClipAnalyzer = &analysis.Analyzer{ + Name: "slicesclip", + Doc: analyzerutil.MustExtractDoc(doc, "slicesclip"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + }, + Run: slicesclip, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesclip", +} + +func slicesclip(pass *analysis.Pass) (any, error) { + if within(pass, "slices", "runtime") { + return nil, nil + } + info := pass.TypesInfo + + // isLenX reports whether e is a call len(x) where x is + // syntactically identical to the operand x of the slice expr. + isLenX := func(e, x ast.Expr) bool { + call, ok := e.(*ast.CallExpr) + if !ok || len(call.Args) != 1 { + return false + } + return typeutil.Callee(info, call) == builtinLen && + astutil.EqualSyntax(call.Args[0], x) + } + + for curFile := range filesUsingGoVersion(pass, versions.Go1_21) { + file := curFile.Node().(*ast.File) + + for curSlice := range curFile.Preorder((*ast.SliceExpr)(nil)) { + slice := curSlice.Node().(*ast.SliceExpr) + _, ok := info.TypeOf(slice.X).Underlying().(*types.Slice) // in case x is an array/pointer to array + if !slice.Slice3 || slice.Low != nil || !ok { + continue + } + + if isLenX(slice.High, slice.X) && isLenX(slice.Max, slice.X) && typesinternal.NoEffects(info, slice.X) { + // Have x[:len(x):len(x)] -> slices.Clip(x) + prefix, edits := refactor.AddImport(info, file, "slices", "slices", "Clip", slice.Pos()) + sx := astutil.Format(pass.Fset, slice.X) + pass.Report(analysis.Diagnostic{ + Pos: slice.Pos(), + End: slice.End(), + Message: "x[:len(x):len(x)] can be simplified using slices.Clip", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace with slices.Clip(%s)", sx), + TextEdits: append(edits, analysis.TextEdit{ + Pos: slice.Pos(), + End: slice.End(), + NewText: fmt.Appendf(nil, "%sClip(%s)", prefix, sx), + }), + }}, + }) + } + } + } + + return nil, nil +}
diff --git a/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go b/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go new file mode 100644 index 0000000..845a831 --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go
@@ -0,0 +1,46 @@ +package slicesclip + +var g struct{ f []int } + +func h() []int { return []int{} } + +var ch chan []int + +func _(test, other []byte, i int) { + _ = test[:len(test):len(test)] // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = test[1:len(test):len(test)] // non-zero low index: no match + + _ = test[:len(test)] // ordinary two-index slice: no match + + _ = test[:len(other):len(other)] // different slice variable: no match + + _ = test[:len(test):len(other)] // mismatched high/max: no match + + _ = g.f[:len(g.f):len(g.f)] // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = h()[:len(h()):len(h())] // potentially has side effects: no match + + _ = (<-ch)[:len(<-ch):len(<-ch)] // has side effects: no match + + if len(test) > 0 { + test = test[:len(test):len(test)] // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + } + + _ = append(other, test[:len(test):len(test)]...) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = i +} + +func shadowed(test []byte) { + len := func(_ []byte) int { return 0 } + _ = test[:len(test):len(test)] // len is shadowed: no match +} + +func arrayCase() { + var a [3]int + _ = a[:len(a):len(a)] // array, not slice: no match + + pa := &a + _ = pa[:len(pa):len(pa)] // pointer to array, not slice: no match +}
diff --git a/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go.golden b/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go.golden new file mode 100644 index 0000000..15d5b45 --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go.golden
@@ -0,0 +1,48 @@ +package slicesclip + +import "slices" + +var g struct{ f []int } + +func h() []int { return []int{} } + +var ch chan []int + +func _(test, other []byte, i int) { + _ = slices.Clip(test) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = test[1:len(test):len(test)] // non-zero low index: no match + + _ = test[:len(test)] // ordinary two-index slice: no match + + _ = test[:len(other):len(other)] // different slice variable: no match + + _ = test[:len(test):len(other)] // mismatched high/max: no match + + _ = slices.Clip(g.f) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = h()[:len(h()):len(h())] // potentially has side effects: no match + + _ = (<-ch)[:len(<-ch):len(<-ch)] // has side effects: no match + + if len(test) > 0 { + test = slices.Clip(test) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + } + + _ = append(other, slices.Clip(test)...) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = i +} + +func shadowed(test []byte) { + len := func(_ []byte) int { return 0 } + _ = test[:len(test):len(test)] // len is shadowed: no match +} + +func arrayCase() { + var a [3]int + _ = a[:len(a):len(a)] // array, not slice: no match + + pa := &a + _ = pa[:len(pa):len(pa)] // pointer to array, not slice: no match +}
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index c4cf653..c5a7987 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md
@@ -4066,6 +4066,24 @@ Package documentation: [slicesbackward](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesbackward) +<a id='slicesclip'></a> +## `slicesclip`: replace three-index slice expressions with slices.Clip + +The slicesclip analyzer suggests replacing a full slice expression of the form + + x[:len(x):len(x)] + +which clips the capacity of a slice to its length, with the simpler and more readable + + slices.Clip(x) + +added in Go 1.21. + + +Default: on. + +Package documentation: [slicesclip](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesclip) + <a id='slicescontains'></a> ## `slicescontains`: replace loops with slices.Contains or slices.ContainsFunc
diff --git a/gopls/doc/release/v0.24.0.md b/gopls/doc/release/v0.24.0.md index f696365..9951ac8 100644 --- a/gopls/doc/release/v0.24.0.md +++ b/gopls/doc/release/v0.24.0.md
@@ -51,4 +51,12 @@ pointer type `*E` as an error. <!-- #80159 --> +### `slicesclip` modernizer + +The new `slicesclip` modernizer suggests replacing a full slice +expression of the form `x[:len(x):len(x)]`, which clips the capacity +of a slice to its length, with the simpler and more readable +`slices.Clip(x)`, added in Go 1.21. +<!-- #80438 --> + ## Code transformation features
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index c919cb0..bf78d30 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json
@@ -1701,6 +1701,12 @@ "Status": "" }, { + "Name": "\"slicesclip\"", + "Doc": "replace three-index slice expressions with slices.Clip\n\nThe slicesclip analyzer suggests replacing a full slice expression of\nthe form\n\n\tx[:len(x):len(x)]\n\nwhich clips the capacity of a slice to its length, with the simpler\nand more readable\n\n\tslices.Clip(x)\n\nadded in Go 1.21.", + "Default": "true", + "Status": "" + }, + { "Name": "\"slicescontains\"", "Doc": "replace loops with slices.Contains or slices.ContainsFunc\n\nThe slicescontains analyzer simplifies loops that check for the existence of\nan element in a slice. It replaces them with calls to `slices.Contains` or\n`slices.ContainsFunc`, which were added in Go 1.21.\n\nIf the expression for the target element has side effects, this\ntransformation will cause those effects to occur only once, not\nonce per tested slice element.", "Default": "true", @@ -3762,6 +3768,12 @@ "Default": true }, { + "Name": "slicesclip", + "Doc": "replace three-index slice expressions with slices.Clip\n\nThe slicesclip analyzer suggests replacing a full slice expression of\nthe form\n\n\tx[:len(x):len(x)]\n\nwhich clips the capacity of a slice to its length, with the simpler\nand more readable\n\n\tslices.Clip(x)\n\nadded in Go 1.21.", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesclip", + "Default": true + }, + { "Name": "slicescontains", "Doc": "replace loops with slices.Contains or slices.ContainsFunc\n\nThe slicescontains analyzer simplifies loops that check for the existence of\nan element in a slice. It replaces them with calls to `slices.Contains` or\n`slices.ContainsFunc`, which were added in Go 1.21.\n\nIf the expression for the target element has side effects, this\ntransformation will cause those effects to occur only once, not\nonce per tested slice element.", "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicescontains",