diff --git a/blog/blog.go b/blog/blog.go
index 947c60e95a2..901b53f440e 100644
--- a/blog/blog.go
+++ b/blog/blog.go
@@ -420,7 +420,7 @@ type rootData struct {
BasePath string
GodocURL string
AnalyticsHTML template.HTML
- Data interface{}
+ Data any
}
// ServeHTTP serves the front, index, and article pages
diff --git a/cmd/auth/go.mod b/cmd/auth/go.mod
new file mode 100644
index 00000000000..ea912ce7743
--- /dev/null
+++ b/cmd/auth/go.mod
@@ -0,0 +1,3 @@
+module golang.org/x/tools/cmd/auth
+
+go 1.23.0
diff --git a/cmd/bundle/gotypesalias.go b/cmd/bundle/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/bundle/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/callgraph/gotypesalias.go b/cmd/callgraph/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/callgraph/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/callgraph/main.go b/cmd/callgraph/main.go
index 9e440bbafb9..e489de883d0 100644
--- a/cmd/callgraph/main.go
+++ b/cmd/callgraph/main.go
@@ -148,10 +148,7 @@ func init() {
// If $GOMAXPROCS isn't set, use the full capacity of the machine.
// For small machines, use at least 4 threads.
if os.Getenv("GOMAXPROCS") == "" {
- n := runtime.NumCPU()
- if n < 4 {
- n = 4
- }
+ n := max(runtime.NumCPU(), 4)
runtime.GOMAXPROCS(n)
}
}
diff --git a/cmd/callgraph/main_test.go b/cmd/callgraph/main_test.go
index ce634139e68..3b56cd7ffef 100644
--- a/cmd/callgraph/main_test.go
+++ b/cmd/callgraph/main_test.go
@@ -5,7 +5,6 @@
// No testdata on Android.
//go:build !android && go1.11
-// +build !android,go1.11
package main
diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go
index f129102cc4c..e164dc22ba8 100644
--- a/cmd/deadcode/deadcode.go
+++ b/cmd/deadcode/deadcode.go
@@ -15,11 +15,13 @@ import (
"go/types"
"io"
"log"
+ "maps"
"os"
"path/filepath"
"regexp"
"runtime"
"runtime/pprof"
+ "slices"
"sort"
"strings"
"text/template"
@@ -175,7 +177,7 @@ func main() {
}
}
- if isGenerated(file) {
+ if ast.IsGenerated(file) {
generated[p.Fset.File(file.Pos()).Name()] = true
}
}
@@ -290,9 +292,7 @@ func main() {
// Build array of jsonPackage objects.
var packages []any
- pkgpaths := keys(byPkgPath)
- sort.Strings(pkgpaths)
- for _, pkgpath := range pkgpaths {
+ for _, pkgpath := range slices.Sorted(maps.Keys(byPkgPath)) {
if !filter.MatchString(pkgpath) {
continue
}
@@ -303,7 +303,7 @@ func main() {
// declaration order. This tends to keep related
// methods such as (T).Marshal and (*T).Unmarshal
// together better than sorting.
- fns := keys(m)
+ fns := slices.Collect(maps.Keys(m))
sort.Slice(fns, func(i, j int) bool {
xposn := prog.Fset.Position(fns[i].Pos())
yposn := prog.Fset.Position(fns[j].Pos())
@@ -368,7 +368,7 @@ func prettyName(fn *ssa.Function, qualified bool) string {
// anonymous?
if fn.Parent() != nil {
format(fn.Parent())
- i := index(fn.Parent().AnonFuncs, fn)
+ i := slices.Index(fn.Parent().AnonFuncs, fn)
fmt.Fprintf(&buf, "$%d", i+1)
return
}
@@ -414,45 +414,6 @@ func printObjects(format string, objects []any) {
}
}
-// TODO(adonovan): use go1.21's ast.IsGenerated.
-
-// isGenerated reports whether the file was generated by a program,
-// not handwritten, by detecting the special comment described
-// at https://go.dev/s/generatedcode.
-//
-// The syntax tree must have been parsed with the ParseComments flag.
-// Example:
-//
-// f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.PackageClauseOnly)
-// if err != nil { ... }
-// gen := ast.IsGenerated(f)
-func isGenerated(file *ast.File) bool {
- _, ok := generator(file)
- return ok
-}
-
-func generator(file *ast.File) (string, bool) {
- for _, group := range file.Comments {
- for _, comment := range group.List {
- if comment.Pos() > file.Package {
- break // after package declaration
- }
- // opt: check Contains first to avoid unnecessary array allocation in Split.
- const prefix = "// Code generated "
- if strings.Contains(comment.Text, prefix) {
- for _, line := range strings.Split(comment.Text, "\n") {
- if rest, ok := strings.CutPrefix(line, prefix); ok {
- if gen, ok := strings.CutSuffix(rest, " DO NOT EDIT."); ok {
- return gen, true
- }
- }
- }
- }
- }
- }
- return "", false
-}
-
// pathSearch returns the shortest path from one of the roots to one
// of the targets (along with the root itself), or zero if no path was found.
func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) {
@@ -466,7 +427,7 @@ func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Functio
// Sort roots into preferred order.
importsTesting := func(fn *ssa.Function) bool {
isTesting := func(p *types.Package) bool { return p.Path() == "testing" }
- return containsFunc(fn.Pkg.Pkg.Imports(), isTesting)
+ return slices.ContainsFunc(fn.Pkg.Pkg.Imports(), isTesting)
}
sort.Slice(roots, func(i, j int) bool {
x, y := roots[i], roots[j]
@@ -500,7 +461,7 @@ func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Functio
for {
edge := seen[node]
if edge == nil {
- reverse(path)
+ slices.Reverse(path)
return path
}
path = append(path, edge)
@@ -604,43 +565,3 @@ type jsonPosition struct {
func (p jsonPosition) String() string {
return fmt.Sprintf("%s:%d:%d", p.File, p.Line, p.Col)
}
-
-// -- from the future --
-
-// TODO(adonovan): use go1.22's slices and maps packages.
-
-func containsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
- return indexFunc(s, f) >= 0
-}
-
-func indexFunc[S ~[]E, E any](s S, f func(E) bool) int {
- for i := range s {
- if f(s[i]) {
- return i
- }
- }
- return -1
-}
-
-func index[S ~[]E, E comparable](s S, v E) int {
- for i := range s {
- if v == s[i] {
- return i
- }
- }
- return -1
-}
-
-func reverse[S ~[]E, E any](s S) {
- for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
- s[i], s[j] = s[j], s[i]
- }
-}
-
-func keys[M ~map[K]V, K comparable, V any](m M) []K {
- r := make([]K, 0, len(m))
- for k := range m {
- r = append(r, k)
- }
- return r
-}
diff --git a/cmd/deadcode/deadcode_test.go b/cmd/deadcode/deadcode_test.go
index 90c067331dc..a9b8327c7d7 100644
--- a/cmd/deadcode/deadcode_test.go
+++ b/cmd/deadcode/deadcode_test.go
@@ -34,7 +34,6 @@ func Test(t *testing.T) {
t.Fatal(err)
}
for _, filename := range matches {
- filename := filename
t.Run(filename, func(t *testing.T) {
t.Parallel()
diff --git a/cmd/deadcode/gotypesalias.go b/cmd/deadcode/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/deadcode/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/eg/gotypesalias.go b/cmd/eg/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/eg/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/file2fuzz/main.go b/cmd/file2fuzz/main.go
index 2a86c2ece88..f9d4708cd28 100644
--- a/cmd/file2fuzz/main.go
+++ b/cmd/file2fuzz/main.go
@@ -34,7 +34,7 @@ import (
var encVersion1 = "go test fuzz v1"
func encodeByteSlice(b []byte) []byte {
- return []byte(fmt.Sprintf("%s\n[]byte(%q)", encVersion1, b))
+ return fmt.Appendf(nil, "%s\n[]byte(%q)", encVersion1, b)
}
func usage() {
diff --git a/cmd/fiximports/main_test.go b/cmd/fiximports/main_test.go
index ebbd7520d2e..69f8726f135 100644
--- a/cmd/fiximports/main_test.go
+++ b/cmd/fiximports/main_test.go
@@ -5,7 +5,6 @@
// No testdata on Android.
//go:build !android
-// +build !android
package main
diff --git a/cmd/go-contrib-init/contrib.go b/cmd/go-contrib-init/contrib.go
index 9254b86388f..0ab93c90f73 100644
--- a/cmd/go-contrib-init/contrib.go
+++ b/cmd/go-contrib-init/contrib.go
@@ -160,44 +160,6 @@ GOPATH: %s
}
return
}
-
- gopath := firstGoPath()
- if gopath == "" {
- log.Fatal("Your GOPATH is not set, please set it")
- }
-
- rightdir := filepath.Join(gopath, "src", "golang.org", "x", *repo)
- if !strings.HasPrefix(wd, rightdir) {
- dirExists, err := exists(rightdir)
- if err != nil {
- log.Fatal(err)
- }
- if !dirExists {
- log.Fatalf("The repo you want to work on is currently not on your system.\n"+
- "Run %q to obtain this repo\n"+
- "then go to the directory %q\n",
- "go get -d golang.org/x/"+*repo, rightdir)
- }
- log.Fatalf("Your current directory is:%q\n"+
- "Working on golang/x/%v requires you be in %q\n",
- wd, *repo, rightdir)
- }
-}
-
-func firstGoPath() string {
- list := filepath.SplitList(build.Default.GOPATH)
- if len(list) < 1 {
- return ""
- }
- return list[0]
-}
-
-func exists(path string) (bool, error) {
- _, err := os.Stat(path)
- if os.IsNotExist(err) {
- return false, nil
- }
- return true, err
}
func inGoPath(wd string) bool {
diff --git a/cmd/godex/gotypesalias.go b/cmd/godex/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/godex/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/godex/isAlias18.go b/cmd/godex/isAlias18.go
index 431602b2243..f1f78731d4c 100644
--- a/cmd/godex/isAlias18.go
+++ b/cmd/godex/isAlias18.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !go1.9
-// +build !go1.9
package main
diff --git a/cmd/godex/isAlias19.go b/cmd/godex/isAlias19.go
index e5889119fa1..db29555fd8c 100644
--- a/cmd/godex/isAlias19.go
+++ b/cmd/godex/isAlias19.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.9
-// +build go1.9
package main
diff --git a/cmd/godex/writetype.go b/cmd/godex/writetype.go
index 866f718f05f..f59760a81c6 100644
--- a/cmd/godex/writetype.go
+++ b/cmd/godex/writetype.go
@@ -14,6 +14,7 @@ package main
import (
"go/types"
+ "slices"
)
func (p *printer) writeType(this *types.Package, typ types.Type) {
@@ -28,11 +29,9 @@ func (p *printer) writeTypeInternal(this *types.Package, typ types.Type, visited
// practice deeply nested composite types with unnamed component
// types are uncommon. This code is likely more efficient than
// using a map.
- for _, t := range visited {
- if t == typ {
- p.printf("○%T", typ) // cycle to typ
- return
- }
+ if slices.Contains(visited, typ) {
+ p.printf("○%T", typ) // cycle to typ
+ return
}
visited = append(visited, typ)
@@ -72,7 +71,7 @@ func (p *printer) writeTypeInternal(this *types.Package, typ types.Type, visited
p.print("struct {\n")
p.indent++
- for i := 0; i < n; i++ {
+ for i := range n {
f := t.Field(i)
if !f.Anonymous() {
p.printf("%s ", f.Name())
@@ -120,7 +119,7 @@ func (p *printer) writeTypeInternal(this *types.Package, typ types.Type, visited
if GcCompatibilityMode {
// print flattened interface
// (useful to compare against gc-generated interfaces)
- for i := 0; i < n; i++ {
+ for i := range n {
m := t.Method(i)
p.print(m.Name())
p.writeSignatureInternal(this, m.Type().(*types.Signature), visited)
diff --git a/cmd/godoc/godoc_test.go b/cmd/godoc/godoc_test.go
index 66b93f10630..7cd38574233 100644
--- a/cmd/godoc/godoc_test.go
+++ b/cmd/godoc/godoc_test.go
@@ -16,6 +16,7 @@ import (
"os/exec"
"regexp"
"runtime"
+ "slices"
"strings"
"sync"
"testing"
@@ -127,12 +128,7 @@ func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse
// hasTag checks whether a given release tag is contained in the current version
// of the go binary.
func hasTag(t string) bool {
- for _, v := range build.Default.ReleaseTags {
- if t == v {
- return true
- }
- }
- return false
+ return slices.Contains(build.Default.ReleaseTags, t)
}
func TestURL(t *testing.T) {
diff --git a/cmd/godoc/gotypesalias.go b/cmd/godoc/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/godoc/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go
index a665be0769d..1bce091f269 100644
--- a/cmd/godoc/main.go
+++ b/cmd/godoc/main.go
@@ -114,7 +114,7 @@ func loggingHandler(h http.Handler) http.Handler {
func handleURLFlag() {
// Try up to 10 fetches, following redirects.
urlstr := *urlFlag
- for i := 0; i < 10; i++ {
+ for range 10 {
// Prepare request.
u, err := url.Parse(urlstr)
if err != nil {
diff --git a/cmd/goimports/goimports.go b/cmd/goimports/goimports.go
index dcb5023a2e7..11f56e0e865 100644
--- a/cmd/goimports/goimports.go
+++ b/cmd/goimports/goimports.go
@@ -361,8 +361,8 @@ func replaceTempFilename(diff []byte, filename string) ([]byte, error) {
}
// Always print filepath with slash separator.
f := filepath.ToSlash(filename)
- bs[0] = []byte(fmt.Sprintf("--- %s%s", f+".orig", t0))
- bs[1] = []byte(fmt.Sprintf("+++ %s%s", f, t1))
+ bs[0] = fmt.Appendf(nil, "--- %s%s", f+".orig", t0)
+ bs[1] = fmt.Appendf(nil, "+++ %s%s", f, t1)
return bytes.Join(bs, []byte{'\n'}), nil
}
diff --git a/cmd/goimports/goimports_gc.go b/cmd/goimports/goimports_gc.go
index 3326646d035..3a88482fe8d 100644
--- a/cmd/goimports/goimports_gc.go
+++ b/cmd/goimports/goimports_gc.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build gc
-// +build gc
package main
diff --git a/cmd/goimports/goimports_not_gc.go b/cmd/goimports/goimports_not_gc.go
index 344fe7576b0..21dc77920be 100644
--- a/cmd/goimports/goimports_not_gc.go
+++ b/cmd/goimports/goimports_not_gc.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !gc
-// +build !gc
package main
diff --git a/cmd/goimports/gotypesalias.go b/cmd/goimports/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/goimports/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/gomvpkg/gotypesalias.go b/cmd/gomvpkg/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/gomvpkg/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/gotype/gotypesalias.go b/cmd/gotype/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/gotype/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/gotype/sizesFor18.go b/cmd/gotype/sizesFor18.go
index 39e3d9f047e..15d2355ca42 100644
--- a/cmd/gotype/sizesFor18.go
+++ b/cmd/gotype/sizesFor18.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !go1.9
-// +build !go1.9
// This file contains a copy of the implementation of types.SizesFor
// since this function is not available in go/types before Go 1.9.
diff --git a/cmd/gotype/sizesFor19.go b/cmd/gotype/sizesFor19.go
index 34181c8d04d..c46bb777024 100644
--- a/cmd/gotype/sizesFor19.go
+++ b/cmd/gotype/sizesFor19.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.9
-// +build go1.9
package main
diff --git a/cmd/goyacc/yacc.go b/cmd/goyacc/yacc.go
index 965a76f14dc..be084da3690 100644
--- a/cmd/goyacc/yacc.go
+++ b/cmd/goyacc/yacc.go
@@ -1478,7 +1478,7 @@ func symnam(i int) string {
// set elements 0 through n-1 to c
func aryfil(v []int, n, c int) {
- for i := 0; i < n; i++ {
+ for i := range n {
v[i] = c
}
}
@@ -1840,7 +1840,7 @@ func closure(i int) {
nexts:
// initially fill the sets
- for s := 0; s < n; s++ {
+ for s := range n {
prd := curres[s]
//
@@ -2609,7 +2609,7 @@ func callopt() {
if adb > 2 {
for p = 0; p <= maxa; p += 10 {
fmt.Fprintf(ftable, "%v ", p)
- for i = 0; i < 10; i++ {
+ for i = range 10 {
fmt.Fprintf(ftable, "%v ", amem[p+i])
}
ftable.WriteRune('\n')
@@ -2653,7 +2653,7 @@ func gin(i int) {
// now, find amem place for it
nextgp:
- for p := 0; p < ACTSIZE; p++ {
+ for p := range ACTSIZE {
if amem[p] != 0 {
continue
}
@@ -3117,7 +3117,7 @@ func aryeq(a []int, b []int) int {
if len(b) != n {
return 0
}
- for ll := 0; ll < n; ll++ {
+ for ll := range n {
if a[ll] != b[ll] {
return 0
}
diff --git a/cmd/html2article/conv.go b/cmd/html2article/conv.go
index 604bb1fd7cd..e2946431ce2 100644
--- a/cmd/html2article/conv.go
+++ b/cmd/html2article/conv.go
@@ -16,6 +16,7 @@ import (
"net/url"
"os"
"regexp"
+ "slices"
"strings"
"golang.org/x/net/html"
@@ -270,10 +271,8 @@ func hasClass(name string) selector {
return func(n *html.Node) bool {
for _, a := range n.Attr {
if a.Key == "class" {
- for _, c := range strings.Fields(a.Val) {
- if c == name {
- return true
- }
+ if slices.Contains(strings.Fields(a.Val), name) {
+ return true
}
}
}
diff --git a/cmd/present/main.go b/cmd/present/main.go
index 340025276f9..99ed838e926 100644
--- a/cmd/present/main.go
+++ b/cmd/present/main.go
@@ -73,8 +73,8 @@ func main() {
origin := &url.URL{Scheme: "http"}
if *originHost != "" {
- if strings.HasPrefix(*originHost, "https://") {
- *originHost = strings.TrimPrefix(*originHost, "https://")
+ if after, ok := strings.CutPrefix(*originHost, "https://"); ok {
+ *originHost = after
origin.Scheme = "https"
}
*originHost = strings.TrimPrefix(*originHost, "http://")
diff --git a/cmd/present2md/main.go b/cmd/present2md/main.go
index a11e57ecf8b..e23bb33daed 100644
--- a/cmd/present2md/main.go
+++ b/cmd/present2md/main.go
@@ -447,10 +447,10 @@ func parseInlineLink(s string) (link string, length int) {
// If the URL is http://foo.com, drop the http://
// In other words, render [[http://golang.org]] as:
// golang.org
- if strings.HasPrefix(rawURL, url.Scheme+"://") {
- simpleURL = strings.TrimPrefix(rawURL, url.Scheme+"://")
- } else if strings.HasPrefix(rawURL, url.Scheme+":") {
- simpleURL = strings.TrimPrefix(rawURL, url.Scheme+":")
+ if after, ok := strings.CutPrefix(rawURL, url.Scheme+"://"); ok {
+ simpleURL = after
+ } else if after, ok := strings.CutPrefix(rawURL, url.Scheme+":"); ok {
+ simpleURL = after
}
}
return renderLink(rawURL, simpleURL), end + 2
diff --git a/cmd/signature-fuzzer/internal/fuzz-generator/gen_test.go b/cmd/signature-fuzzer/internal/fuzz-generator/gen_test.go
index 4bd5bab7c38..f10a7e9a7df 100644
--- a/cmd/signature-fuzzer/internal/fuzz-generator/gen_test.go
+++ b/cmd/signature-fuzzer/internal/fuzz-generator/gen_test.go
@@ -35,7 +35,7 @@ func mkGenState() *genstate {
func TestBasic(t *testing.T) {
checkTunables(tunables)
s := mkGenState()
- for i := 0; i < 1000; i++ {
+ for i := range 1000 {
s.wr = NewWrapRand(int64(i), RandCtlChecks|RandCtlPanic)
fp := s.GenFunc(i, i)
var buf bytes.Buffer
@@ -58,7 +58,7 @@ func TestMoreComplicated(t *testing.T) {
checkTunables(tunables)
s := mkGenState()
- for i := 0; i < 10000; i++ {
+ for i := range 10000 {
s.wr = NewWrapRand(int64(i), RandCtlChecks|RandCtlPanic)
fp := s.GenFunc(i, i)
var buf bytes.Buffer
diff --git a/cmd/signature-fuzzer/internal/fuzz-generator/generator.go b/cmd/signature-fuzzer/internal/fuzz-generator/generator.go
index 6c8002f9f0c..261dd6c029b 100644
--- a/cmd/signature-fuzzer/internal/fuzz-generator/generator.go
+++ b/cmd/signature-fuzzer/internal/fuzz-generator/generator.go
@@ -48,6 +48,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "slices"
"strconv"
"strings"
)
@@ -561,12 +562,7 @@ func (s *genstate) popTunables() {
// See precludeSelectedTypes below for more info.
func (s *genstate) redistributeFraction(toIncorporate uint8, avoid []int) {
inavoid := func(j int) bool {
- for _, k := range avoid {
- if j == k {
- return true
- }
- }
- return false
+ return slices.Contains(avoid, j)
}
doredis := func() {
@@ -631,7 +627,7 @@ func (s *genstate) GenParm(f *funcdef, depth int, mkctl bool, pidx int) parm {
// Convert tf into a cumulative sum
tf := s.tunables.typeFractions
sum := uint8(0)
- for i := 0; i < len(tf); i++ {
+ for i := range len(tf) {
sum += tf[i]
tf[i] = sum
}
@@ -662,7 +658,7 @@ func (s *genstate) GenParm(f *funcdef, depth int, mkctl bool, pidx int) parm {
f.structdefs = append(f.structdefs, sp)
tnf := int64(s.tunables.nStructFields) / int64(depth+1)
nf := int(s.wr.Intn(tnf))
- for fi := 0; fi < nf; fi++ {
+ for range nf {
fp := s.GenParm(f, depth+1, false, pidx)
skComp := tunables.doSkipCompare &&
uint8(s.wr.Intn(100)) < s.tunables.skipCompareFraction
@@ -832,7 +828,7 @@ func (s *genstate) GenFunc(fidx int, pidx int) *funcdef {
needControl := f.recur
f.dodefc = uint8(s.wr.Intn(100))
pTaken := uint8(s.wr.Intn(100)) < s.tunables.takenFraction
- for pi := 0; pi < numParams; pi++ {
+ for range numParams {
newparm := s.GenParm(f, 0, needControl, pidx)
if !pTaken {
newparm.SetAddrTaken(notAddrTaken)
@@ -848,7 +844,7 @@ func (s *genstate) GenFunc(fidx int, pidx int) *funcdef {
}
rTaken := uint8(s.wr.Intn(100)) < s.tunables.takenFraction
- for ri := 0; ri < numReturns; ri++ {
+ for range numReturns {
r := s.GenReturn(f, 0, pidx)
if !rTaken {
r.SetAddrTaken(notAddrTaken)
@@ -903,7 +899,7 @@ func (s *genstate) emitCompareFunc(f *funcdef, b *bytes.Buffer, p parm) {
b.WriteString(" return ")
numel := p.NumElements()
ncmp := 0
- for i := 0; i < numel; i++ {
+ for i := range numel {
lelref, lelparm := p.GenElemRef(i, "left")
relref, _ := p.GenElemRef(i, "right")
if lelref == "" || lelref == "_" {
@@ -1501,7 +1497,7 @@ func (s *genstate) emitParamChecks(f *funcdef, b *bytes.Buffer, pidx int, value
} else {
numel := p.NumElements()
cel := checkableElements(p)
- for i := 0; i < numel; i++ {
+ for i := range numel {
verb(4, "emitting check-code for p%d el %d value=%d", pi, i, value)
elref, elparm := p.GenElemRef(i, s.genParamRef(p, pi))
valstr, value = s.GenValue(f, elparm, value, false)
@@ -1535,7 +1531,7 @@ func (s *genstate) emitParamChecks(f *funcdef, b *bytes.Buffer, pidx int, value
// receiver value check
if f.isMethod {
numel := f.receiver.NumElements()
- for i := 0; i < numel; i++ {
+ for i := range numel {
verb(4, "emitting check-code for rcvr el %d value=%d", i, value)
elref, elparm := f.receiver.GenElemRef(i, "rcvr")
valstr, value = s.GenValue(f, elparm, value, false)
@@ -1608,7 +1604,7 @@ func (s *genstate) emitDeferChecks(f *funcdef, b *bytes.Buffer, value int) int {
b.WriteString(" // check parm " + which + "\n")
numel := p.NumElements()
cel := checkableElements(p)
- for i := 0; i < numel; i++ {
+ for i := range numel {
elref, elparm := p.GenElemRef(i, s.genParamRef(p, pi))
if elref == "" || elref == "_" || cel == 0 {
verb(4, "empty skip p%d el %d", pi, i)
@@ -2058,7 +2054,7 @@ func (s *genstate) emitMain(outf *os.File, numit int, fcnmask map[int]int, pkmas
for k := 0; k < s.NumTestPackages; k++ {
cp := fmt.Sprintf("%s%s%d", s.Tag, CallerName, k)
fmt.Fprintf(outf, " go func(ch chan bool) {\n")
- for i := 0; i < numit; i++ {
+ for i := range numit {
if shouldEmitFP(i, k, fcnmask, pkmask) {
fmt.Fprintf(outf, " %s.%s%d(\"normal\")\n", cp, CallerName, i)
if s.tunables.doReflectCall {
diff --git a/cmd/signature-fuzzer/internal/fuzz-generator/wraprand.go b/cmd/signature-fuzzer/internal/fuzz-generator/wraprand.go
index bba178dc317..f83a5f22e27 100644
--- a/cmd/signature-fuzzer/internal/fuzz-generator/wraprand.go
+++ b/cmd/signature-fuzzer/internal/fuzz-generator/wraprand.go
@@ -6,7 +6,7 @@ package generator
import (
"fmt"
- "math/rand"
+ "math/rand/v2"
"os"
"runtime"
"strings"
@@ -20,8 +20,7 @@ const (
)
func NewWrapRand(seed int64, ctl int) *wraprand {
- rand.Seed(seed)
- return &wraprand{seed: seed, ctl: ctl}
+ return &wraprand{seed: seed, ctl: ctl, rand: rand.New(rand.NewPCG(0, uint64(seed)))}
}
type wraprand struct {
@@ -32,6 +31,7 @@ type wraprand struct {
tag string
calls []string
ctl int
+ rand *rand.Rand
}
func (w *wraprand) captureCall(tag string, val string) {
@@ -59,7 +59,7 @@ func (w *wraprand) captureCall(tag string, val string) {
func (w *wraprand) Intn(n int64) int64 {
w.intncalls++
- rv := rand.Int63n(n)
+ rv := w.rand.Int64N(n)
if w.ctl&RandCtlCapture != 0 {
w.captureCall("Intn", fmt.Sprintf("%d", rv))
}
@@ -68,7 +68,7 @@ func (w *wraprand) Intn(n int64) int64 {
func (w *wraprand) Float32() float32 {
w.f32calls++
- rv := rand.Float32()
+ rv := w.rand.Float32()
if w.ctl&RandCtlCapture != 0 {
w.captureCall("Float32", fmt.Sprintf("%f", rv))
}
@@ -77,7 +77,7 @@ func (w *wraprand) Float32() float32 {
func (w *wraprand) NormFloat64() float64 {
w.f64calls++
- rv := rand.NormFloat64()
+ rv := w.rand.NormFloat64()
if w.ctl&RandCtlCapture != 0 {
w.captureCall("NormFloat64", fmt.Sprintf("%f", rv))
}
@@ -85,7 +85,7 @@ func (w *wraprand) NormFloat64() float64 {
}
func (w *wraprand) emitCalls(fn string) {
- outf, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+ outf, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
if err != nil {
panic(err)
}
diff --git a/cmd/splitdwarf/splitdwarf.go b/cmd/splitdwarf/splitdwarf.go
index 90ff10b6a05..24aa239226c 100644
--- a/cmd/splitdwarf/splitdwarf.go
+++ b/cmd/splitdwarf/splitdwarf.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd
-// +build aix darwin dragonfly freebsd linux netbsd openbsd
/*
Splitdwarf uncompresses and copies the DWARF segment of a Mach-O
diff --git a/cmd/ssadump/gotypesalias.go b/cmd/ssadump/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/ssadump/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/stress/stress.go b/cmd/stress/stress.go
index 6472064f933..e8b8641b387 100644
--- a/cmd/stress/stress.go
+++ b/cmd/stress/stress.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris windows
// The stress utility is intended for catching sporadic failures.
// It runs a given process in parallel in a loop and collects any failures.
diff --git a/cmd/stringer/golden_test.go b/cmd/stringer/golden_test.go
index 2a81c0855aa..e40b7c53c91 100644
--- a/cmd/stringer/golden_test.go
+++ b/cmd/stringer/golden_test.go
@@ -453,7 +453,6 @@ func TestGolden(t *testing.T) {
dir := t.TempDir()
for _, test := range golden {
- test := test
t.Run(test.name, func(t *testing.T) {
input := "package test\n" + test.input
file := test.name + ".go"
diff --git a/cmd/stringer/gotypesalias.go b/cmd/stringer/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/cmd/stringer/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/cmd/toolstash/main.go b/cmd/toolstash/main.go
index c533ed1e572..3a92c00bfff 100644
--- a/cmd/toolstash/main.go
+++ b/cmd/toolstash/main.go
@@ -225,7 +225,7 @@ func main() {
return
}
- tool = cmd[0]
+ tool = exeName(cmd[0])
if i := strings.LastIndexAny(tool, `/\`); i >= 0 {
tool = tool[i+1:]
}
@@ -530,7 +530,7 @@ func runCmd(cmd []string, keepLog bool, logName string) (output []byte, err erro
}()
}
- xcmd := exec.Command(cmd[0], cmd[1:]...)
+ xcmd := exec.Command(exeName(cmd[0]), cmd[1:]...)
if !keepLog {
return xcmd.CombinedOutput()
}
@@ -571,9 +571,10 @@ func save() {
if !shouldSave(name) {
continue
}
- src := filepath.Join(binDir, name)
+ bin := exeName(name)
+ src := filepath.Join(binDir, bin)
if _, err := os.Stat(src); err == nil {
- cp(src, filepath.Join(stashDir, name))
+ cp(src, filepath.Join(stashDir, bin))
}
}
@@ -641,3 +642,10 @@ func cp(src, dst string) {
log.Fatal(err)
}
}
+
+func exeName(name string) string {
+ if runtime.GOOS == "windows" {
+ return name + ".exe"
+ }
+ return name
+}
diff --git a/container/intsets/sparse.go b/container/intsets/sparse.go
index c56aacc28bb..b9b4c91ed21 100644
--- a/container/intsets/sparse.go
+++ b/container/intsets/sparse.go
@@ -267,7 +267,7 @@ func (s *Sparse) init() {
// loop. Fail fast before this occurs.
// We don't want to call panic here because it prevents the
// inlining of this function.
- _ = (interface{}(nil)).(to_copy_a_sparse_you_must_call_its_Copy_method)
+ _ = (any(nil)).(to_copy_a_sparse_you_must_call_its_Copy_method)
}
}
diff --git a/container/intsets/sparse_test.go b/container/intsets/sparse_test.go
index cd8ec6e0840..f218e09b6a3 100644
--- a/container/intsets/sparse_test.go
+++ b/container/intsets/sparse_test.go
@@ -236,7 +236,7 @@ func (set *pset) check(t *testing.T, msg string) {
func randomPset(prng *rand.Rand, maxSize int) *pset {
set := makePset()
size := int(prng.Int()) % maxSize
- for i := 0; i < size; i++ {
+ for range size {
// TODO(adonovan): benchmark how performance varies
// with this sparsity parameter.
n := int(prng.Int()) % 10000
@@ -252,7 +252,7 @@ func TestRandomMutations(t *testing.T) {
set := makePset()
prng := rand.New(rand.NewSource(0))
- for i := 0; i < 10000; i++ {
+ for i := range 10000 {
n := int(prng.Int())%2000 - 1000
if i%2 == 0 {
if debug {
@@ -278,9 +278,9 @@ func TestRandomMutations(t *testing.T) {
func TestLowerBound(t *testing.T) {
// Use random sets of sizes from 0 to about 4000.
prng := rand.New(rand.NewSource(0))
- for i := uint(0); i < 12; i++ {
+ for i := range uint(12) {
x := randomPset(prng, 1<= j && e < found {
@@ -302,7 +302,7 @@ func TestSetOperations(t *testing.T) {
// For each operator, we test variations such as
// Z.op(X, Y), Z.op(X, Z) and Z.op(Z, Y) to exercise
// the degenerate cases of each method implementation.
- for i := uint(0); i < 12; i++ {
+ for i := range uint(12) {
X := randomPset(prng, 1< default GODEBUG has gotypesalias=0
+go 1.23.0
require (
github.com/google/go-cmp v0.6.0
github.com/yuin/goldmark v1.4.13
- golang.org/x/mod v0.23.0
- golang.org/x/net v0.35.0
- golang.org/x/sync v0.11.0
+ golang.org/x/mod v0.24.0
+ golang.org/x/net v0.40.0
+ golang.org/x/sync v0.14.0
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457
)
-require golang.org/x/sys v0.30.0 // indirect
+require golang.org/x/sys v0.33.0 // indirect
diff --git a/go.sum b/go.sum
index 2d11b060c08..6a01512f3e4 100644
--- a/go.sum
+++ b/go.sum
@@ -2,13 +2,13 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
-golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
-golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
diff --git a/go/analysis/analysis.go b/go/analysis/analysis.go
index 3a73084a53c..a7df4d1fe4e 100644
--- a/go/analysis/analysis.go
+++ b/go/analysis/analysis.go
@@ -45,7 +45,7 @@ type Analyzer struct {
// To pass analysis results between packages (and thus
// potentially between address spaces), use Facts, which are
// serializable.
- Run func(*Pass) (interface{}, error)
+ Run func(*Pass) (any, error)
// RunDespiteErrors allows the driver to invoke
// the Run method of this analyzer even on a
@@ -112,7 +112,7 @@ type Pass struct {
// The map keys are the elements of Analysis.Required,
// and the type of each corresponding value is the required
// analysis's ResultType.
- ResultOf map[*Analyzer]interface{}
+ ResultOf map[*Analyzer]any
// ReadFile returns the contents of the named file.
//
@@ -186,7 +186,7 @@ type ObjectFact struct {
// Reportf is a helper function that reports a Diagnostic using the
// specified position and formatted error message.
-func (pass *Pass) Reportf(pos token.Pos, format string, args ...interface{}) {
+func (pass *Pass) Reportf(pos token.Pos, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
pass.Report(Diagnostic{Pos: pos, Message: msg})
}
@@ -201,7 +201,7 @@ type Range interface {
// ReportRangef is a helper function that reports a Diagnostic using the
// range provided. ast.Node values can be passed in as the range because
// they satisfy the Range interface.
-func (pass *Pass) ReportRangef(rng Range, format string, args ...interface{}) {
+func (pass *Pass) ReportRangef(rng Range, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
pass.Report(Diagnostic{Pos: rng.Pos(), End: rng.End(), Message: msg})
}
diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go
index 775fd20094d..20312345018 100644
--- a/go/analysis/analysistest/analysistest.go
+++ b/go/analysis/analysistest/analysistest.go
@@ -12,6 +12,7 @@ import (
"go/token"
"go/types"
"log"
+ "maps"
"os"
"path/filepath"
"regexp"
@@ -75,19 +76,27 @@ var TestData = func() string {
// Testing is an abstraction of a *testing.T.
type Testing interface {
- Errorf(format string, args ...interface{})
+ Errorf(format string, args ...any)
}
-// RunWithSuggestedFixes behaves like Run, but additionally verifies suggested fixes.
-// It uses golden files placed alongside the source code under analysis:
-// suggested fixes for code in example.go will be compared against example.go.golden.
+// RunWithSuggestedFixes behaves like Run, but additionally applies
+// suggested fixes and verifies their output.
//
-// Golden files can be formatted in one of two ways: as plain Go source code, or as txtar archives.
-// In the first case, all suggested fixes will be applied to the original source, which will then be compared against the golden file.
-// In the second case, suggested fixes will be grouped by their messages, and each set of fixes will be applied and tested separately.
-// Each section in the archive corresponds to a single message.
+// It uses golden files, placed alongside each source file, to express
+// the desired output: the expected transformation of file example.go
+// is specified in file example.go.golden.
//
-// A golden file using txtar may look like this:
+// Golden files may be of two forms: a plain Go source file, or a
+// txtar archive.
+//
+// A plain Go source file indicates the expected result of applying
+// all suggested fixes to the original file.
+//
+// A txtar archive specifies, in each section, the expected result of
+// applying all suggested fixes of a given message to the original
+// file; the name of the archive section is the fix's message. In this
+// way, the various alternative fixes offered by a single diagnostic
+// can be tested independently. Here's an example:
//
// -- turn into single negation --
// package pkg
@@ -109,41 +118,28 @@ type Testing interface {
//
// # Conflicts
//
-// A single analysis pass may offer two or more suggested fixes that
-// (1) conflict but are nonetheless logically composable, (e.g.
-// because both update the import declaration), or (2) are
-// fundamentally incompatible (e.g. alternative fixes to the same
-// statement).
+// Regardless of the form of the golden file, it is possible for
+// multiple fixes to conflict, either because they overlap, or are
+// close enough together that the particular diff algorithm cannot
+// separate them.
//
-// It is up to the driver to decide how to apply such fixes. A
-// sophisticated driver could attempt to resolve conflicts of the
-// first kind, but this test driver simply reports the fact of the
-// conflict with the expectation that the user will split their tests
-// into nonconflicting parts.
+// RunWithSuggestedFixes uses a simple three-way merge to accumulate
+// fixes, similar to a git merge. The merge algorithm may be able to
+// coalesce identical edits, for example duplicate imports of the same
+// package. (Bear in mind that this is an editorial decision. In
+// general, coalescing identical edits may not be correct: consider
+// two statements that increment the same counter.)
//
-// Conflicts of the second kind can be avoided by giving the
-// alternative fixes different names (SuggestedFix.Message) and
-// defining the .golden file as a multi-section txtar file with a
-// named section for each alternative fix, as shown above.
+// If there are conflicts, the test fails. In any case, the
+// non-conflicting edits will be compared against the expected output.
+// In this situation, we recommend that you increase the textual
+// separation between conflicting parts or, if that fails, split
+// your tests into smaller parts.
//
-// Analyzers that compute fixes from a textual diff of the
-// before/after file contents (instead of directly from syntax tree
-// positions) may produce fixes that, although logically
-// non-conflicting, nonetheless conflict due to the particulars of the
-// diff algorithm. In such cases it may suffice to introduce
-// sufficient separation of the statements in the test input so that
-// the computed diffs do not overlap. If that fails, break the test
-// into smaller parts.
-//
-// TODO(adonovan): the behavior of RunWithSuggestedFixes as documented
-// above is impractical for tests that report multiple diagnostics and
-// offer multiple alternative fixes for the same diagnostic, and it is
-// inconsistent with the interpretation of multiple diagnostics
-// described at Diagnostic.SuggestedFixes.
-// We need to rethink the analyzer testing API to better support such
-// cases. In the meantime, users of RunWithSuggestedFixes testing
-// analyzers that offer alternative fixes are advised to put each fix
-// in a separate .go file in the testdata.
+// If a diagnostic offers multiple fixes for the same problem, they
+// are almost certain to conflict, so in this case you should define
+// the expected output using a multi-section txtar file as described
+// above.
func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result {
results := Run(t, dir, a, patterns...)
@@ -173,133 +169,165 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
for _, result := range results {
act := result.Action
- // file -> message -> edits
- // TODO(adonovan): this mapping assumes fix.Messages are unique across analyzers,
- // whereas they are only unique within a given Diagnostic.
- fileEdits := make(map[*token.File]map[string][]diff.Edit)
-
- // We may assume that fixes are validated upon creation in Pass.Report.
- // Group fixes by file and message.
+ // For each fix, split its edits by file and convert to diff form.
+ var (
+ // fixEdits: message -> fixes -> filename -> edits
+ //
+ // TODO(adonovan): this mapping assumes fix.Messages
+ // are unique across analyzers, whereas they are only
+ // unique within a given Diagnostic.
+ fixEdits = make(map[string][]map[string][]diff.Edit)
+ allFilenames = make(map[string]bool)
+ )
for _, diag := range act.Diagnostics {
+ // Fixes are validated upon creation in Pass.Report.
for _, fix := range diag.SuggestedFixes {
// Assert that lazy fixes have a Category (#65578, #65087).
if inTools && len(fix.TextEdits) == 0 && diag.Category == "" {
t.Errorf("missing Diagnostic.Category for SuggestedFix without TextEdits (gopls requires the category for the name of the fix command")
}
+ // Convert edits to diff form.
+ // Group fixes by message and file.
+ edits := make(map[string][]diff.Edit)
for _, edit := range fix.TextEdits {
file := act.Package.Fset.File(edit.Pos)
- if _, ok := fileEdits[file]; !ok {
- fileEdits[file] = make(map[string][]diff.Edit)
- }
- fileEdits[file][fix.Message] = append(fileEdits[file][fix.Message], diff.Edit{
+ allFilenames[file.Name()] = true
+ edits[file.Name()] = append(edits[file.Name()], diff.Edit{
Start: file.Offset(edit.Pos),
End: file.Offset(edit.End),
New: string(edit.NewText),
})
}
+ fixEdits[fix.Message] = append(fixEdits[fix.Message], edits)
+ }
+ }
+
+ merge := func(file, message string, x, y []diff.Edit) []diff.Edit {
+ z, ok := diff.Merge(x, y)
+ if !ok {
+ t.Errorf("in file %s, conflict applying fix %q", file, message)
+ return x // discard y
}
+ return z
}
- for file, fixes := range fileEdits {
- // Get the original file contents.
- // TODO(adonovan): plumb pass.ReadFile.
- orig, err := os.ReadFile(file.Name())
+ // Because the checking is driven by original
+ // filenames, there is no way to express that a fix
+ // (e.g. extract declaration) creates a new file.
+ for _, filename := range slices.Sorted(maps.Keys(allFilenames)) {
+ // Read the original file.
+ content, err := os.ReadFile(filename)
if err != nil {
- t.Errorf("error reading %s: %v", file.Name(), err)
+ t.Errorf("error reading %s: %v", filename, err)
continue
}
- // Get the golden file and read the contents.
- ar, err := txtar.ParseFile(file.Name() + ".golden")
+ // check checks that the accumulated edits applied
+ // to the original content yield the wanted content.
+ check := func(prefix string, accumulated []diff.Edit, want []byte) {
+ if err := applyDiffsAndCompare(filename, content, want, accumulated); err != nil {
+ t.Errorf("%s: %s", prefix, err)
+ }
+ }
+
+ // Read the golden file. It may have one of two forms:
+ // (1) A txtar archive with one section per fix title,
+ // including all fixes of just that title.
+ // (2) The expected output for file.Name after all (?) fixes are applied.
+ // This form requires that no diagnostic has multiple fixes.
+ ar, err := txtar.ParseFile(filename + ".golden")
if err != nil {
- t.Errorf("error reading %s.golden: %v", file.Name(), err)
+ t.Errorf("error reading %s.golden: %v", filename, err)
continue
}
-
if len(ar.Files) > 0 {
- // one virtual file per kind of suggested fix
-
- if len(ar.Comment) != 0 {
- // we allow either just the comment, or just virtual
- // files, not both. it is not clear how "both" should
- // behave.
- t.Errorf("%s.golden has leading comment; we don't know what to do with it", file.Name())
+ // Form #1: one archive section per kind of suggested fix.
+ if len(ar.Comment) > 0 {
+ // Disallow the combination of comment and archive sections.
+ t.Errorf("%s.golden has leading comment; we don't know what to do with it", filename)
continue
}
- // Sort map keys for determinism in tests.
- // TODO(jba): replace with slices.Sorted(maps.Keys(fixes)) when go.mod >= 1.23.
- var keys []string
- for k := range fixes {
- keys = append(keys, k)
- }
- slices.Sort(keys)
- for _, sf := range keys {
- edits := fixes[sf]
- found := false
- for _, vf := range ar.Files {
- if vf.Name == sf {
- found = true
- // the file may contain multiple trailing
- // newlines if the user places empty lines
- // between files in the archive. normalize
- // this to a single newline.
- golden := append(bytes.TrimRight(vf.Data, "\n"), '\n')
-
- if err := applyDiffsAndCompare(orig, golden, edits, file.Name()); err != nil {
- t.Errorf("%s", err)
- }
- break
- }
- }
- if !found {
- t.Errorf("no section for suggested fix %q in %s.golden", sf, file.Name())
+
+ // Each archive section is named for a fix.Message.
+ // Accumulate the parts of the fix that apply to the current file,
+ // using a simple three-way merge, discarding conflicts,
+ // then apply the merged edits and compare to the archive section.
+ for _, section := range ar.Files {
+ message, want := section.Name, section.Data
+ var accumulated []diff.Edit
+ for _, fix := range fixEdits[message] {
+ accumulated = merge(filename, message, accumulated, fix[filename])
}
- }
- } else {
- // all suggested fixes are represented by a single file
- // TODO(adonovan): fix: this makes no sense if len(fixes) > 1.
- // Sort map keys for determinism in tests.
- // TODO(jba): replace with slices.Sorted(maps.Keys(fixes)) when go.mod >= 1.23.
- var keys []string
- for k := range fixes {
- keys = append(keys, k)
- }
- slices.Sort(keys)
- var catchallEdits []diff.Edit
- for _, k := range keys {
- catchallEdits = append(catchallEdits, fixes[k]...)
+ check(fmt.Sprintf("all fixes of message %q", message), accumulated, want)
}
- if err := applyDiffsAndCompare(orig, ar.Comment, catchallEdits, file.Name()); err != nil {
- t.Errorf("%s", err)
+ } else {
+ // Form #2: all suggested fixes are represented by a single file.
+ want := ar.Comment
+ var accumulated []diff.Edit
+ for _, message := range slices.Sorted(maps.Keys(fixEdits)) {
+ for _, fix := range fixEdits[message] {
+ accumulated = merge(filename, message, accumulated, fix[filename])
+ }
}
+ check("all fixes", accumulated, want)
}
}
}
+
return results
}
-// applyDiffsAndCompare applies edits to src and compares the results against
-// golden after formatting both. fileName is use solely for error reporting.
-func applyDiffsAndCompare(src, golden []byte, edits []diff.Edit, fileName string) error {
- out, err := diff.ApplyBytes(src, edits)
+// applyDiffsAndCompare applies edits to original and compares the results against
+// want after formatting both. fileName is use solely for error reporting.
+func applyDiffsAndCompare(filename string, original, want []byte, edits []diff.Edit) error {
+ // Relativize filename, for tidier errors.
+ if cwd, err := os.Getwd(); err == nil {
+ if rel, err := filepath.Rel(cwd, filename); err == nil {
+ filename = rel
+ }
+ }
+
+ if len(edits) == 0 {
+ return fmt.Errorf("%s: no edits", filename)
+ }
+ fixedBytes, err := diff.ApplyBytes(original, edits)
if err != nil {
- return fmt.Errorf("%s: error applying fixes: %v (see possible explanations at RunWithSuggestedFixes)", fileName, err)
+ return fmt.Errorf("%s: error applying fixes: %v (see possible explanations at RunWithSuggestedFixes)", filename, err)
}
- wantRaw, err := format.Source(golden)
+ fixed, err := format.Source(fixedBytes)
if err != nil {
- return fmt.Errorf("%s.golden: error formatting golden file: %v\n%s", fileName, err, out)
+ return fmt.Errorf("%s: error formatting resulting source: %v\n%s", filename, err, fixedBytes)
}
- want := string(wantRaw)
- formatted, err := format.Source(out)
+ want, err = format.Source(want)
if err != nil {
- return fmt.Errorf("%s: error formatting resulting source: %v\n%s", fileName, err, out)
+ return fmt.Errorf("%s.golden: error formatting golden file: %v\n%s", filename, err, fixed)
}
- if got := string(formatted); got != want {
- unified := diff.Unified(fileName+".golden", "actual", want, got)
- return fmt.Errorf("suggested fixes failed for %s:\n%s", fileName, unified)
+
+ // Keep error reporting logic below consistent with
+ // TestScript in ../internal/checker/fix_test.go!
+
+ unified := func(xlabel, ylabel string, x, y []byte) string {
+ x = append(slices.Clip(bytes.TrimSpace(x)), '\n')
+ y = append(slices.Clip(bytes.TrimSpace(y)), '\n')
+ return diff.Unified(xlabel, ylabel, string(x), string(y))
+ }
+
+ if diff := unified(filename+" (fixed)", filename+" (want)", fixed, want); diff != "" {
+ return fmt.Errorf("unexpected %s content:\n"+
+ "-- original --\n%s\n"+
+ "-- fixed --\n%s\n"+
+ "-- want --\n%s\n"+
+ "-- diff original fixed --\n%s\n"+
+ "-- diff fixed want --\n%s",
+ filename,
+ original,
+ fixed,
+ want,
+ unified(filename+" (original)", filename+" (fixed)", original, fixed),
+ diff)
}
return nil
}
@@ -499,7 +527,7 @@ func check(t Testing, gopath string, act *checker.Action) {
// Any comment starting with "want" is treated
// as an expectation, even without following whitespace.
- if rest := strings.TrimPrefix(text, "want"); rest != text {
+ if rest, ok := strings.CutPrefix(text, "want"); ok {
lineDelta, expects, err := parseExpectations(rest)
if err != nil {
t.Errorf("%s:%d: in 'want' comment: %s", filename, linenum, err)
@@ -551,7 +579,7 @@ func check(t Testing, gopath string, act *checker.Action) {
// ignored. (This was previously a hack in the respective
// analyzers' tests.)
if act.Analyzer.Name == "buildtag" || act.Analyzer.Name == "directive" {
- files = append(files[:len(files):len(files)], act.Package.IgnoredFiles...)
+ files = slices.Concat(files, act.Package.IgnoredFiles)
}
for _, filename := range files {
diff --git a/go/analysis/analysistest/analysistest_test.go b/go/analysis/analysistest/analysistest_test.go
index eedbb5c2a90..88cd8f8f1d5 100644
--- a/go/analysis/analysistest/analysistest_test.go
+++ b/go/analysis/analysistest/analysistest_test.go
@@ -262,6 +262,6 @@ type T string
type errorfunc func(string)
-func (f errorfunc) Errorf(format string, args ...interface{}) {
+func (f errorfunc) Errorf(format string, args ...any) {
f(fmt.Sprintf(format, args...))
}
diff --git a/go/analysis/checker/checker.go b/go/analysis/checker/checker.go
index 502ec922179..94808733b9d 100644
--- a/go/analysis/checker/checker.go
+++ b/go/analysis/checker/checker.go
@@ -594,7 +594,7 @@ func (act *Action) exportPackageFact(fact analysis.Fact) {
func factType(fact analysis.Fact) reflect.Type {
t := reflect.TypeOf(fact)
- if t.Kind() != reflect.Ptr {
+ if t.Kind() != reflect.Pointer {
log.Fatalf("invalid Fact type: got %T, want pointer", fact)
}
return t
diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go
index c2445575cff..6aefef25815 100644
--- a/go/analysis/internal/analysisflags/flags.go
+++ b/go/analysis/internal/analysisflags/flags.go
@@ -201,7 +201,7 @@ func addVersionFlag() {
type versionFlag struct{}
func (versionFlag) IsBoolFlag() bool { return true }
-func (versionFlag) Get() interface{} { return nil }
+func (versionFlag) Get() any { return nil }
func (versionFlag) String() string { return "" }
func (versionFlag) Set(s string) error {
if s != "full" {
@@ -252,7 +252,7 @@ const (
// triState implements flag.Value, flag.Getter, and flag.boolFlag.
// They work like boolean flags: we can say vet -printf as well as vet -printf=true
-func (ts *triState) Get() interface{} {
+func (ts *triState) Get() any {
return *ts == setTrue
}
@@ -340,7 +340,7 @@ func PrintPlain(out io.Writer, fset *token.FileSet, contextLines int, diag analy
// A JSONTree is a mapping from package ID to analysis name to result.
// Each result is either a jsonError or a list of JSONDiagnostic.
-type JSONTree map[string]map[string]interface{}
+type JSONTree map[string]map[string]any
// A TextEdit describes the replacement of a portion of a file.
// Start and End are zero-based half-open indices into the original byte
@@ -383,7 +383,7 @@ type JSONRelatedInformation struct {
// Add adds the result of analysis 'name' on package 'id'.
// The result is either a list of diagnostics or an error.
func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) {
- var v interface{}
+ var v any
if err != nil {
type jsonError struct {
Err string `json:"error"`
@@ -429,7 +429,7 @@ func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.
if v != nil {
m, ok := tree[id]
if !ok {
- m = make(map[string]interface{})
+ m = make(map[string]any)
tree[id] = m
}
m[name] = v
diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go
index fb3c47b1625..bc57dc6e673 100644
--- a/go/analysis/internal/checker/checker.go
+++ b/go/analysis/internal/checker/checker.go
@@ -86,7 +86,36 @@ func RegisterFlags() {
// It provides most of the logic for the main functions of both the
// singlechecker and the multi-analysis commands.
// It returns the appropriate exit code.
-func Run(args []string, analyzers []*analysis.Analyzer) int {
+//
+// TODO(adonovan): tests should not call this function directly;
+// fiddling with global variables (flags) is error-prone and hostile
+// to parallelism. Instead, use unit tests of the actual units (e.g.
+// checker.Analyze) and integration tests (e.g. TestScript) of whole
+// executables.
+func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) {
+ // Instead of returning a code directly,
+ // call this function to monotonically increase the exit code.
+ // This allows us to keep going in the face of some errors
+ // without having to remember what code to return.
+ //
+ // TODO(adonovan): interpreting exit codes is like reading tea-leaves.
+ // Insted of wasting effort trying to encode a multidimensional result
+ // into 7 bits we should just emit structured JSON output, and
+ // an exit code of 0 or 1 for success or failure.
+ exitAtLeast := func(code int) {
+ exitcode = max(code, exitcode)
+ }
+
+ // When analysisflags is linked in (for {single,multi}checker),
+ // then the -v flag is registered for complex legacy reasons
+ // related to cmd/vet CLI.
+ // Treat it as an undocumented alias for -debug=v.
+ if v := flag.CommandLine.Lookup("v"); v != nil &&
+ v.Value.(flag.Getter).Get() == true &&
+ !strings.Contains(Debug, "v") {
+ Debug += "v"
+ }
+
if CPUProfile != "" {
f, err := os.Create(CPUProfile)
if err != nil {
@@ -142,17 +171,14 @@ func Run(args []string, analyzers []*analysis.Analyzer) int {
initial, err := load(args, allSyntax)
if err != nil {
log.Print(err)
- return 1
+ exitAtLeast(1)
+ return
}
- // TODO(adonovan): simplify exit code logic by using a single
- // exit code variable and applying "code = max(code, X)" each
- // time an error of code X occurs.
- pkgsExitCode := 0
// Print package and module errors regardless of RunDespiteErrors.
// Do not exit if there are errors, yet.
if n := packages.PrintErrors(initial); n > 0 {
- pkgsExitCode = 1
+ exitAtLeast(1)
}
var factLog io.Writer
@@ -172,7 +198,8 @@ func Run(args []string, analyzers []*analysis.Analyzer) int {
graph, err := checker.Analyze(analyzers, initial, opts)
if err != nil {
log.Print(err)
- return 1
+ exitAtLeast(1)
+ return
}
// Don't print the diagnostics,
@@ -181,22 +208,22 @@ func Run(args []string, analyzers []*analysis.Analyzer) int {
if err := applyFixes(graph.Roots, Diff); err != nil {
// Fail when applying fixes failed.
log.Print(err)
- return 1
+ exitAtLeast(1)
+ return
}
- // TODO(adonovan): don't proceed to print the text or JSON output
- // if we applied fixes; stop here.
- //
- // return pkgsExitCode
+ // Don't proceed to print text/JSON,
+ // and don't report an error
+ // just because there were diagnostics.
+ return
}
// Print the results. If !RunDespiteErrors and there
// are errors in the packages, this will have 0 exit
// code. Otherwise, we prefer to return exit code
// indicating diagnostics.
- if diagExitCode := printDiagnostics(graph); diagExitCode != 0 {
- return diagExitCode // there were diagnostics
- }
- return pkgsExitCode // package errors but no diagnostics
+ exitAtLeast(printDiagnostics(graph))
+
+ return
}
// printDiagnostics prints diagnostics in text or JSON form
@@ -215,15 +242,14 @@ func printDiagnostics(graph *checker.Graph) (exitcode int) {
// Compute the exit code.
var numErrors, rootDiags int
- // TODO(adonovan): use "for act := range graph.All() { ... }" in go1.23.
- graph.All()(func(act *checker.Action) bool {
+ for act := range graph.All() {
if act.Err != nil {
numErrors++
} else if act.IsRoot {
rootDiags += len(act.Diagnostics)
}
- return true
- })
+ }
+
if numErrors > 0 {
exitcode = 1 // analysis failed, at least partially
} else if rootDiags > 0 {
@@ -239,12 +265,10 @@ func printDiagnostics(graph *checker.Graph) (exitcode int) {
var list []*checker.Action
var total time.Duration
- // TODO(adonovan): use "for act := range graph.All() { ... }" in go1.23.
- graph.All()(func(act *checker.Action) bool {
+ for act := range graph.All() {
list = append(list, act)
total += act.Duration
- return true
- })
+ }
// Print actions accounting for 90% of the total.
sort.Slice(list, func(i, j int) bool {
@@ -541,6 +565,10 @@ fixloop:
}
}
+ if dbg('v') {
+ log.Printf("applied %d fixes, updated %d files", len(fixes), filesUpdated)
+ }
+
return nil
}
diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go
index fcf5f66e03e..7d73aa3c6bb 100644
--- a/go/analysis/internal/checker/checker_test.go
+++ b/go/analysis/internal/checker/checker_test.go
@@ -49,8 +49,10 @@ func Foo() {
t.Fatal(err)
}
path := filepath.Join(testdata, "src/rename/test.go")
+
checker.Fix = true
checker.Run([]string{"file=" + path}, []*analysis.Analyzer{renameAnalyzer})
+ checker.Fix = false
contents, err := os.ReadFile(path)
if err != nil {
@@ -105,7 +107,7 @@ func NewT1() *T1 { return &T1{T} }
Name: "noop",
Doc: "noop",
Requires: []*analysis.Analyzer{inspect.Analyzer},
- Run: func(pass *analysis.Pass) (interface{}, error) {
+ Run: func(pass *analysis.Pass) (any, error) {
return nil, nil
},
RunDespiteErrors: true,
@@ -117,7 +119,7 @@ func NewT1() *T1 { return &T1{T} }
Name: "noopfact",
Doc: "noopfact",
Requires: []*analysis.Analyzer{inspect.Analyzer},
- Run: func(pass *analysis.Pass) (interface{}, error) {
+ Run: func(pass *analysis.Pass) (any, error) {
return nil, nil
},
RunDespiteErrors: true,
@@ -138,31 +140,33 @@ func NewT1() *T1 { return &T1{T} }
// package from source. For the rest, it asks 'go list' for export data,
// which fails because the compiler encounters the type error. Since the
// errors come from 'go list', the driver doesn't run the analyzer.
- {name: "despite-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1},
+ {name: "despite-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noop}, code: exitCodeFailed},
// The noopfact analyzer does use facts, so the driver loads source for
// all dependencies, does type checking itself, recognizes the error as a
// type error, and runs the analyzer.
- {name: "despite-error-fact", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 1},
+ {name: "despite-error-fact", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noopWithFact}, code: exitCodeFailed},
// combination of parse/type errors and no errors
- {name: "despite-error-and-no-error", pattern: []string{rderrFile, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
+ {name: "despite-error-and-no-error", pattern: []string{rderrFile, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: exitCodeFailed},
// non-existing package error
- {name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
- {name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
- {name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
+ {name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: exitCodeFailed},
+ {name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: exitCodeFailed},
+ {name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: exitCodeFailed},
// combination of type/parsing and different errors
- {name: "different-errors", pattern: []string{rderrFile, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
+ {name: "different-errors", pattern: []string{rderrFile, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: exitCodeFailed},
// non existing dir error
- {name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
+ {name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: exitCodeFailed},
// no errors
- {name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 0},
+ {name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: exitCodeSuccess},
// duplicate list error with no findings
- {name: "list-error", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1},
+ {name: "list-error", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{noop}, code: exitCodeFailed},
// duplicate list errors with findings (issue #67790)
- {name: "list-error-findings", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 3},
+ {name: "list-error-findings", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: exitCodeDiagnostics},
} {
- if got := checker.Run(test.pattern, test.analyzers); got != test.code {
- t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code)
- }
+ t.Run(test.name, func(t *testing.T) {
+ if got := checker.Run(test.pattern, test.analyzers); got != test.code {
+ t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code)
+ }
+ })
}
}
@@ -181,7 +185,7 @@ func TestURL(t *testing.T) {
Name: "pkgname",
Doc: "trivial analyzer that reports package names",
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/internal/checker",
- Run: func(p *analysis.Pass) (interface{}, error) {
+ Run: func(p *analysis.Pass) (any, error) {
for _, f := range p.Files {
p.ReportRangef(f.Name, "package name is %s", f.Name.Name)
}
diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go
index 8fb7506ac70..00710cc0e1b 100644
--- a/go/analysis/internal/checker/fix_test.go
+++ b/go/analysis/internal/checker/fix_test.go
@@ -52,9 +52,9 @@ func TestMain(m *testing.M) {
}
const (
- exitCodeSuccess = 0 // success (no diagnostics)
+ exitCodeSuccess = 0 // success (no diagnostics, or successful -fix)
exitCodeFailed = 1 // analysis failed to run
- exitCodeDiagnostics = 3 // diagnostics were reported
+ exitCodeDiagnostics = 3 // diagnostics were reported (and no -fix)
)
// TestReportInvalidDiagnostic tests that a call to pass.Report with
@@ -93,7 +93,7 @@ func TestReportInvalidDiagnostic(t *testing.T) {
// TextEdit has invalid Pos.
{
"bad Pos",
- `analyzer "a" suggests invalid fix .*: missing file info for pos`,
+ `analyzer "a" suggests invalid fix .*: no token.File for TextEdit.Pos .0.`,
func(pos token.Pos) analysis.Diagnostic {
return analysis.Diagnostic{
Pos: pos,
@@ -110,7 +110,7 @@ func TestReportInvalidDiagnostic(t *testing.T) {
// TextEdit has invalid End.
{
"End < Pos",
- `analyzer "a" suggests invalid fix .*: pos .* > end`,
+ `analyzer "a" suggests invalid fix .*: TextEdit.Pos .* > TextEdit.End .*`,
func(pos token.Pos) analysis.Diagnostic {
return analysis.Diagnostic{
Pos: pos,
@@ -281,6 +281,9 @@ func TestScript(t *testing.T) {
t.Logf("%s: $ %s\nstdout:\n%s\nstderr:\n%s", prefix, clean(cmd), stdout, lastStderr)
}
+ // Keep error reporting logic below consistent with
+ // applyDiffsAndCompare in ../../analysistest/analysistest.go!
+
unified := func(xlabel, ylabel string, x, y []byte) string {
x = append(slices.Clip(bytes.TrimSpace(x)), '\n')
y = append(slices.Clip(bytes.TrimSpace(y)), '\n')
diff --git a/go/analysis/internal/checker/start_test.go b/go/analysis/internal/checker/start_test.go
index 618ccd09b93..60ed54464ae 100644
--- a/go/analysis/internal/checker/start_test.go
+++ b/go/analysis/internal/checker/start_test.go
@@ -40,6 +40,7 @@ package comment
path := filepath.Join(testdata, "src/comment/doc.go")
checker.Fix = true
checker.Run([]string{"file=" + path}, []*analysis.Analyzer{commentAnalyzer})
+ checker.Fix = false
contents, err := os.ReadFile(path)
if err != nil {
@@ -61,7 +62,7 @@ var commentAnalyzer = &analysis.Analyzer{
Run: commentRun,
}
-func commentRun(pass *analysis.Pass) (interface{}, error) {
+func commentRun(pass *analysis.Pass) (any, error) {
const (
from = "/* Package comment */"
to = "// Package comment"
diff --git a/go/analysis/internal/checker/testdata/diff.txt b/go/analysis/internal/checker/testdata/diff.txt
index 5a0c9c2a3b2..f11f01ad1e4 100644
--- a/go/analysis/internal/checker/testdata/diff.txt
+++ b/go/analysis/internal/checker/testdata/diff.txt
@@ -8,8 +8,7 @@
skip GOOS=windows
checker -rename -fix -diff example.com/p
-exit 3
-stderr renaming "bar" to "baz"
+exit 0
-- go.mod --
module example.com
diff --git a/go/analysis/internal/checker/testdata/fixes.txt b/go/analysis/internal/checker/testdata/fixes.txt
index 89f245f9ace..4d906ca3f54 100644
--- a/go/analysis/internal/checker/testdata/fixes.txt
+++ b/go/analysis/internal/checker/testdata/fixes.txt
@@ -2,9 +2,9 @@
# particular when processing duplicate fixes for overlapping packages
# in the same directory ("p", "p [p.test]", "p_test [p.test]").
-checker -rename -fix example.com/p
-exit 3
-stderr renaming "bar" to "baz"
+checker -rename -fix -v example.com/p
+stderr applied 8 fixes, updated 3 files
+exit 0
-- go.mod --
module example.com
diff --git a/go/analysis/internal/checker/testdata/importdup.txt b/go/analysis/internal/checker/testdata/importdup.txt
index e1783777858..4c144a61221 100644
--- a/go/analysis/internal/checker/testdata/importdup.txt
+++ b/go/analysis/internal/checker/testdata/importdup.txt
@@ -1,8 +1,9 @@
# Test that duplicate imports--and, more generally, duplicate
# identical insertions--are coalesced.
-checker -marker -fix example.com/a
-exit 3
+checker -marker -fix -v example.com/a
+stderr applied 2 fixes, updated 1 files
+exit 0
-- go.mod --
module example.com
diff --git a/go/analysis/internal/checker/testdata/importdup2.txt b/go/analysis/internal/checker/testdata/importdup2.txt
index 118fdc0184b..c2da0f33195 100644
--- a/go/analysis/internal/checker/testdata/importdup2.txt
+++ b/go/analysis/internal/checker/testdata/importdup2.txt
@@ -19,8 +19,9 @@
# In more complex examples, the result
# may be more subtly order-dependent.
-checker -marker -fix example.com/a example.com/b
-exit 3
+checker -marker -fix -v example.com/a example.com/b
+stderr applied 6 fixes, updated 2 files
+exit 0
-- go.mod --
module example.com
diff --git a/go/analysis/internal/checker/testdata/noend.txt b/go/analysis/internal/checker/testdata/noend.txt
index 2d6be074565..5ebc5e011ba 100644
--- a/go/analysis/internal/checker/testdata/noend.txt
+++ b/go/analysis/internal/checker/testdata/noend.txt
@@ -2,8 +2,7 @@
# interpreted as if equal to SuggestedFix.Pos (see issue #64199).
checker -noend -fix example.com/a
-exit 3
-stderr say hello
+exit 0
-- go.mod --
module example.com
diff --git a/go/analysis/internal/checker/testdata/overlap.txt b/go/analysis/internal/checker/testdata/overlap.txt
index f556ef308b9..581f2e18950 100644
--- a/go/analysis/internal/checker/testdata/overlap.txt
+++ b/go/analysis/internal/checker/testdata/overlap.txt
@@ -15,9 +15,12 @@
# (This is a pretty unlikely situation, but it corresponds
# to a historical test, TestOther, that used to check for
# a conflict, and it seemed wrong to delete it without explanation.)
+#
+# The fixes are silently and successfully applied.
-checker -rename -marker -fix example.com/a
-exit 3
+checker -rename -marker -fix -v example.com/a
+stderr applied 2 fixes, updated 1 file
+exit 0
-- go.mod --
module example.com
diff --git a/go/analysis/internal/internal.go b/go/analysis/internal/internal.go
index e7c8247fd33..327c4b50579 100644
--- a/go/analysis/internal/internal.go
+++ b/go/analysis/internal/internal.go
@@ -9,4 +9,4 @@ import "golang.org/x/tools/go/analysis"
// This function is set by the checker package to provide
// backdoor access to the private Pass field
// of the checker.Action type, for use by analysistest.
-var Pass func(interface{}) *analysis.Pass
+var Pass func(any) *analysis.Pass
diff --git a/go/analysis/internal/versiontest/version_test.go b/go/analysis/internal/versiontest/version_test.go
index 43c52f565f7..5bd6d3027dd 100644
--- a/go/analysis/internal/versiontest/version_test.go
+++ b/go/analysis/internal/versiontest/version_test.go
@@ -26,7 +26,7 @@ import (
var analyzer = &analysis.Analyzer{
Name: "versiontest",
Doc: "off",
- Run: func(pass *analysis.Pass) (interface{}, error) {
+ Run: func(pass *analysis.Pass) (any, error) {
pass.Reportf(pass.Files[0].Package, "goversion=%s", pass.Pkg.GoVersion())
return nil, nil
},
diff --git a/go/analysis/multichecker/multichecker_test.go b/go/analysis/multichecker/multichecker_test.go
index 07bf977369b..1491df153b9 100644
--- a/go/analysis/multichecker/multichecker_test.go
+++ b/go/analysis/multichecker/multichecker_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.12
-// +build go1.12
package multichecker_test
@@ -24,7 +23,7 @@ func main() {
fail := &analysis.Analyzer{
Name: "fail",
Doc: "always fail on a package 'sort'",
- Run: func(pass *analysis.Pass) (interface{}, error) {
+ Run: func(pass *analysis.Pass) (any, error) {
if pass.Pkg.Path() == "sort" {
return nil, fmt.Errorf("failed")
}
diff --git a/go/analysis/passes/appends/appends.go b/go/analysis/passes/appends/appends.go
index 6976f0d9090..e554c3cc903 100644
--- a/go/analysis/passes/appends/appends.go
+++ b/go/analysis/passes/appends/appends.go
@@ -29,7 +29,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
diff --git a/go/analysis/passes/asmdecl/asmdecl.go b/go/analysis/passes/asmdecl/asmdecl.go
index a47ecbae731..436b03cb290 100644
--- a/go/analysis/passes/asmdecl/asmdecl.go
+++ b/go/analysis/passes/asmdecl/asmdecl.go
@@ -150,7 +150,7 @@ var (
abiSuff = re(`^(.+)<(ABI.+)>$`)
)
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
// No work if no assembly files.
var sfiles []string
for _, fname := range pass.OtherFiles {
@@ -226,7 +226,7 @@ Files:
for lineno, line := range lines {
lineno++
- badf := func(format string, args ...interface{}) {
+ badf := func(format string, args ...any) {
pass.Reportf(analysisutil.LineStart(tf, lineno), "[%s] %s: %s", arch, fnName, fmt.Sprintf(format, args...))
}
@@ -646,7 +646,7 @@ func asmParseDecl(pass *analysis.Pass, decl *ast.FuncDecl) map[string]*asmFunc {
}
// asmCheckVar checks a single variable reference.
-func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar, archDef *asmArch) {
+func asmCheckVar(badf func(string, ...any), fn *asmFunc, line, expr string, off int, v *asmVar, archDef *asmArch) {
m := asmOpcode.FindStringSubmatch(line)
if m == nil {
if !strings.HasPrefix(strings.TrimSpace(line), "//") {
diff --git a/go/analysis/passes/buildssa/buildssa.go b/go/analysis/passes/buildssa/buildssa.go
index f077ea28247..f49fea51762 100644
--- a/go/analysis/passes/buildssa/buildssa.go
+++ b/go/analysis/passes/buildssa/buildssa.go
@@ -32,7 +32,7 @@ type SSA struct {
SrcFuncs []*ssa.Function
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
// We must create a new Program for each Package because the
// analysis API provides no place to hang a Program shared by
// all Packages. Consequently, SSA Packages and Functions do not
diff --git a/go/analysis/passes/buildtag/buildtag.go b/go/analysis/passes/buildtag/buildtag.go
index e7434e8fed2..6c7a0df585d 100644
--- a/go/analysis/passes/buildtag/buildtag.go
+++ b/go/analysis/passes/buildtag/buildtag.go
@@ -26,7 +26,7 @@ var Analyzer = &analysis.Analyzer{
Run: runBuildTag,
}
-func runBuildTag(pass *analysis.Pass) (interface{}, error) {
+func runBuildTag(pass *analysis.Pass) (any, error) {
for _, f := range pass.Files {
checkGoFile(pass, f)
}
diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go
index 4f3bb035d65..d9189b5b696 100644
--- a/go/analysis/passes/cgocall/cgocall.go
+++ b/go/analysis/passes/cgocall/cgocall.go
@@ -55,7 +55,7 @@ func run(pass *analysis.Pass) (any, error) {
return nil, nil
}
-func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(token.Pos, string, ...interface{})) {
+func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(token.Pos, string, ...any)) {
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go
index f56c3e622fb..25c98a97bbc 100644
--- a/go/analysis/passes/composite/composite.go
+++ b/go/analysis/passes/composite/composite.go
@@ -51,7 +51,7 @@ func init() {
// runUnkeyedLiteral checks if a composite literal is a struct literal with
// unkeyed fields.
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
@@ -115,7 +115,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
missingKeys = append(missingKeys, analysis.TextEdit{
Pos: e.Pos(),
End: e.Pos(),
- NewText: []byte(fmt.Sprintf("%s: ", field.Name())),
+ NewText: fmt.Appendf(nil, "%s: ", field.Name()),
})
}
}
diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go
index a9f02ac62e6..a4e455d9b30 100644
--- a/go/analysis/passes/copylock/copylock.go
+++ b/go/analysis/passes/copylock/copylock.go
@@ -36,7 +36,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
var goversion string // effective file version ("" => unknown)
@@ -355,7 +355,7 @@ func lockPath(tpkg *types.Package, typ types.Type, seen map[types.Type]bool) typ
}
nfields := styp.NumFields()
- for i := 0; i < nfields; i++ {
+ for i := range nfields {
ftyp := styp.Field(i).Type()
subpath := lockPath(tpkg, ftyp, seen)
if subpath != nil {
@@ -378,7 +378,7 @@ var lockerType *types.Interface
// Construct a sync.Locker interface type.
func init() {
- nullary := types.NewSignature(nil, nil, nil, false) // func()
+ nullary := types.NewSignatureType(nil, nil, nil, nil, nil, false) // func()
methods := []*types.Func{
types.NewFunc(token.NoPos, nil, "Lock", nullary),
types.NewFunc(token.NoPos, nil, "Unlock", nullary),
diff --git a/go/analysis/passes/ctrlflow/ctrlflow.go b/go/analysis/passes/ctrlflow/ctrlflow.go
index d21adeee900..951aaed00fd 100644
--- a/go/analysis/passes/ctrlflow/ctrlflow.go
+++ b/go/analysis/passes/ctrlflow/ctrlflow.go
@@ -80,7 +80,7 @@ func (c *CFGs) FuncLit(lit *ast.FuncLit) *cfg.CFG {
return c.funcLits[lit].cfg
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// Because CFG construction consumes and produces noReturn
diff --git a/go/analysis/passes/defers/cmd/defers/gotypesalias.go b/go/analysis/passes/defers/cmd/defers/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/defers/cmd/defers/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/directive/directive.go b/go/analysis/passes/directive/directive.go
index b205402388e..bebec891408 100644
--- a/go/analysis/passes/directive/directive.go
+++ b/go/analysis/passes/directive/directive.go
@@ -40,7 +40,7 @@ var Analyzer = &analysis.Analyzer{
Run: runDirective,
}
-func runDirective(pass *analysis.Pass) (interface{}, error) {
+func runDirective(pass *analysis.Pass) (any, error) {
for _, f := range pass.Files {
checkGoFile(pass, f)
}
diff --git a/go/analysis/passes/errorsas/errorsas_test.go b/go/analysis/passes/errorsas/errorsas_test.go
index 6689d8114a7..5414f9e8b6d 100644
--- a/go/analysis/passes/errorsas/errorsas_test.go
+++ b/go/analysis/passes/errorsas/errorsas_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.13
-// +build go1.13
package errorsas_test
diff --git a/go/analysis/passes/fieldalignment/cmd/fieldalignment/gotypesalias.go b/go/analysis/passes/fieldalignment/cmd/fieldalignment/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/fieldalignment/cmd/fieldalignment/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/fieldalignment/fieldalignment.go b/go/analysis/passes/fieldalignment/fieldalignment.go
index 93fa39140e6..4987ec5afdd 100644
--- a/go/analysis/passes/fieldalignment/fieldalignment.go
+++ b/go/analysis/passes/fieldalignment/fieldalignment.go
@@ -65,7 +65,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.StructType)(nil),
@@ -168,7 +168,7 @@ func optimalOrder(str *types.Struct, sizes *gcSizes) (*types.Struct, []int) {
}
elems := make([]elem, nf)
- for i := 0; i < nf; i++ {
+ for i := range nf {
field := str.Field(i)
ft := field.Type()
elems[i] = elem{
@@ -312,7 +312,7 @@ func (s *gcSizes) Sizeof(T types.Type) int64 {
var o int64
max := int64(1)
- for i := 0; i < nf; i++ {
+ for i := range nf {
ft := t.Field(i).Type()
a, sz := s.Alignof(ft), s.Sizeof(ft)
if a > max {
@@ -366,7 +366,7 @@ func (s *gcSizes) ptrdata(T types.Type) int64 {
}
var o, p int64
- for i := 0; i < nf; i++ {
+ for i := range nf {
ft := t.Field(i).Type()
a, sz := s.Alignof(ft), s.Sizeof(ft)
fp := s.ptrdata(ft)
diff --git a/go/analysis/passes/findcall/cmd/findcall/gotypesalias.go b/go/analysis/passes/findcall/cmd/findcall/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/findcall/cmd/findcall/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/findcall/findcall.go b/go/analysis/passes/findcall/findcall.go
index 2671573d1fe..9db4de1c20f 100644
--- a/go/analysis/passes/findcall/findcall.go
+++ b/go/analysis/passes/findcall/findcall.go
@@ -38,7 +38,7 @@ func init() {
Analyzer.Flags.StringVar(&name, "name", name, "name of the function to find")
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
for _, f := range pass.Files {
ast.Inspect(f, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
diff --git a/go/analysis/passes/framepointer/framepointer.go b/go/analysis/passes/framepointer/framepointer.go
index 8012de99daa..ba94fd68ea4 100644
--- a/go/analysis/passes/framepointer/framepointer.go
+++ b/go/analysis/passes/framepointer/framepointer.go
@@ -113,7 +113,7 @@ var arm64Branch = map[string]bool{
"RET": true,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
arch, ok := arches[build.Default.GOARCH]
if !ok {
return nil, nil
diff --git a/go/analysis/passes/gofix/doc.go b/go/analysis/passes/gofix/doc.go
new file mode 100644
index 00000000000..cb66e83fae1
--- /dev/null
+++ b/go/analysis/passes/gofix/doc.go
@@ -0,0 +1,50 @@
+// Copyright 2025 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 gofix defines an Analyzer that checks "//go:fix inline" directives.
+See golang.org/x/tools/internal/gofix/doc.go for details.
+
+# Analyzer gofixdirective
+
+gofixdirective: validate uses of gofix comment directives
+
+The gofixdirective analyzer checks "//go:fix inline" directives for correctness.
+
+The proposal https://go.dev/issue/32816 introduces the "//go:fix" directives.
+
+The analyzer checks for the following issues:
+
+- A constant definition can be marked for inlining only if it refers to another
+named constant.
+
+ //go:fix inline
+ const (
+ a = 1 // error
+ b = iota // error
+ c = a // OK
+ d = math.Pi // OK
+ )
+
+- A type definition can be marked for inlining only if it is an alias.
+
+ //go:fix inline
+ type (
+ T int // error
+ A = int // OK
+ )
+
+- An alias whose right-hand side contains a non-literal array size
+cannot be marked for inlining.
+
+ const two = 2
+
+ //go:fix inline
+ type (
+ A = []int // OK
+ B = [1]int // OK
+ C = [two]int // error
+ )
+*/
+package gofix
diff --git a/go/analysis/passes/gofix/gofix.go b/go/analysis/passes/gofix/gofix.go
new file mode 100644
index 00000000000..706e0759c3a
--- /dev/null
+++ b/go/analysis/passes/gofix/gofix.go
@@ -0,0 +1,34 @@
+// Copyright 2025 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 gofix defines an analyzer that checks go:fix directives.
+package gofix
+
+import (
+ _ "embed"
+
+ "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"
+ "golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/gofix/findgofix"
+)
+
+//go:embed doc.go
+var doc string
+
+var Analyzer = &analysis.Analyzer{
+ Name: "gofixdirective",
+ Doc: analysisinternal.MustExtractDoc(doc, "gofixdirective"),
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/gofix",
+ Run: run,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+}
+
+func run(pass *analysis.Pass) (any, error) {
+ root := cursor.Root(pass.ResultOf[inspect.Analyzer].(*inspector.Inspector))
+ findgofix.Find(pass, root, nil)
+ return nil, nil
+}
diff --git a/go/analysis/passes/gofix/gofix_test.go b/go/analysis/passes/gofix/gofix_test.go
new file mode 100644
index 00000000000..b2e6d4387d4
--- /dev/null
+++ b/go/analysis/passes/gofix/gofix_test.go
@@ -0,0 +1,17 @@
+// Copyright 2025 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 gofix_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+ "golang.org/x/tools/go/analysis/passes/gofix"
+)
+
+func Test(t *testing.T) {
+ testdata := analysistest.TestData()
+ analysistest.Run(t, testdata, gofix.Analyzer, "a")
+}
diff --git a/go/analysis/passes/gofix/testdata/src/a/a.go b/go/analysis/passes/gofix/testdata/src/a/a.go
new file mode 100644
index 00000000000..3588290cfb3
--- /dev/null
+++ b/go/analysis/passes/gofix/testdata/src/a/a.go
@@ -0,0 +1,47 @@
+// Copyright 2023 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.
+
+// This file contains tests for the gofix checker.
+
+package a
+
+const one = 1
+
+//go:fix inline
+const (
+ in3 = one
+ in4 = one
+ bad1 = 1 // want `invalid //go:fix inline directive: const value is not the name of another constant`
+)
+
+//go:fix inline
+const in5,
+ in6,
+ bad2 = one, one,
+ one + 1 // want `invalid //go:fix inline directive: const value is not the name of another constant`
+
+//go:fix inline
+const (
+ a = iota // want `invalid //go:fix inline directive: const value is iota`
+ b
+ in7 = one
+)
+
+func shadow() {
+ //go:fix inline
+ const a = iota // want `invalid //go:fix inline directive: const value is iota`
+
+ const iota = 2
+
+ //go:fix inline
+ const b = iota // not an error: iota is not the builtin
+}
+
+// Type aliases
+
+//go:fix inline
+type A int // want `invalid //go:fix inline directive: not a type alias`
+
+//go:fix inline
+type E = map[[one]string][]int // want `invalid //go:fix inline directive: array types not supported`
diff --git a/go/analysis/passes/hostport/hostport.go b/go/analysis/passes/hostport/hostport.go
new file mode 100644
index 00000000000..e808b1aa1ba
--- /dev/null
+++ b/go/analysis/passes/hostport/hostport.go
@@ -0,0 +1,185 @@
+// 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 hostport defines an analyzer for calls to net.Dial with
+// addresses of the form "%s:%d" or "%s:%s", which work only with IPv4.
+package hostport
+
+import (
+ "fmt"
+ "go/ast"
+ "go/constant"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/types/typeutil"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
+)
+
+const Doc = `check format of addresses passed to net.Dial
+
+This analyzer flags code that produce network address strings using
+fmt.Sprintf, as in this example:
+
+ addr := fmt.Sprintf("%s:%d", host, 12345) // "will not work with IPv6"
+ ...
+ conn, err := net.Dial("tcp", addr) // "when passed to dial here"
+
+The analyzer suggests a fix to use the correct approach, a call to
+net.JoinHostPort:
+
+ addr := net.JoinHostPort(host, "12345")
+ ...
+ conn, err := net.Dial("tcp", addr)
+
+A similar diagnostic and fix are produced for a format string of "%s:%s".
+`
+
+var Analyzer = &analysis.Analyzer{
+ Name: "hostport",
+ Doc: Doc,
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/hostport",
+ Requires: []*analysis.Analyzer{inspect.Analyzer, typeindexanalyzer.Analyzer},
+ Run: run,
+}
+
+func run(pass *analysis.Pass) (any, error) {
+ var (
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
+ fmtSprintf = index.Object("fmt", "Sprintf")
+ )
+ if !index.Used(fmtSprintf) {
+ return nil, nil // fast path: package doesn't use fmt.Sprintf
+ }
+
+ // checkAddr reports a diagnostic (and returns true) if e
+ // is a call of the form fmt.Sprintf("%d:%d", ...).
+ // The diagnostic includes a fix.
+ //
+ // dialCall is non-nil if the Dial call is non-local
+ // but within the same file.
+ checkAddr := func(e ast.Expr, dialCall *ast.CallExpr) {
+ if call, ok := e.(*ast.CallExpr); ok && typeutil.Callee(info, call) == fmtSprintf {
+ // Examine format string.
+ formatArg := call.Args[0]
+ if tv := info.Types[formatArg]; tv.Value != nil {
+ numericPort := false
+ format := constant.StringVal(tv.Value)
+ switch format {
+ case "%s:%d":
+ // Have: fmt.Sprintf("%s:%d", host, port)
+ numericPort = true
+
+ case "%s:%s":
+ // Have: fmt.Sprintf("%s:%s", host, portStr)
+ // Keep port string as is.
+
+ default:
+ return
+ }
+
+ // Use granular edits to preserve original formatting.
+ edits := []analysis.TextEdit{
+ {
+ // Replace fmt.Sprintf with net.JoinHostPort.
+ Pos: call.Fun.Pos(),
+ End: call.Fun.End(),
+ NewText: []byte("net.JoinHostPort"),
+ },
+ {
+ // Delete format string.
+ Pos: formatArg.Pos(),
+ End: call.Args[1].Pos(),
+ },
+ }
+
+ // Turn numeric port into a string.
+ if numericPort {
+ // port => fmt.Sprintf("%d", port)
+ // 123 => "123"
+ port := call.Args[2]
+ newPort := fmt.Sprintf(`fmt.Sprintf("%%d", %s)`, port)
+ if port := info.Types[port].Value; port != nil {
+ if i, ok := constant.Int64Val(port); ok {
+ newPort = fmt.Sprintf(`"%d"`, i) // numeric constant
+ }
+ }
+
+ edits = append(edits, analysis.TextEdit{
+ Pos: port.Pos(),
+ End: port.End(),
+ NewText: []byte(newPort),
+ })
+ }
+
+ // Refer to Dial call, if not adjacent.
+ suffix := ""
+ if dialCall != nil {
+ suffix = fmt.Sprintf(" (passed to net.Dial at L%d)",
+ pass.Fset.Position(dialCall.Pos()).Line)
+ }
+
+ pass.Report(analysis.Diagnostic{
+ // Highlight the format string.
+ Pos: formatArg.Pos(),
+ End: formatArg.End(),
+ Message: fmt.Sprintf("address format %q does not work with IPv6%s", format, suffix),
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: "Replace fmt.Sprintf with net.JoinHostPort",
+ TextEdits: edits,
+ }},
+ })
+ }
+ }
+ }
+
+ // Check address argument of each call to net.Dial et al.
+ for _, callee := range []types.Object{
+ index.Object("net", "Dial"),
+ index.Object("net", "DialTimeout"),
+ index.Selection("net", "Dialer", "Dial"),
+ } {
+ for curCall := range index.Calls(callee) {
+ call := curCall.Node().(*ast.CallExpr)
+ switch address := call.Args[1].(type) {
+ case *ast.CallExpr:
+ if len(call.Args) == 2 { // avoid spread-call edge case
+ // net.Dial("tcp", fmt.Sprintf("%s:%d", ...))
+ checkAddr(address, nil)
+ }
+
+ case *ast.Ident:
+ // addr := fmt.Sprintf("%s:%d", ...)
+ // ...
+ // net.Dial("tcp", addr)
+
+ // Search for decl of addrVar within common ancestor of addrVar and Dial call.
+ // TODO(adonovan): abstract "find RHS of statement that assigns var v".
+ // TODO(adonovan): reject if there are other assignments to var v.
+ if addrVar, ok := info.Uses[address].(*types.Var); ok {
+ if curId, ok := index.Def(addrVar); ok {
+ // curIdent is the declaring ast.Ident of addr.
+ switch parent := curId.Parent().Node().(type) {
+ case *ast.AssignStmt:
+ if len(parent.Rhs) == 1 {
+ // Have: addr := fmt.Sprintf("%s:%d", ...)
+ checkAddr(parent.Rhs[0], call)
+ }
+
+ case *ast.ValueSpec:
+ if len(parent.Values) == 1 {
+ // Have: var addr = fmt.Sprintf("%s:%d", ...)
+ checkAddr(parent.Values[0], call)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return nil, nil
+}
diff --git a/gopls/internal/analysis/hostport/hostport_test.go b/go/analysis/passes/hostport/hostport_test.go
similarity index 87%
rename from gopls/internal/analysis/hostport/hostport_test.go
rename to go/analysis/passes/hostport/hostport_test.go
index 4e57a43e8d4..f3c18840fa0 100644
--- a/gopls/internal/analysis/hostport/hostport_test.go
+++ b/go/analysis/passes/hostport/hostport_test.go
@@ -8,7 +8,7 @@ import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
- "golang.org/x/tools/gopls/internal/analysis/hostport"
+ "golang.org/x/tools/go/analysis/passes/hostport"
)
func Test(t *testing.T) {
diff --git a/gopls/internal/analysis/hostport/main.go b/go/analysis/passes/hostport/main.go
similarity index 100%
rename from gopls/internal/analysis/hostport/main.go
rename to go/analysis/passes/hostport/main.go
diff --git a/gopls/internal/analysis/hostport/testdata/src/a/a.go b/go/analysis/passes/hostport/testdata/src/a/a.go
similarity index 100%
rename from gopls/internal/analysis/hostport/testdata/src/a/a.go
rename to go/analysis/passes/hostport/testdata/src/a/a.go
diff --git a/gopls/internal/analysis/hostport/testdata/src/a/a.go.golden b/go/analysis/passes/hostport/testdata/src/a/a.go.golden
similarity index 100%
rename from gopls/internal/analysis/hostport/testdata/src/a/a.go.golden
rename to go/analysis/passes/hostport/testdata/src/a/a.go.golden
diff --git a/go/analysis/passes/httpmux/cmd/httpmux/gotypesalias.go b/go/analysis/passes/httpmux/cmd/httpmux/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/httpmux/cmd/httpmux/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/httpmux/httpmux.go b/go/analysis/passes/httpmux/httpmux.go
index 58d3ed5daca..655b78fd1cb 100644
--- a/go/analysis/passes/httpmux/httpmux.go
+++ b/go/analysis/passes/httpmux/httpmux.go
@@ -9,6 +9,7 @@ import (
"go/constant"
"go/types"
"regexp"
+ "slices"
"strings"
"golang.org/x/mod/semver"
@@ -103,12 +104,7 @@ func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool {
if f.Type().(*types.Signature).Recv() == nil {
return false // not a method
}
- for _, n := range names {
- if f.Name() == n {
- return true
- }
- }
- return false // not in names
+ return slices.Contains(names, f.Name())
}
// stringConstantExpr returns expression's string constant value.
diff --git a/go/analysis/passes/ifaceassert/cmd/ifaceassert/gotypesalias.go b/go/analysis/passes/ifaceassert/cmd/ifaceassert/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/ifaceassert/cmd/ifaceassert/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/ifaceassert/ifaceassert.go b/go/analysis/passes/ifaceassert/ifaceassert.go
index 5f07ed3ffde..4022dbe7c22 100644
--- a/go/analysis/passes/ifaceassert/ifaceassert.go
+++ b/go/analysis/passes/ifaceassert/ifaceassert.go
@@ -52,7 +52,7 @@ func assertableTo(free *typeparams.Free, v, t types.Type) *types.Func {
return nil
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.TypeAssertExpr)(nil),
diff --git a/go/analysis/passes/inspect/inspect.go b/go/analysis/passes/inspect/inspect.go
index 3b121cb0ce7..ee1972f56df 100644
--- a/go/analysis/passes/inspect/inspect.go
+++ b/go/analysis/passes/inspect/inspect.go
@@ -44,6 +44,6 @@ var Analyzer = &analysis.Analyzer{
ResultType: reflect.TypeOf(new(inspector.Inspector)),
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
return inspector.New(pass.Files), nil
}
diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go
index d3181242153..64df1b106a1 100644
--- a/go/analysis/passes/loopclosure/loopclosure.go
+++ b/go/analysis/passes/loopclosure/loopclosure.go
@@ -30,7 +30,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
diff --git a/go/analysis/passes/lostcancel/cmd/lostcancel/gotypesalias.go b/go/analysis/passes/lostcancel/cmd/lostcancel/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/lostcancel/cmd/lostcancel/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/lostcancel/lostcancel.go b/go/analysis/passes/lostcancel/lostcancel.go
index f8a661aa5db..c0746789e9c 100644
--- a/go/analysis/passes/lostcancel/lostcancel.go
+++ b/go/analysis/passes/lostcancel/lostcancel.go
@@ -17,6 +17,7 @@ import (
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/cfg"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/astutil"
)
//go:embed doc.go
@@ -47,7 +48,7 @@ var contextPackage = "context"
// containing the assignment, we assume that other uses exist.
//
// checkLostCancel analyzes a single named or literal function.
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
// Fast path: bypass check if file doesn't use context.WithCancel.
if !analysisinternal.Imports(pass.Pkg, contextPackage) {
return nil, nil
@@ -83,30 +84,22 @@ func runFunc(pass *analysis.Pass, node ast.Node) {
// {FuncDecl,FuncLit,CallExpr,SelectorExpr}.
// Find the set of cancel vars to analyze.
- stack := make([]ast.Node, 0, 32)
- ast.Inspect(node, func(n ast.Node) bool {
- switch n.(type) {
- case *ast.FuncLit:
- if len(stack) > 0 {
- return false // don't stray into nested functions
- }
- case nil:
- stack = stack[:len(stack)-1] // pop
- return true
+ astutil.PreorderStack(node, nil, func(n ast.Node, stack []ast.Node) bool {
+ if _, ok := n.(*ast.FuncLit); ok && len(stack) > 0 {
+ return false // don't stray into nested functions
}
- stack = append(stack, n) // push
- // Look for [{AssignStmt,ValueSpec} CallExpr SelectorExpr]:
+ // Look for n=SelectorExpr beneath stack=[{AssignStmt,ValueSpec} CallExpr]:
//
// ctx, cancel := context.WithCancel(...)
// ctx, cancel = context.WithCancel(...)
// var ctx, cancel = context.WithCancel(...)
//
- if !isContextWithCancel(pass.TypesInfo, n) || !isCall(stack[len(stack)-2]) {
+ if !isContextWithCancel(pass.TypesInfo, n) || !isCall(stack[len(stack)-1]) {
return true
}
var id *ast.Ident // id of cancel var
- stmt := stack[len(stack)-3]
+ stmt := stack[len(stack)-2]
switch stmt := stmt.(type) {
case *ast.ValueSpec:
if len(stmt.Names) > 1 {
diff --git a/go/analysis/passes/nilfunc/nilfunc.go b/go/analysis/passes/nilfunc/nilfunc.go
index 778f7f1f8f9..fa1883b0c34 100644
--- a/go/analysis/passes/nilfunc/nilfunc.go
+++ b/go/analysis/passes/nilfunc/nilfunc.go
@@ -16,7 +16,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
"golang.org/x/tools/go/ast/inspector"
- "golang.org/x/tools/internal/typeparams"
+ "golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
@@ -30,7 +30,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
@@ -55,24 +55,8 @@ func run(pass *analysis.Pass) (interface{}, error) {
return
}
- // Only want identifiers or selector expressions.
- var obj types.Object
- switch v := e2.(type) {
- case *ast.Ident:
- obj = pass.TypesInfo.Uses[v]
- case *ast.SelectorExpr:
- obj = pass.TypesInfo.Uses[v.Sel]
- case *ast.IndexExpr, *ast.IndexListExpr:
- // Check generic functions such as "f[T1,T2]".
- x, _, _, _ := typeparams.UnpackIndexExpr(v)
- if id, ok := x.(*ast.Ident); ok {
- obj = pass.TypesInfo.Uses[id]
- }
- default:
- return
- }
-
// Only want functions.
+ obj := pass.TypesInfo.Uses[typesinternal.UsedIdent(pass.TypesInfo, e2)]
if _, ok := obj.(*types.Func); !ok {
return
}
diff --git a/go/analysis/passes/nilness/cmd/nilness/gotypesalias.go b/go/analysis/passes/nilness/cmd/nilness/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/nilness/cmd/nilness/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/nilness/nilness.go b/go/analysis/passes/nilness/nilness.go
index faaf12a9385..af61ae6088d 100644
--- a/go/analysis/passes/nilness/nilness.go
+++ b/go/analysis/passes/nilness/nilness.go
@@ -28,7 +28,7 @@ var Analyzer = &analysis.Analyzer{
Requires: []*analysis.Analyzer{buildssa.Analyzer},
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
for _, fn := range ssainput.SrcFuncs {
runFunc(pass, fn)
@@ -37,7 +37,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
}
func runFunc(pass *analysis.Pass, fn *ssa.Function) {
- reportf := func(category string, pos token.Pos, format string, args ...interface{}) {
+ reportf := func(category string, pos token.Pos, format string, args ...any) {
// We ignore nil-checking ssa.Instructions
// that don't correspond to syntax.
if pos.IsValid() {
diff --git a/go/analysis/passes/pkgfact/pkgfact.go b/go/analysis/passes/pkgfact/pkgfact.go
index 077c8780815..31748795dac 100644
--- a/go/analysis/passes/pkgfact/pkgfact.go
+++ b/go/analysis/passes/pkgfact/pkgfact.go
@@ -53,7 +53,7 @@ type pairsFact []string
func (f *pairsFact) AFact() {}
func (f *pairsFact) String() string { return "pairs(" + strings.Join(*f, ", ") + ")" }
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
result := make(map[string]string)
// At each import, print the fact from the imported
diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go
index 81600a283aa..a28ed365d1e 100644
--- a/go/analysis/passes/printf/printf.go
+++ b/go/analysis/passes/printf/printf.go
@@ -924,9 +924,14 @@ func checkPrint(pass *analysis.Pass, call *ast.CallExpr, name string) {
// The % in "abc 0.0%" couldn't be a formatting directive.
s = strings.TrimSuffix(s, "%")
if strings.Contains(s, "%") {
- m := printFormatRE.FindStringSubmatch(s)
- if m != nil {
- pass.ReportRangef(call, "%s call has possible Printf formatting directive %s", name, m[0])
+ for _, m := range printFormatRE.FindAllString(s, -1) {
+ // Allow %XX where XX are hex digits,
+ // as this is common in URLs.
+ if len(m) >= 3 && isHex(m[1]) && isHex(m[2]) {
+ continue
+ }
+ pass.ReportRangef(call, "%s call has possible Printf formatting directive %s", name, m)
+ break // report only the first one
}
}
}
@@ -992,3 +997,10 @@ func (ss stringSet) Set(flag string) error {
//
// Remove this after the 1.24 release.
var suppressNonconstants bool
+
+// isHex reports whether b is a hex digit.
+func isHex(b byte) bool {
+ return '0' <= b && b <= '9' ||
+ 'A' <= b && b <= 'F' ||
+ 'a' <= b && b <= 'f'
+}
diff --git a/go/analysis/passes/printf/testdata/src/a/a.go b/go/analysis/passes/printf/testdata/src/a/a.go
index 02ce425f8a3..da48f98f0a8 100644
--- a/go/analysis/passes/printf/testdata/src/a/a.go
+++ b/go/analysis/passes/printf/testdata/src/a/a.go
@@ -154,6 +154,8 @@ func PrintfTests() {
fmt.Println("%v", "hi") // want "fmt.Println call has possible Printf formatting directive %v"
fmt.Println("%T", "hi") // want "fmt.Println call has possible Printf formatting directive %T"
fmt.Println("%s"+" there", "hi") // want "fmt.Println call has possible Printf formatting directive %s"
+ fmt.Println("http://foo.com?q%2Fabc") // no diagnostic: %XX is excepted
+ fmt.Println("http://foo.com?q%2Fabc-%s") // want"fmt.Println call has possible Printf formatting directive %s"
fmt.Println("0.0%") // correct (trailing % couldn't be a formatting directive)
fmt.Printf("%s", "hi", 3) // want "fmt.Printf call needs 1 arg but has 2 args"
_ = fmt.Sprintf("%"+("s"), "hi", 3) // want "fmt.Sprintf call needs 1 arg but has 2 args"
diff --git a/go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare/main.go b/go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare/main.go
new file mode 100644
index 00000000000..f3f9e163913
--- /dev/null
+++ b/go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare/main.go
@@ -0,0 +1,18 @@
+// Copyright 2025 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.
+
+// The reflectvaluecompare command applies the reflectvaluecompare
+// checker to the specified packages of Go source code.
+//
+// Run with:
+//
+// $ go run ./go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare -- packages...
+package main
+
+import (
+ "golang.org/x/tools/go/analysis/passes/reflectvaluecompare"
+ "golang.org/x/tools/go/analysis/singlechecker"
+)
+
+func main() { singlechecker.Main(reflectvaluecompare.Analyzer) }
diff --git a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
index 72435b2fc7a..d0632dbdafe 100644
--- a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
+++ b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
@@ -28,7 +28,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
diff --git a/go/analysis/passes/shadow/cmd/shadow/gotypesalias.go b/go/analysis/passes/shadow/cmd/shadow/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/shadow/cmd/shadow/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/shadow/shadow.go b/go/analysis/passes/shadow/shadow.go
index 30258c991f3..8f768bb76c5 100644
--- a/go/analysis/passes/shadow/shadow.go
+++ b/go/analysis/passes/shadow/shadow.go
@@ -36,7 +36,7 @@ func init() {
Analyzer.Flags.BoolVar(&strict, "strict", strict, "whether to be strict about shadowing; can be noisy")
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
spans := make(map[types.Object]span)
diff --git a/go/analysis/passes/shift/shift.go b/go/analysis/passes/shift/shift.go
index 46b5f6d68c6..57987b3d203 100644
--- a/go/analysis/passes/shift/shift.go
+++ b/go/analysis/passes/shift/shift.go
@@ -34,7 +34,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// Do a complete pass to compute dead nodes.
diff --git a/go/analysis/passes/stdmethods/stdmethods.go b/go/analysis/passes/stdmethods/stdmethods.go
index 28f51b1ec9a..a0bdf001abd 100644
--- a/go/analysis/passes/stdmethods/stdmethods.go
+++ b/go/analysis/passes/stdmethods/stdmethods.go
@@ -66,7 +66,7 @@ var canonicalMethods = map[string]struct{ args, results []string }{
"WriteTo": {[]string{"=io.Writer"}, []string{"int64", "error"}}, // io.WriterTo
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
diff --git a/go/analysis/passes/stdversion/main.go b/go/analysis/passes/stdversion/main.go
index 2156d41e4a9..bf1c3a0b31f 100644
--- a/go/analysis/passes/stdversion/main.go
+++ b/go/analysis/passes/stdversion/main.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build ignore
-// +build ignore
package main
diff --git a/go/analysis/passes/stringintconv/cmd/stringintconv/gotypesalias.go b/go/analysis/passes/stringintconv/cmd/stringintconv/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/stringintconv/cmd/stringintconv/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/stringintconv/string.go b/go/analysis/passes/stringintconv/string.go
index f56e6ecaa29..a23721cd26f 100644
--- a/go/analysis/passes/stringintconv/string.go
+++ b/go/analysis/passes/stringintconv/string.go
@@ -70,7 +70,7 @@ func typeName(t types.Type) string {
return ""
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.File)(nil),
diff --git a/go/analysis/passes/structtag/structtag.go b/go/analysis/passes/structtag/structtag.go
index 4115ef76943..13a9997316e 100644
--- a/go/analysis/passes/structtag/structtag.go
+++ b/go/analysis/passes/structtag/structtag.go
@@ -13,6 +13,7 @@ import (
"go/types"
"path/filepath"
"reflect"
+ "slices"
"strconv"
"strings"
@@ -34,7 +35,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
@@ -88,8 +89,7 @@ var checkTagSpaces = map[string]bool{"json": true, "xml": true, "asn1": true}
// checkCanonicalFieldTag checks a single struct field tag.
func checkCanonicalFieldTag(pass *analysis.Pass, field *types.Var, tag string, seen *namesSeen) {
- switch pass.Pkg.Path() {
- case "encoding/json", "encoding/json/v2", "encoding/xml":
+ if strings.HasPrefix(pass.Pkg.Path(), "encoding/") {
// These packages know how to use their own APIs.
// Sometimes they are testing what happens to incorrect programs.
return
@@ -167,11 +167,8 @@ func checkTagDuplicates(pass *analysis.Pass, tag, key string, nearest, field *ty
if i := strings.Index(val, ","); i >= 0 {
if key == "xml" {
// Use a separate namespace for XML attributes.
- for _, opt := range strings.Split(val[i:], ",") {
- if opt == "attr" {
- key += " attribute" // Key is part of the error message.
- break
- }
+ if slices.Contains(strings.Split(val[i:], ","), "attr") {
+ key += " attribute" // Key is part of the error message.
}
}
val = val[:i]
diff --git a/go/analysis/passes/testinggoroutine/testinggoroutine.go b/go/analysis/passes/testinggoroutine/testinggoroutine.go
index fef5a6014c4..360ba0e74d8 100644
--- a/go/analysis/passes/testinggoroutine/testinggoroutine.go
+++ b/go/analysis/passes/testinggoroutine/testinggoroutine.go
@@ -17,6 +17,7 @@ import (
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
@@ -36,7 +37,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
if !analysisinternal.Imports(pass.Pkg, "testing") {
@@ -186,7 +187,7 @@ func goAsyncCall(info *types.Info, goStmt *ast.GoStmt, toDecl func(*types.Func)
call := goStmt.Call
fun := ast.Unparen(call.Fun)
- if id := funcIdent(fun); id != nil {
+ if id := typesinternal.UsedIdent(info, fun); id != nil {
if lit := funcLitInScope(id); lit != nil {
return &asyncCall{region: lit, async: goStmt, scope: nil, fun: fun}
}
@@ -217,7 +218,7 @@ func tRunAsyncCall(info *types.Info, call *ast.CallExpr) *asyncCall {
return &asyncCall{region: lit, async: call, scope: lit, fun: fun}
}
- if id := funcIdent(fun); id != nil {
+ if id := typesinternal.UsedIdent(info, fun); id != nil {
if lit := funcLitInScope(id); lit != nil { // function lit in variable?
return &asyncCall{region: lit, async: call, scope: lit, fun: fun}
}
diff --git a/go/analysis/passes/testinggoroutine/util.go b/go/analysis/passes/testinggoroutine/util.go
index 027c99e6b0f..db2e5f76d14 100644
--- a/go/analysis/passes/testinggoroutine/util.go
+++ b/go/analysis/passes/testinggoroutine/util.go
@@ -7,8 +7,7 @@ package testinggoroutine
import (
"go/ast"
"go/types"
-
- "golang.org/x/tools/internal/typeparams"
+ "slices"
)
// AST and types utilities that not specific to testinggoroutines.
@@ -48,25 +47,7 @@ func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool {
if f.Type().(*types.Signature).Recv() == nil {
return false
}
- for _, n := range names {
- if f.Name() == n {
- return true
- }
- }
- return false
-}
-
-func funcIdent(fun ast.Expr) *ast.Ident {
- switch fun := ast.Unparen(fun).(type) {
- case *ast.IndexExpr, *ast.IndexListExpr:
- x, _, _, _ := typeparams.UnpackIndexExpr(fun) // necessary?
- id, _ := x.(*ast.Ident)
- return id
- case *ast.Ident:
- return fun
- default:
- return nil
- }
+ return slices.Contains(names, f.Name())
}
// funcLitInScope returns a FuncLit that id is at least initially assigned to.
diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go
index 285b34218c3..9f59006ebb2 100644
--- a/go/analysis/passes/tests/tests.go
+++ b/go/analysis/passes/tests/tests.go
@@ -47,7 +47,7 @@ var acceptedFuzzTypes = []types.Type{
types.NewSlice(types.Universe.Lookup("byte").Type()),
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
for _, f := range pass.Files {
if !strings.HasSuffix(pass.Fset.File(f.FileStart).Name(), "_test.go") {
continue
diff --git a/go/analysis/passes/unmarshal/cmd/unmarshal/gotypesalias.go b/go/analysis/passes/unmarshal/cmd/unmarshal/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/unmarshal/cmd/unmarshal/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/unreachable/testdata/src/a/a.go b/go/analysis/passes/unreachable/testdata/src/a/a.go
index b283fd00b9a..136a07caa21 100644
--- a/go/analysis/passes/unreachable/testdata/src/a/a.go
+++ b/go/analysis/passes/unreachable/testdata/src/a/a.go
@@ -2118,11 +2118,6 @@ var _ = func() int {
println() // ok
}
-var _ = func() {
- // goto without label used to panic
- goto
-}
-
func _() int {
// Empty switch tag with non-bool case value used to panic.
switch {
diff --git a/go/analysis/passes/unreachable/testdata/src/a/a.go.golden b/go/analysis/passes/unreachable/testdata/src/a/a.go.golden
index 40494030423..79cb89d4181 100644
--- a/go/analysis/passes/unreachable/testdata/src/a/a.go.golden
+++ b/go/analysis/passes/unreachable/testdata/src/a/a.go.golden
@@ -2082,11 +2082,6 @@ var _ = func() int {
println() // ok
}
-var _ = func() {
- // goto without label used to panic
- goto
-}
-
func _() int {
// Empty switch tag with non-bool case value used to panic.
switch {
diff --git a/go/analysis/passes/unreachable/unreachable.go b/go/analysis/passes/unreachable/unreachable.go
index b810db7ee95..317f034992b 100644
--- a/go/analysis/passes/unreachable/unreachable.go
+++ b/go/analysis/passes/unreachable/unreachable.go
@@ -30,7 +30,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
@@ -188,6 +188,9 @@ func (d *deadState) findDead(stmt ast.Stmt) {
case *ast.EmptyStmt:
// do not warn about unreachable empty statements
default:
+ // (This call to pass.Report is a frequent source
+ // of diagnostics beyond EOF in a truncated file;
+ // see #71659.)
d.pass.Report(analysis.Diagnostic{
Pos: stmt.Pos(),
End: stmt.End(),
diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go
index fb5b944faad..57c6da64ff3 100644
--- a/go/analysis/passes/unsafeptr/unsafeptr.go
+++ b/go/analysis/passes/unsafeptr/unsafeptr.go
@@ -30,7 +30,7 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
diff --git a/go/analysis/passes/unusedresult/cmd/unusedresult/gotypesalias.go b/go/analysis/passes/unusedresult/cmd/unusedresult/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/analysis/passes/unusedresult/cmd/unusedresult/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go
index d7cc1e6ae2c..932f1347e56 100644
--- a/go/analysis/passes/unusedresult/unusedresult.go
+++ b/go/analysis/passes/unusedresult/unusedresult.go
@@ -85,7 +85,7 @@ func init() {
"comma-separated list of names of methods of type func() string whose results must be used")
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// Split functions into (pkg, name) pairs to save allocation later.
@@ -130,9 +130,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
}
// func() string
-var sigNoArgsStringResult = types.NewSignature(nil, nil,
- types.NewTuple(types.NewParam(token.NoPos, nil, "", types.Typ[types.String])),
- false)
+var sigNoArgsStringResult = types.NewSignatureType(nil, nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "", types.Typ[types.String])), false)
type stringSetFlag map[string]bool
diff --git a/go/analysis/passes/usesgenerics/usesgenerics.go b/go/analysis/passes/usesgenerics/usesgenerics.go
index 5c5df3a79a0..b7ff3ad6877 100644
--- a/go/analysis/passes/usesgenerics/usesgenerics.go
+++ b/go/analysis/passes/usesgenerics/usesgenerics.go
@@ -53,7 +53,7 @@ type featuresFact struct {
func (f *featuresFact) AFact() {}
func (f *featuresFact) String() string { return f.Features.String() }
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
direct := genericfeatures.ForPackage(inspect, pass.TypesInfo)
diff --git a/go/analysis/unitchecker/main.go b/go/analysis/unitchecker/main.go
index 4374e7bf945..246be909249 100644
--- a/go/analysis/unitchecker/main.go
+++ b/go/analysis/unitchecker/main.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build ignore
-// +build ignore
// This file provides an example command for static checkers
// conforming to the golang.org/x/tools/go/analysis API.
diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go
index 82c3db6a39d..a1ee80388b6 100644
--- a/go/analysis/unitchecker/unitchecker.go
+++ b/go/analysis/unitchecker/unitchecker.go
@@ -287,7 +287,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
// Also build a map to hold working state and result.
type action struct {
once sync.Once
- result interface{}
+ result any
err error
usesFacts bool // (transitively uses)
diagnostics []analysis.Diagnostic
@@ -337,7 +337,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
// The inputs to this analysis are the
// results of its prerequisites.
- inputs := make(map[*analysis.Analyzer]interface{})
+ inputs := make(map[*analysis.Analyzer]any)
var failed []string
for _, req := range a.Requires {
reqact := exec(req)
diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go
index 173d76348f7..6c3bba6793e 100644
--- a/go/analysis/unitchecker/unitchecker_test.go
+++ b/go/analysis/unitchecker/unitchecker_test.go
@@ -59,7 +59,7 @@ func testIntegration(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a
func _() {
diff --git a/go/analysis/unitchecker/vet_std_test.go b/go/analysis/unitchecker/vet_std_test.go
index a79224c7188..a761bc02f31 100644
--- a/go/analysis/unitchecker/vet_std_test.go
+++ b/go/analysis/unitchecker/vet_std_test.go
@@ -5,6 +5,7 @@
package unitchecker_test
import (
+ "go/version"
"os"
"os/exec"
"runtime"
@@ -24,6 +25,8 @@ import (
"golang.org/x/tools/go/analysis/passes/directive"
"golang.org/x/tools/go/analysis/passes/errorsas"
"golang.org/x/tools/go/analysis/passes/framepointer"
+ "golang.org/x/tools/go/analysis/passes/gofix"
+ "golang.org/x/tools/go/analysis/passes/hostport"
"golang.org/x/tools/go/analysis/passes/httpresponse"
"golang.org/x/tools/go/analysis/passes/ifaceassert"
"golang.org/x/tools/go/analysis/passes/loopclosure"
@@ -62,7 +65,9 @@ func vet() {
directive.Analyzer,
errorsas.Analyzer,
framepointer.Analyzer,
+ gofix.Analyzer,
httpresponse.Analyzer,
+ hostport.Analyzer,
ifaceassert.Analyzer,
loopclosure.Analyzer,
lostcancel.Analyzer,
@@ -91,8 +96,14 @@ func TestVetStdlib(t *testing.T) {
if testing.Short() {
t.Skip("skipping in -short mode")
}
- if version := runtime.Version(); !strings.HasPrefix(version, "devel") {
- t.Skipf("This test is only wanted on development branches where code can be easily fixed. Skipping because runtime.Version=%q.", version)
+ if builder := os.Getenv("GO_BUILDER_NAME"); builder != "" && !strings.HasPrefix(builder, "x_tools-gotip-") {
+ // Run on builders like x_tools-gotip-linux-amd64-longtest,
+ // skip on others like x_tools-go1.24-linux-amd64-longtest.
+ t.Skipf("This test is only wanted on development branches where code can be easily fixed. Skipping on non-gotip builder %q.", builder)
+ } else if v := runtime.Version(); !strings.Contains(v, "devel") || version.Compare(v, version.Lang(v)) != 0 {
+ // Run on versions like "go1.25-devel_9ce47e66e8 Wed Mar 26 03:48:50 2025 -0700",
+ // skip on others like "go1.24.2" or "go1.24.2-devel_[…]".
+ t.Skipf("This test is only wanted on development versions where code can be easily fixed. Skipping on non-gotip version %q.", v)
}
cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "std")
diff --git a/go/analysis/validate.go b/go/analysis/validate.go
index 4f2c4045622..14539392116 100644
--- a/go/analysis/validate.go
+++ b/go/analysis/validate.go
@@ -63,7 +63,7 @@ func Validate(analyzers []*Analyzer) error {
return fmt.Errorf("fact type %s registered by two analyzers: %v, %v",
t, a, prev)
}
- if t.Kind() != reflect.Ptr {
+ if t.Kind() != reflect.Pointer {
return fmt.Errorf("%s: fact type %s is not a pointer", a, t)
}
factTypes[t] = a
diff --git a/go/analysis/validate_test.go b/go/analysis/validate_test.go
index 7f4ee2c05b9..b192ef0a3c0 100644
--- a/go/analysis/validate_test.go
+++ b/go/analysis/validate_test.go
@@ -11,7 +11,7 @@ import (
func TestValidate(t *testing.T) {
var (
- run = func(p *Pass) (interface{}, error) {
+ run = func(p *Pass) (any, error) {
return nil, nil
}
dependsOnSelf = &Analyzer{
@@ -130,7 +130,7 @@ func TestCycleInRequiresGraphErrorMessage(t *testing.T) {
func TestValidateEmptyDoc(t *testing.T) {
withoutDoc := &Analyzer{
Name: "withoutDoc",
- Run: func(p *Pass) (interface{}, error) {
+ Run: func(p *Pass) (any, error) {
return nil, nil
},
}
diff --git a/go/ast/astutil/imports.go b/go/ast/astutil/imports.go
index a6b5ed0a893..5e5601aa467 100644
--- a/go/ast/astutil/imports.go
+++ b/go/ast/astutil/imports.go
@@ -9,6 +9,7 @@ import (
"fmt"
"go/ast"
"go/token"
+ "slices"
"strconv"
"strings"
)
@@ -186,7 +187,7 @@ func AddNamedImport(fset *token.FileSet, f *ast.File, name, path string) (added
spec.(*ast.ImportSpec).Path.ValuePos = first.Pos()
first.Specs = append(first.Specs, spec)
}
- f.Decls = append(f.Decls[:i], f.Decls[i+1:]...)
+ f.Decls = slices.Delete(f.Decls, i, i+1)
i--
}
diff --git a/go/ast/astutil/rewrite.go b/go/ast/astutil/rewrite.go
index 58934f76633..5c8dbbb7a35 100644
--- a/go/ast/astutil/rewrite.go
+++ b/go/ast/astutil/rewrite.go
@@ -183,7 +183,7 @@ type application struct {
func (a *application) apply(parent ast.Node, name string, iter *iterator, n ast.Node) {
// convert typed nil into untyped nil
- if v := reflect.ValueOf(n); v.Kind() == reflect.Ptr && v.IsNil() {
+ if v := reflect.ValueOf(n); v.Kind() == reflect.Pointer && v.IsNil() {
n = nil
}
diff --git a/go/ast/astutil/rewrite_test.go b/go/ast/astutil/rewrite_test.go
index 57136a07cab..2e1c77034c8 100644
--- a/go/ast/astutil/rewrite_test.go
+++ b/go/ast/astutil/rewrite_test.go
@@ -244,7 +244,6 @@ func vardecl(name, typ string) *ast.GenDecl {
func TestRewrite(t *testing.T) {
t.Run("*", func(t *testing.T) {
for _, test := range rewriteTests {
- test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
fset := token.NewFileSet()
diff --git a/go/ast/astutil/util.go b/go/ast/astutil/util.go
index ca71e3e1055..c820b208499 100644
--- a/go/ast/astutil/util.go
+++ b/go/ast/astutil/util.go
@@ -8,4 +8,6 @@ import "go/ast"
// Unparen returns e with any enclosing parentheses stripped.
// Deprecated: use [ast.Unparen].
+//
+//go:fix inline
func Unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) }
diff --git a/go/ast/inspector/inspector.go b/go/ast/inspector/inspector.go
index 0d5050fe405..674490a65b4 100644
--- a/go/ast/inspector/inspector.go
+++ b/go/ast/inspector/inspector.go
@@ -10,6 +10,7 @@
// builds a list of push/pop events and their node type. Subsequent
// method calls that request a traversal scan this list, rather than walk
// the AST, and perform type filtering using efficient bit sets.
+// This representation is sometimes called a "balanced parenthesis tree."
//
// Experiments suggest the inspector's traversals are about 2.5x faster
// than ast.Inspect, but it may take around 5 traversals for this
@@ -47,9 +48,10 @@ type Inspector struct {
events []event
}
-//go:linkname events
+//go:linkname events golang.org/x/tools/go/ast/inspector.events
func events(in *Inspector) []event { return in.events }
+//go:linkname packEdgeKindAndIndex golang.org/x/tools/go/ast/inspector.packEdgeKindAndIndex
func packEdgeKindAndIndex(ek edge.Kind, index int) int32 {
return int32(uint32(index+1)<<7 | uint32(ek))
}
@@ -57,7 +59,7 @@ func packEdgeKindAndIndex(ek edge.Kind, index int) int32 {
// unpackEdgeKindAndIndex unpacks the edge kind and edge index (within
// an []ast.Node slice) from the parent field of a pop event.
//
-//go:linkname unpackEdgeKindAndIndex
+//go:linkname unpackEdgeKindAndIndex golang.org/x/tools/go/ast/inspector.unpackEdgeKindAndIndex
func unpackEdgeKindAndIndex(x int32) (edge.Kind, int) {
// The "parent" field of a pop node holds the
// edge Kind in the lower 7 bits and the index+1
diff --git a/go/ast/inspector/typeof.go b/go/ast/inspector/typeof.go
index 97784484578..e936c67c985 100644
--- a/go/ast/inspector/typeof.go
+++ b/go/ast/inspector/typeof.go
@@ -217,7 +217,7 @@ func typeOf(n ast.Node) uint64 {
return 0
}
-//go:linkname maskOf
+//go:linkname maskOf golang.org/x/tools/go/ast/inspector.maskOf
func maskOf(nodes []ast.Node) uint64 {
if len(nodes) == 0 {
return math.MaxUint64 // match all node types
diff --git a/go/buildutil/allpackages.go b/go/buildutil/allpackages.go
index dfb8cd6c7b0..32886a7175f 100644
--- a/go/buildutil/allpackages.go
+++ b/go/buildutil/allpackages.go
@@ -52,7 +52,6 @@ func ForEachPackage(ctxt *build.Context, found func(importPath string, err error
var wg sync.WaitGroup
for _, root := range ctxt.SrcDirs() {
- root := root
wg.Add(1)
go func() {
allPackages(ctxt, root, ch)
@@ -107,7 +106,6 @@ func allPackages(ctxt *build.Context, root string, ch chan<- item) {
ch <- item{pkg, err}
}
for _, fi := range files {
- fi := fi
if fi.IsDir() {
wg.Add(1)
go func() {
diff --git a/go/buildutil/allpackages_test.go b/go/buildutil/allpackages_test.go
index 6af86771104..2df5f27e223 100644
--- a/go/buildutil/allpackages_test.go
+++ b/go/buildutil/allpackages_test.go
@@ -5,7 +5,6 @@
// Incomplete source tree on Android.
//go:build !android
-// +build !android
package buildutil_test
diff --git a/go/buildutil/fakecontext.go b/go/buildutil/fakecontext.go
index 763d18809b4..1f75141d504 100644
--- a/go/buildutil/fakecontext.go
+++ b/go/buildutil/fakecontext.go
@@ -95,7 +95,7 @@ func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
type fakeFileInfo string
func (fi fakeFileInfo) Name() string { return string(fi) }
-func (fakeFileInfo) Sys() interface{} { return nil }
+func (fakeFileInfo) Sys() any { return nil }
func (fakeFileInfo) ModTime() time.Time { return time.Time{} }
func (fakeFileInfo) IsDir() bool { return false }
func (fakeFileInfo) Size() int64 { return 0 }
@@ -104,7 +104,7 @@ func (fakeFileInfo) Mode() os.FileMode { return 0644 }
type fakeDirInfo string
func (fd fakeDirInfo) Name() string { return string(fd) }
-func (fakeDirInfo) Sys() interface{} { return nil }
+func (fakeDirInfo) Sys() any { return nil }
func (fakeDirInfo) ModTime() time.Time { return time.Time{} }
func (fakeDirInfo) IsDir() bool { return true }
func (fakeDirInfo) Size() int64 { return 0 }
diff --git a/go/buildutil/tags.go b/go/buildutil/tags.go
index 32c8d1424d2..410c8e72d48 100644
--- a/go/buildutil/tags.go
+++ b/go/buildutil/tags.go
@@ -51,7 +51,7 @@ func (v *TagsFlag) Set(s string) error {
return nil
}
-func (v *TagsFlag) Get() interface{} { return *v }
+func (v *TagsFlag) Get() any { return *v }
func splitQuotedFields(s string) ([]string, error) {
// See $GOROOT/src/cmd/internal/quoted/quoted.go (Split)
diff --git a/go/callgraph/cha/cha_test.go b/go/callgraph/cha/cha_test.go
index 5ac64e17244..922541d6c56 100644
--- a/go/callgraph/cha/cha_test.go
+++ b/go/callgraph/cha/cha_test.go
@@ -5,7 +5,6 @@
// No testdata on Android.
//go:build !android
-// +build !android
package cha_test
@@ -41,7 +40,7 @@ var inputs = []string{
func expectation(f *ast.File) (string, token.Pos) {
for _, c := range f.Comments {
text := strings.TrimSpace(c.Text())
- if t := strings.TrimPrefix(text, "WANT:\n"); t != text {
+ if t, ok := strings.CutPrefix(text, "WANT:\n"); ok {
return t, c.Pos()
}
}
diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go
index b489b0178c8..224c0b96ce0 100644
--- a/go/callgraph/rta/rta.go
+++ b/go/callgraph/rta/rta.go
@@ -371,7 +371,7 @@ func (r *rta) interfaces(C types.Type) []*types.Interface {
// Ascertain set of interfaces C implements
// and update the 'implements' relation.
- r.interfaceTypes.Iterate(func(I types.Type, v interface{}) {
+ r.interfaceTypes.Iterate(func(I types.Type, v any) {
iinfo := v.(*interfaceTypeInfo)
if I := types.Unalias(I).(*types.Interface); implements(cinfo, iinfo) {
iinfo.implementations = append(iinfo.implementations, C)
@@ -400,7 +400,7 @@ func (r *rta) implementations(I *types.Interface) []types.Type {
// Ascertain set of concrete types that implement I
// and update the 'implements' relation.
- r.concreteTypes.Iterate(func(C types.Type, v interface{}) {
+ r.concreteTypes.Iterate(func(C types.Type, v any) {
cinfo := v.(*concreteTypeInfo)
if implements(cinfo, iinfo) {
cinfo.implements = append(cinfo.implements, I)
diff --git a/go/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go
index 74e77b01291..8cfc73ee4db 100644
--- a/go/callgraph/rta/rta_test.go
+++ b/go/callgraph/rta/rta_test.go
@@ -5,7 +5,6 @@
// No testdata on Android.
//go:build !android
-// +build !android
package rta_test
@@ -106,7 +105,7 @@ func check(t *testing.T, f *ast.File, pkg *ssa.Package, res *rta.Result) {
expectation := func(f *ast.File) (string, int) {
for _, c := range f.Comments {
text := strings.TrimSpace(c.Text())
- if t := strings.TrimPrefix(text, "WANT:\n"); t != text {
+ if t, ok := strings.CutPrefix(text, "WANT:\n"); ok {
return t, tokFile.Line(c.Pos())
}
}
@@ -135,7 +134,7 @@ func check(t *testing.T, f *ast.File, pkg *ssa.Package, res *rta.Result) {
// A leading "!" negates the assertion.
sense := true
- if rest := strings.TrimPrefix(line, "!"); rest != line {
+ if rest, ok := strings.CutPrefix(line, "!"); ok {
sense = false
line = strings.TrimSpace(rest)
if line == "" {
@@ -221,7 +220,7 @@ func check(t *testing.T, f *ast.File, pkg *ssa.Package, res *rta.Result) {
// Check runtime types.
{
got := make(stringset)
- res.RuntimeTypes.Iterate(func(key types.Type, value interface{}) {
+ res.RuntimeTypes.Iterate(func(key types.Type, value any) {
if !value.(bool) { // accessible to reflection
typ := types.TypeString(types.Unalias(key), types.RelativeTo(pkg.Pkg))
got[typ] = true
diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go
index c13b8a5e6cb..26225e7db37 100644
--- a/go/callgraph/vta/graph.go
+++ b/go/callgraph/vta/graph.go
@@ -8,6 +8,7 @@ import (
"fmt"
"go/token"
"go/types"
+ "iter"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/types/typeutil"
@@ -270,7 +271,7 @@ func (g *vtaGraph) numNodes() int {
return len(g.idx)
}
-func (g *vtaGraph) successors(x idx) func(yield func(y idx) bool) {
+func (g *vtaGraph) successors(x idx) iter.Seq[idx] {
return func(yield func(y idx) bool) {
for y := range g.m[x] {
if !yield(y) {
@@ -633,12 +634,12 @@ func (b *builder) call(c ssa.CallInstruction) {
return
}
- siteCallees(c, b.callees)(func(f *ssa.Function) bool {
+ for f := range siteCallees(c, b.callees) {
addArgumentFlows(b, c, f)
site, ok := c.(ssa.Value)
if !ok {
- return true // go or defer
+ continue // go or defer
}
results := f.Signature.Results()
@@ -653,8 +654,7 @@ func (b *builder) call(c ssa.CallInstruction) {
b.addInFlowEdge(resultVar{f: f, index: i}, local)
}
}
- return true
- })
+ }
}
func addArgumentFlows(b *builder, c ssa.CallInstruction, f *ssa.Function) {
diff --git a/go/callgraph/vta/graph_test.go b/go/callgraph/vta/graph_test.go
index 9e780c7e4e2..725749ea6ab 100644
--- a/go/callgraph/vta/graph_test.go
+++ b/go/callgraph/vta/graph_test.go
@@ -148,7 +148,9 @@ func TestVtaGraph(t *testing.T) {
{n4, 0},
} {
sl := 0
- g.successors(g.idx[test.n])(func(_ idx) bool { sl++; return true })
+ for range g.successors(g.idx[test.n]) {
+ sl++
+ }
if sl != test.l {
t.Errorf("want %d successors; got %d", test.l, sl)
}
@@ -163,10 +165,10 @@ func vtaGraphStr(g *vtaGraph) []string {
var vgs []string
for n := 0; n < g.numNodes(); n++ {
var succStr []string
- g.successors(idx(n))(func(s idx) bool {
+ for s := range g.successors(idx(n)) {
succStr = append(succStr, g.node[s].String())
- return true
- })
+ }
+
sort.Strings(succStr)
entry := fmt.Sprintf("%v -> %v", g.node[n].String(), strings.Join(succStr, ", "))
vgs = append(vgs, removeModulePrefix(entry))
diff --git a/go/callgraph/vta/helpers_test.go b/go/callgraph/vta/helpers_test.go
index 59a9277f759..be5e756dcd5 100644
--- a/go/callgraph/vta/helpers_test.go
+++ b/go/callgraph/vta/helpers_test.go
@@ -28,7 +28,7 @@ import (
func want(f *ast.File) []string {
for _, c := range f.Comments {
text := strings.TrimSpace(c.Text())
- if t := strings.TrimPrefix(text, "WANT:\n"); t != text {
+ if t, ok := strings.CutPrefix(text, "WANT:\n"); ok {
return strings.Split(t, "\n")
}
}
diff --git a/go/callgraph/vta/internal/trie/bits_test.go b/go/callgraph/vta/internal/trie/bits_test.go
index 07784cdffac..f6e510eccd0 100644
--- a/go/callgraph/vta/internal/trie/bits_test.go
+++ b/go/callgraph/vta/internal/trie/bits_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.13
-// +build go1.13
package trie
diff --git a/go/callgraph/vta/internal/trie/builder.go b/go/callgraph/vta/internal/trie/builder.go
index c814c039f72..bdd39397ec6 100644
--- a/go/callgraph/vta/internal/trie/builder.go
+++ b/go/callgraph/vta/internal/trie/builder.go
@@ -14,13 +14,13 @@ package trie
//
// Collisions functions may be applied whenever a value is inserted
// or two maps are merged, or intersected.
-type Collision func(lhs interface{}, rhs interface{}) interface{}
+type Collision func(lhs any, rhs any) any
// TakeLhs always returns the left value in a collision.
-func TakeLhs(lhs, rhs interface{}) interface{} { return lhs }
+func TakeLhs(lhs, rhs any) any { return lhs }
// TakeRhs always returns the right hand side in a collision.
-func TakeRhs(lhs, rhs interface{}) interface{} { return rhs }
+func TakeRhs(lhs, rhs any) any { return rhs }
// Builder creates new Map. Each Builder has a unique Scope.
//
@@ -78,7 +78,7 @@ func (b *Builder) Empty() Map { return Map{b.Scope(), b.empty} }
// if _, ok := m[k]; ok { m[k] = c(m[k], v} else { m[k] = v}
//
// An insertion or update happened whenever Insert(m, ...) != m .
-func (b *Builder) InsertWith(c Collision, m Map, k uint64, v interface{}) Map {
+func (b *Builder) InsertWith(c Collision, m Map, k uint64, v any) Map {
m = b.Clone(m)
return Map{b.Scope(), b.insert(c, m.n, b.mkLeaf(key(k), v), false)}
}
@@ -92,7 +92,7 @@ func (b *Builder) InsertWith(c Collision, m Map, k uint64, v interface{}) Map {
// if _, ok := m[k]; ok { m[k] = val }
//
// This is equivalent to b.Merge(m, b.Create({k: v})).
-func (b *Builder) Insert(m Map, k uint64, v interface{}) Map {
+func (b *Builder) Insert(m Map, k uint64, v any) Map {
return b.InsertWith(TakeLhs, m, k, v)
}
@@ -100,7 +100,7 @@ func (b *Builder) Insert(m Map, k uint64, v interface{}) Map {
// updating a map[uint64]interface{} by:
//
// m[key] = val
-func (b *Builder) Update(m Map, key uint64, val interface{}) Map {
+func (b *Builder) Update(m Map, key uint64, val any) Map {
return b.InsertWith(TakeRhs, m, key, val)
}
@@ -185,14 +185,14 @@ func (b *Builder) MutEmpty() MutMap {
// Insert an element into the map using the collision function for the builder.
// Returns true if the element was inserted.
-func (mm *MutMap) Insert(k uint64, v interface{}) bool {
+func (mm *MutMap) Insert(k uint64, v any) bool {
old := mm.M
mm.M = mm.B.Insert(old, k, v)
return old != mm.M
}
// Updates an element in the map. Returns true if the map was updated.
-func (mm *MutMap) Update(k uint64, v interface{}) bool {
+func (mm *MutMap) Update(k uint64, v any) bool {
old := mm.M
mm.M = mm.B.Update(old, k, v)
return old != mm.M
@@ -221,7 +221,7 @@ func (mm *MutMap) Intersect(other Map) bool {
return old != mm.M
}
-func (b *Builder) Create(m map[uint64]interface{}) Map {
+func (b *Builder) Create(m map[uint64]any) Map {
var leaves []*leaf
for k, v := range m {
leaves = append(leaves, b.mkLeaf(key(k), v))
@@ -259,7 +259,7 @@ func (b *Builder) create(leaves []*leaf) node {
}
// mkLeaf returns the hash-consed representative of (k, v) in the current scope.
-func (b *Builder) mkLeaf(k key, v interface{}) *leaf {
+func (b *Builder) mkLeaf(k key, v any) *leaf {
rep, ok := b.leaves[leaf{k, v}]
if !ok {
rep = &leaf{k, v} // heap-allocated copy
diff --git a/go/callgraph/vta/internal/trie/op_test.go b/go/callgraph/vta/internal/trie/op_test.go
index ba0d5be71a9..535e7ac2775 100644
--- a/go/callgraph/vta/internal/trie/op_test.go
+++ b/go/callgraph/vta/internal/trie/op_test.go
@@ -12,6 +12,7 @@ import (
"time"
"golang.org/x/tools/go/callgraph/vta/internal/trie"
+ "maps"
)
// This file tests trie.Map by cross checking operations on a collection of
@@ -21,13 +22,13 @@ import (
// mapCollection is effectively a []map[uint64]interface{}.
// These support operations being applied to the i'th maps.
type mapCollection interface {
- Elements() []map[uint64]interface{}
+ Elements() []map[uint64]any
DeepEqual(l, r int) bool
- Lookup(id int, k uint64) (interface{}, bool)
+ Lookup(id int, k uint64) (any, bool)
- Insert(id int, k uint64, v interface{})
- Update(id int, k uint64, v interface{})
+ Insert(id int, k uint64, v any)
+ Update(id int, k uint64, v any)
Remove(id int, k uint64)
Intersect(l int, r int)
Merge(l int, r int)
@@ -86,19 +87,19 @@ type trieCollection struct {
tries []trie.MutMap
}
-func (c *trieCollection) Elements() []map[uint64]interface{} {
- var maps []map[uint64]interface{}
+func (c *trieCollection) Elements() []map[uint64]any {
+ var maps []map[uint64]any
for _, m := range c.tries {
maps = append(maps, trie.Elems(m.M))
}
return maps
}
-func (c *trieCollection) Eq(id int, m map[uint64]interface{}) bool {
+func (c *trieCollection) Eq(id int, m map[uint64]any) bool {
elems := trie.Elems(c.tries[id].M)
return !reflect.DeepEqual(elems, m)
}
-func (c *trieCollection) Lookup(id int, k uint64) (interface{}, bool) {
+func (c *trieCollection) Lookup(id int, k uint64) (any, bool) {
return c.tries[id].M.Lookup(k)
}
func (c *trieCollection) DeepEqual(l, r int) bool {
@@ -109,11 +110,11 @@ func (c *trieCollection) Add() {
c.tries = append(c.tries, c.b.MutEmpty())
}
-func (c *trieCollection) Insert(id int, k uint64, v interface{}) {
+func (c *trieCollection) Insert(id int, k uint64, v any) {
c.tries[id].Insert(k, v)
}
-func (c *trieCollection) Update(id int, k uint64, v interface{}) {
+func (c *trieCollection) Update(id int, k uint64, v any) {
c.tries[id].Update(k, v)
}
@@ -140,7 +141,7 @@ func (c *trieCollection) Assign(l, r int) {
c.tries[l] = c.tries[r]
}
-func average(x interface{}, y interface{}) interface{} {
+func average(x any, y any) any {
if x, ok := x.(float32); ok {
if y, ok := y.(float32); ok {
return (x + y) / 2.0
@@ -149,13 +150,13 @@ func average(x interface{}, y interface{}) interface{} {
return x
}
-type builtinCollection []map[uint64]interface{}
+type builtinCollection []map[uint64]any
-func (c builtinCollection) Elements() []map[uint64]interface{} {
+func (c builtinCollection) Elements() []map[uint64]any {
return c
}
-func (c builtinCollection) Lookup(id int, k uint64) (interface{}, bool) {
+func (c builtinCollection) Lookup(id int, k uint64) (any, bool) {
v, ok := c[id][k]
return v, ok
}
@@ -163,13 +164,13 @@ func (c builtinCollection) DeepEqual(l, r int) bool {
return reflect.DeepEqual(c[l], c[r])
}
-func (c builtinCollection) Insert(id int, k uint64, v interface{}) {
+func (c builtinCollection) Insert(id int, k uint64, v any) {
if _, ok := c[id][k]; !ok {
c[id][k] = v
}
}
-func (c builtinCollection) Update(id int, k uint64, v interface{}) {
+func (c builtinCollection) Update(id int, k uint64, v any) {
c[id][k] = v
}
@@ -178,7 +179,7 @@ func (c builtinCollection) Remove(id int, k uint64) {
}
func (c builtinCollection) Intersect(l int, r int) {
- result := map[uint64]interface{}{}
+ result := map[uint64]any{}
for k, v := range c[l] {
if _, ok := c[r][k]; ok {
result[k] = v
@@ -188,18 +189,14 @@ func (c builtinCollection) Intersect(l int, r int) {
}
func (c builtinCollection) Merge(l int, r int) {
- result := map[uint64]interface{}{}
- for k, v := range c[r] {
- result[k] = v
- }
- for k, v := range c[l] {
- result[k] = v
- }
+ result := map[uint64]any{}
+ maps.Copy(result, c[r])
+ maps.Copy(result, c[l])
c[l] = result
}
func (c builtinCollection) Average(l int, r int) {
- avg := map[uint64]interface{}{}
+ avg := map[uint64]any{}
for k, lv := range c[l] {
if rv, ok := c[r][k]; ok {
avg[k] = average(lv, rv)
@@ -216,15 +213,13 @@ func (c builtinCollection) Average(l int, r int) {
}
func (c builtinCollection) Assign(l, r int) {
- m := map[uint64]interface{}{}
- for k, v := range c[r] {
- m[k] = v
- }
+ m := map[uint64]any{}
+ maps.Copy(m, c[r])
c[l] = m
}
func (c builtinCollection) Clear(id int) {
- c[id] = map[uint64]interface{}{}
+ c[id] = map[uint64]any{}
}
func newTriesCollection(size int) *trieCollection {
@@ -232,7 +227,7 @@ func newTriesCollection(size int) *trieCollection {
b: trie.NewBuilder(),
tries: make([]trie.MutMap, size),
}
- for i := 0; i < size; i++ {
+ for i := range size {
tc.tries[i] = tc.b.MutEmpty()
}
return tc
@@ -240,8 +235,8 @@ func newTriesCollection(size int) *trieCollection {
func newMapsCollection(size int) *builtinCollection {
maps := make(builtinCollection, size)
- for i := 0; i < size; i++ {
- maps[i] = map[uint64]interface{}{}
+ for i := range size {
+ maps[i] = map[uint64]any{}
}
return &maps
}
@@ -255,9 +250,9 @@ type operation struct {
}
// Apply the operation to maps.
-func (op operation) Apply(maps mapCollection) interface{} {
+func (op operation) Apply(maps mapCollection) any {
type lookupresult struct {
- v interface{}
+ v any
ok bool
}
switch op.code {
@@ -290,7 +285,7 @@ func (op operation) Apply(maps mapCollection) interface{} {
func distribution(dist map[opCode]int) []opCode {
var codes []opCode
for op, n := range dist {
- for i := 0; i < n; i++ {
+ for range n {
codes = append(codes, op)
}
}
@@ -326,7 +321,7 @@ func randOperator(r *rand.Rand, opts options) operation {
func randOperators(r *rand.Rand, numops int, opts options) []operation {
ops := make([]operation, numops)
- for i := 0; i < numops; i++ {
+ for i := range numops {
ops[i] = randOperator(r, opts)
}
return ops
diff --git a/go/callgraph/vta/internal/trie/trie.go b/go/callgraph/vta/internal/trie/trie.go
index 511fde51565..a8480192556 100644
--- a/go/callgraph/vta/internal/trie/trie.go
+++ b/go/callgraph/vta/internal/trie/trie.go
@@ -55,7 +55,7 @@ func (m Map) Size() int {
}
return m.n.size()
}
-func (m Map) Lookup(k uint64) (interface{}, bool) {
+func (m Map) Lookup(k uint64) (any, bool) {
if m.n != nil {
if leaf := m.n.find(key(k)); leaf != nil {
return leaf.v, true
@@ -68,7 +68,7 @@ func (m Map) Lookup(k uint64) (interface{}, bool) {
// %s string conversion for .
func (m Map) String() string {
var kvs []string
- m.Range(func(u uint64, i interface{}) bool {
+ m.Range(func(u uint64, i any) bool {
kvs = append(kvs, fmt.Sprintf("%d: %s", u, i))
return true
})
@@ -78,7 +78,7 @@ func (m Map) String() string {
// Range over the leaf (key, value) pairs in the map in order and
// applies cb(key, value) to each. Stops early if cb returns false.
// Returns true if all elements were visited without stopping early.
-func (m Map) Range(cb func(uint64, interface{}) bool) bool {
+func (m Map) Range(cb func(uint64, any) bool) bool {
if m.n != nil {
return m.n.visit(cb)
}
@@ -100,9 +100,9 @@ func (m Map) DeepEqual(other Map) bool {
}
// Elems are the (k,v) elements in the Map as a map[uint64]interface{}
-func Elems(m Map) map[uint64]interface{} {
- dest := make(map[uint64]interface{}, m.Size())
- m.Range(func(k uint64, v interface{}) bool {
+func Elems(m Map) map[uint64]any {
+ dest := make(map[uint64]any, m.Size())
+ m.Range(func(k uint64, v any) bool {
dest[k] = v
return true
})
@@ -117,7 +117,7 @@ type node interface {
// visit the leaves (key, value) pairs in the map in order and
// applies cb(key, value) to each. Stops early if cb returns false.
// Returns true if all elements were visited without stopping early.
- visit(cb func(uint64, interface{}) bool) bool
+ visit(cb func(uint64, any) bool) bool
// Two nodes contain the same elements regardless of scope.
deepEqual(node) bool
@@ -139,7 +139,7 @@ type empty struct {
// leaf represents a single pair.
type leaf struct {
k key
- v interface{}
+ v any
}
// branch represents a tree node within the Patricia trie.
@@ -215,13 +215,13 @@ func (br *branch) deepEqual(m node) bool {
return false
}
-func (*empty) visit(cb func(uint64, interface{}) bool) bool {
+func (*empty) visit(cb func(uint64, any) bool) bool {
return true
}
-func (l *leaf) visit(cb func(uint64, interface{}) bool) bool {
+func (l *leaf) visit(cb func(uint64, any) bool) bool {
return cb(uint64(l.k), l.v)
}
-func (br *branch) visit(cb func(uint64, interface{}) bool) bool {
+func (br *branch) visit(cb func(uint64, any) bool) bool {
if !br.left.visit(cb) {
return false
}
diff --git a/go/callgraph/vta/internal/trie/trie_test.go b/go/callgraph/vta/internal/trie/trie_test.go
index c0651b0ef86..817cb5c5e28 100644
--- a/go/callgraph/vta/internal/trie/trie_test.go
+++ b/go/callgraph/vta/internal/trie/trie_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.13
-// +build go1.13
package trie
@@ -35,8 +34,8 @@ func TestScope(t *testing.T) {
}
func TestCollision(t *testing.T) {
- var x interface{} = 1
- var y interface{} = 2
+ var x any = 1
+ var y any = 2
if v := TakeLhs(x, y); v != x {
t.Errorf("TakeLhs(%s, %s) got %s. want %s", x, y, v, x)
@@ -58,7 +57,7 @@ func TestDefault(t *testing.T) {
if v, ok := def.Lookup(123); !(v == nil && !ok) {
t.Errorf("Scope{}.Lookup() = (%s, %v) not (nil, false)", v, ok)
}
- if !def.Range(func(k uint64, v interface{}) bool {
+ if !def.Range(func(k uint64, v any) bool {
t.Errorf("Scope{}.Range() called it callback on %d:%s", k, v)
return true
}) {
@@ -115,7 +114,7 @@ func TestEmpty(t *testing.T) {
if l := e.n.find(123); l != nil {
t.Errorf("empty.find(123) got %v. want nil", l)
}
- e.Range(func(k uint64, v interface{}) bool {
+ e.Range(func(k uint64, v any) bool {
t.Errorf("empty.Range() called it callback on %d:%s", k, v)
return true
})
@@ -130,23 +129,23 @@ func TestCreate(t *testing.T) {
// The node orders are printed in lexicographic little-endian.
b := NewBuilder()
for _, c := range []struct {
- m map[uint64]interface{}
+ m map[uint64]any
want string
}{
{
- map[uint64]interface{}{},
+ map[uint64]any{},
"{}",
},
{
- map[uint64]interface{}{1: "a"},
+ map[uint64]any{1: "a"},
"{1: a}",
},
{
- map[uint64]interface{}{2: "b", 1: "a"},
+ map[uint64]any{2: "b", 1: "a"},
"{1: a, 2: b}",
},
{
- map[uint64]interface{}{1: "x", 4: "y", 5: "z"},
+ map[uint64]any{1: "x", 4: "y", 5: "z"},
"{1: x, 4: y, 5: z}",
},
} {
@@ -159,7 +158,7 @@ func TestCreate(t *testing.T) {
func TestElems(t *testing.T) {
b := NewBuilder()
- for _, orig := range []map[uint64]interface{}{
+ for _, orig := range []map[uint64]any{
{},
{1: "a"},
{1: "a", 2: "b"},
@@ -175,10 +174,10 @@ func TestElems(t *testing.T) {
func TestRange(t *testing.T) {
b := NewBuilder()
- m := b.Create(map[uint64]interface{}{1: "x", 3: "y", 5: "z", 6: "stop", 8: "a"})
+ m := b.Create(map[uint64]any{1: "x", 3: "y", 5: "z", 6: "stop", 8: "a"})
calls := 0
- cb := func(k uint64, v interface{}) bool {
+ cb := func(k uint64, v any) bool {
t.Logf("visiting (%d, %v)", k, v)
calls++
return k%2 != 0 // stop after the first even number.
@@ -196,7 +195,7 @@ func TestRange(t *testing.T) {
}
func TestDeepEqual(t *testing.T) {
- for _, m := range []map[uint64]interface{}{
+ for _, m := range []map[uint64]any{
{},
{1: "x"},
{1: "x", 2: "y"},
@@ -211,32 +210,32 @@ func TestDeepEqual(t *testing.T) {
func TestNotDeepEqual(t *testing.T) {
for _, c := range []struct {
- left map[uint64]interface{}
- right map[uint64]interface{}
+ left map[uint64]any
+ right map[uint64]any
}{
{
- map[uint64]interface{}{1: "x"},
- map[uint64]interface{}{},
+ map[uint64]any{1: "x"},
+ map[uint64]any{},
},
{
- map[uint64]interface{}{},
- map[uint64]interface{}{1: "y"},
+ map[uint64]any{},
+ map[uint64]any{1: "y"},
},
{
- map[uint64]interface{}{1: "x"},
- map[uint64]interface{}{1: "y"},
+ map[uint64]any{1: "x"},
+ map[uint64]any{1: "y"},
},
{
- map[uint64]interface{}{1: "x"},
- map[uint64]interface{}{1: "x", 2: "Y"},
+ map[uint64]any{1: "x"},
+ map[uint64]any{1: "x", 2: "Y"},
},
{
- map[uint64]interface{}{1: "x", 2: "Y"},
- map[uint64]interface{}{1: "x"},
+ map[uint64]any{1: "x", 2: "Y"},
+ map[uint64]any{1: "x"},
},
{
- map[uint64]interface{}{1: "x", 2: "y"},
- map[uint64]interface{}{1: "x", 2: "Y"},
+ map[uint64]any{1: "x", 2: "y"},
+ map[uint64]any{1: "x", 2: "Y"},
},
} {
l := NewBuilder().Create(c.left)
@@ -250,97 +249,97 @@ func TestNotDeepEqual(t *testing.T) {
func TestMerge(t *testing.T) {
b := NewBuilder()
for _, c := range []struct {
- left map[uint64]interface{}
- right map[uint64]interface{}
+ left map[uint64]any
+ right map[uint64]any
want string
}{
{
- map[uint64]interface{}{},
- map[uint64]interface{}{},
+ map[uint64]any{},
+ map[uint64]any{},
"{}",
},
{
- map[uint64]interface{}{},
- map[uint64]interface{}{1: "a"},
+ map[uint64]any{},
+ map[uint64]any{1: "a"},
"{1: a}",
},
{
- map[uint64]interface{}{1: "a"},
- map[uint64]interface{}{},
+ map[uint64]any{1: "a"},
+ map[uint64]any{},
"{1: a}",
},
{
- map[uint64]interface{}{1: "a", 2: "b"},
- map[uint64]interface{}{},
+ map[uint64]any{1: "a", 2: "b"},
+ map[uint64]any{},
"{1: a, 2: b}",
},
{
- map[uint64]interface{}{1: "x"},
- map[uint64]interface{}{1: "y"},
+ map[uint64]any{1: "x"},
+ map[uint64]any{1: "y"},
"{1: x}", // default collision is left
},
{
- map[uint64]interface{}{1: "x"},
- map[uint64]interface{}{2: "y"},
+ map[uint64]any{1: "x"},
+ map[uint64]any{2: "y"},
"{1: x, 2: y}",
},
{
- map[uint64]interface{}{4: "y", 5: "z"},
- map[uint64]interface{}{1: "x"},
+ map[uint64]any{4: "y", 5: "z"},
+ map[uint64]any{1: "x"},
"{1: x, 4: y, 5: z}",
},
{
- map[uint64]interface{}{1: "x", 5: "z"},
- map[uint64]interface{}{4: "y"},
+ map[uint64]any{1: "x", 5: "z"},
+ map[uint64]any{4: "y"},
"{1: x, 4: y, 5: z}",
},
{
- map[uint64]interface{}{1: "x", 4: "y"},
- map[uint64]interface{}{5: "z"},
+ map[uint64]any{1: "x", 4: "y"},
+ map[uint64]any{5: "z"},
"{1: x, 4: y, 5: z}",
},
{
- map[uint64]interface{}{1: "a", 4: "c"},
- map[uint64]interface{}{2: "b", 5: "d"},
+ map[uint64]any{1: "a", 4: "c"},
+ map[uint64]any{2: "b", 5: "d"},
"{1: a, 2: b, 4: c, 5: d}",
},
{
- map[uint64]interface{}{1: "a", 4: "c"},
- map[uint64]interface{}{2: "b", 5 + 8: "d"},
+ map[uint64]any{1: "a", 4: "c"},
+ map[uint64]any{2: "b", 5 + 8: "d"},
"{1: a, 2: b, 4: c, 13: d}",
},
{
- map[uint64]interface{}{2: "b", 5 + 8: "d"},
- map[uint64]interface{}{1: "a", 4: "c"},
+ map[uint64]any{2: "b", 5 + 8: "d"},
+ map[uint64]any{1: "a", 4: "c"},
"{1: a, 2: b, 4: c, 13: d}",
},
{
- map[uint64]interface{}{1: "a", 4: "c"},
- map[uint64]interface{}{2: "b", 5 + 8: "d"},
+ map[uint64]any{1: "a", 4: "c"},
+ map[uint64]any{2: "b", 5 + 8: "d"},
"{1: a, 2: b, 4: c, 13: d}",
},
{
- map[uint64]interface{}{2: "b", 5 + 8: "d"},
- map[uint64]interface{}{1: "a", 4: "c"},
+ map[uint64]any{2: "b", 5 + 8: "d"},
+ map[uint64]any{1: "a", 4: "c"},
"{1: a, 2: b, 4: c, 13: d}",
},
{
- map[uint64]interface{}{2: "b", 5 + 8: "d"},
- map[uint64]interface{}{2: "", 3: "a"},
+ map[uint64]any{2: "b", 5 + 8: "d"},
+ map[uint64]any{2: "", 3: "a"},
"{2: b, 3: a, 13: d}",
},
{
// crafted for `!prefixesOverlap(p, m, q, n)`
- left: map[uint64]interface{}{1: "a", 2 + 1: "b"},
- right: map[uint64]interface{}{4 + 1: "c", 4 + 2: "d"},
+ left: map[uint64]any{1: "a", 2 + 1: "b"},
+ right: map[uint64]any{4 + 1: "c", 4 + 2: "d"},
// p: 5, m: 2 q: 1, n: 2
want: "{1: a, 3: b, 5: c, 6: d}",
},
{
// crafted for `ord(m, n) && !zeroBit(q, m)`
- left: map[uint64]interface{}{8 + 2 + 1: "a", 16 + 4: "b"},
- right: map[uint64]interface{}{16 + 8 + 2 + 1: "c", 16 + 8 + 4 + 2 + 1: "d"},
+ left: map[uint64]any{8 + 2 + 1: "a", 16 + 4: "b"},
+ right: map[uint64]any{16 + 8 + 2 + 1: "c", 16 + 8 + 4 + 2 + 1: "d"},
// left: p: 15, m: 16
// right: q: 27, n: 4
want: "{11: a, 20: b, 27: c, 31: d}",
@@ -348,8 +347,8 @@ func TestMerge(t *testing.T) {
{
// crafted for `ord(n, m) && !zeroBit(p, n)`
// p: 6, m: 1 q: 5, n: 2
- left: map[uint64]interface{}{4 + 2: "b", 4 + 2 + 1: "c"},
- right: map[uint64]interface{}{4: "a", 4 + 2 + 1: "dropped"},
+ left: map[uint64]any{4 + 2: "b", 4 + 2 + 1: "c"},
+ right: map[uint64]any{4: "a", 4 + 2 + 1: "dropped"},
want: "{4: a, 6: b, 7: c}",
},
} {
@@ -365,65 +364,65 @@ func TestIntersect(t *testing.T) {
// Most of the test cases go after specific branches of intersect.
b := NewBuilder()
for _, c := range []struct {
- left map[uint64]interface{}
- right map[uint64]interface{}
+ left map[uint64]any
+ right map[uint64]any
want string
}{
{
- left: map[uint64]interface{}{10: "a", 39: "b"},
- right: map[uint64]interface{}{10: "A", 39: "B", 75: "C"},
+ left: map[uint64]any{10: "a", 39: "b"},
+ right: map[uint64]any{10: "A", 39: "B", 75: "C"},
want: "{10: a, 39: b}",
},
{
- left: map[uint64]interface{}{10: "a", 39: "b"},
- right: map[uint64]interface{}{},
+ left: map[uint64]any{10: "a", 39: "b"},
+ right: map[uint64]any{},
want: "{}",
},
{
- left: map[uint64]interface{}{},
- right: map[uint64]interface{}{10: "A", 39: "B", 75: "C"},
+ left: map[uint64]any{},
+ right: map[uint64]any{10: "A", 39: "B", 75: "C"},
want: "{}",
},
{ // m == n && p == q && left.(*empty) case
- left: map[uint64]interface{}{4: 1, 6: 3, 10: 8, 15: "on left"},
- right: map[uint64]interface{}{0: 8, 7: 6, 11: 0, 15: "on right"},
+ left: map[uint64]any{4: 1, 6: 3, 10: 8, 15: "on left"},
+ right: map[uint64]any{0: 8, 7: 6, 11: 0, 15: "on right"},
want: "{15: on left}",
},
{ // m == n && p == q && right.(*empty) case
- left: map[uint64]interface{}{0: "on left", 1: 2, 2: 3, 3: 1, 7: 3},
- right: map[uint64]interface{}{0: "on right", 5: 1, 6: 8},
+ left: map[uint64]any{0: "on left", 1: 2, 2: 3, 3: 1, 7: 3},
+ right: map[uint64]any{0: "on right", 5: 1, 6: 8},
want: "{0: on left}",
},
{ // m == n && p == q && both left and right are not empty
- left: map[uint64]interface{}{1: "a", 2: "b", 3: "c"},
- right: map[uint64]interface{}{0: "A", 1: "B", 2: "C"},
+ left: map[uint64]any{1: "a", 2: "b", 3: "c"},
+ right: map[uint64]any{0: "A", 1: "B", 2: "C"},
want: "{1: a, 2: b}",
},
{ // m == n && p == q && both left and right are not empty
- left: map[uint64]interface{}{1: "a", 2: "b", 3: "c"},
- right: map[uint64]interface{}{0: "A", 1: "B", 2: "C"},
+ left: map[uint64]any{1: "a", 2: "b", 3: "c"},
+ right: map[uint64]any{0: "A", 1: "B", 2: "C"},
want: "{1: a, 2: b}",
},
{ // !prefixesOverlap(p, m, q, n)
// p = 1, m = 2, q = 5, n = 2
- left: map[uint64]interface{}{0b001: 1, 0b011: 3},
- right: map[uint64]interface{}{0b100: 4, 0b111: 7},
+ left: map[uint64]any{0b001: 1, 0b011: 3},
+ right: map[uint64]any{0b100: 4, 0b111: 7},
want: "{}",
},
{ // ord(m, n) && zeroBit(q, m)
// p = 3, m = 4, q = 0, n = 1
- left: map[uint64]interface{}{0b010: 2, 0b101: 5},
- right: map[uint64]interface{}{0b000: 0, 0b001: 1},
+ left: map[uint64]any{0b010: 2, 0b101: 5},
+ right: map[uint64]any{0b000: 0, 0b001: 1},
want: "{}",
},
{ // ord(m, n) && !zeroBit(q, m)
// p = 29, m = 2, q = 30, n = 1
- left: map[uint64]interface{}{
+ left: map[uint64]any{
0b11101: "29",
0b11110: "30",
},
- right: map[uint64]interface{}{
+ right: map[uint64]any{
0b11110: "30 on right",
0b11111: "31",
},
@@ -431,14 +430,14 @@ func TestIntersect(t *testing.T) {
},
{ // ord(n, m) && zeroBit(p, n)
// p = 5, m = 2, q = 3, n = 4
- left: map[uint64]interface{}{0b000: 0, 0b001: 1},
- right: map[uint64]interface{}{0b010: 2, 0b101: 5},
+ left: map[uint64]any{0b000: 0, 0b001: 1},
+ right: map[uint64]any{0b010: 2, 0b101: 5},
want: "{}",
},
{ // default case
// p = 5, m = 2, q = 3, n = 4
- left: map[uint64]interface{}{0b100: 1, 0b110: 3},
- right: map[uint64]interface{}{0b000: 8, 0b111: 6},
+ left: map[uint64]any{0b100: 1, 0b110: 3},
+ right: map[uint64]any{0b000: 8, 0b111: 6},
want: "{}",
},
} {
@@ -452,10 +451,10 @@ func TestIntersect(t *testing.T) {
func TestIntersectWith(t *testing.T) {
b := NewBuilder()
- l := b.Create(map[uint64]interface{}{10: 2.0, 39: 32.0})
- r := b.Create(map[uint64]interface{}{10: 6.0, 39: 10.0, 75: 1.0})
+ l := b.Create(map[uint64]any{10: 2.0, 39: 32.0})
+ r := b.Create(map[uint64]any{10: 6.0, 39: 10.0, 75: 1.0})
- prodIfDifferent := func(x interface{}, y interface{}) interface{} {
+ prodIfDifferent := func(x any, y any) any {
if x, ok := x.(float64); ok {
if y, ok := y.(float64); ok {
if x == y {
@@ -479,24 +478,24 @@ func TestRemove(t *testing.T) {
// Most of the test cases go after specific branches of intersect.
b := NewBuilder()
for _, c := range []struct {
- m map[uint64]interface{}
+ m map[uint64]any
key uint64
want string
}{
- {map[uint64]interface{}{}, 10, "{}"},
- {map[uint64]interface{}{10: "a"}, 10, "{}"},
- {map[uint64]interface{}{39: "b"}, 10, "{39: b}"},
+ {map[uint64]any{}, 10, "{}"},
+ {map[uint64]any{10: "a"}, 10, "{}"},
+ {map[uint64]any{39: "b"}, 10, "{39: b}"},
// Branch cases:
// !matchPrefix(kp, br.prefix, br.branching)
- {map[uint64]interface{}{10: "a", 39: "b"}, 128, "{10: a, 39: b}"},
+ {map[uint64]any{10: "a", 39: "b"}, 128, "{10: a, 39: b}"},
// case: left == br.left && right == br.right
- {map[uint64]interface{}{10: "a", 39: "b"}, 16, "{10: a, 39: b}"},
+ {map[uint64]any{10: "a", 39: "b"}, 16, "{10: a, 39: b}"},
// left updated and is empty.
- {map[uint64]interface{}{10: "a", 39: "b"}, 10, "{39: b}"},
+ {map[uint64]any{10: "a", 39: "b"}, 10, "{39: b}"},
// right updated and is empty.
- {map[uint64]interface{}{10: "a", 39: "b"}, 39, "{10: a}"},
+ {map[uint64]any{10: "a", 39: "b"}, 39, "{10: a}"},
// final b.mkBranch(...) case.
- {map[uint64]interface{}{10: "a", 39: "b", 128: "c"}, 39, "{10: a, 128: c}"},
+ {map[uint64]any{10: "a", 39: "b", 128: "c"}, 39, "{10: a, 128: c}"},
} {
pre := b.Create(c.m)
post := b.Remove(pre, c.key)
@@ -508,8 +507,8 @@ func TestRemove(t *testing.T) {
func TestRescope(t *testing.T) {
b := NewBuilder()
- l := b.Create(map[uint64]interface{}{10: "a", 39: "b"})
- r := b.Create(map[uint64]interface{}{10: "A", 39: "B", 75: "C"})
+ l := b.Create(map[uint64]any{10: "a", 39: "b"})
+ r := b.Create(map[uint64]any{10: "A", 39: "B", 75: "C"})
b.Rescope()
@@ -527,8 +526,8 @@ func TestRescope(t *testing.T) {
func TestSharing(t *testing.T) {
b := NewBuilder()
- l := b.Create(map[uint64]interface{}{0: "a", 1: "b"})
- r := b.Create(map[uint64]interface{}{1: "B", 2: "C"})
+ l := b.Create(map[uint64]any{0: "a", 1: "b"})
+ r := b.Create(map[uint64]any{1: "B", 2: "C"})
rleftold := r.n.(*branch).left
diff --git a/go/callgraph/vta/propagation.go b/go/callgraph/vta/propagation.go
index f448cde1135..a71c5b0034a 100644
--- a/go/callgraph/vta/propagation.go
+++ b/go/callgraph/vta/propagation.go
@@ -6,6 +6,7 @@ package vta
import (
"go/types"
+ "iter"
"slices"
"golang.org/x/tools/go/callgraph/vta/internal/trie"
@@ -41,7 +42,7 @@ func scc(g *vtaGraph) (sccs [][]idx, idxToSccID []int) {
*ns = state{pre: nextPre, lowLink: nextPre, onStack: true}
stack = append(stack, n)
- g.successors(n)(func(s idx) bool {
+ for s := range g.successors(n) {
if ss := &states[s]; ss.pre == 0 {
// Analyze successor s that has not been visited yet.
doSCC(s)
@@ -51,8 +52,7 @@ func scc(g *vtaGraph) (sccs [][]idx, idxToSccID []int) {
// in the current SCC.
ns.lowLink = min(ns.lowLink, ss.pre)
}
- return true
- })
+ }
// if n is a root node, pop the stack and generate a new SCC.
if ns.lowLink == ns.pre {
@@ -113,14 +113,12 @@ type propType struct {
// the role of a map from nodes to a set of propTypes.
type propTypeMap map[node]*trie.MutMap
-// propTypes returns a go1.23 iterator for the propTypes associated with
+// propTypes returns an iterator for the propTypes associated with
// node `n` in map `ptm`.
-func (ptm propTypeMap) propTypes(n node) func(yield func(propType) bool) {
- // TODO: when x/tools uses go1.23, change callers to use range-over-func
- // (https://go.dev/issue/65237).
+func (ptm propTypeMap) propTypes(n node) iter.Seq[propType] {
return func(yield func(propType) bool) {
if types := ptm[n]; types != nil {
- types.M.Range(func(_ uint64, elem interface{}) bool {
+ types.M.Range(func(_ uint64, elem any) bool {
return yield(elem.(propType))
})
}
@@ -167,10 +165,9 @@ func propagate(graph *vtaGraph, canon *typeutil.Map) propTypeMap {
for i := len(sccs) - 1; i >= 0; i-- {
nextSccs := make(map[int]empty)
for _, n := range sccs[i] {
- graph.successors(n)(func(succ idx) bool {
+ for succ := range graph.successors(n) {
nextSccs[idxToSccID[succ]] = empty{}
- return true
- })
+ }
}
// Propagate types to all successor SCCs.
for nextScc := range nextSccs {
diff --git a/go/callgraph/vta/propagation_test.go b/go/callgraph/vta/propagation_test.go
index 492258f81e3..2b36cf39bb7 100644
--- a/go/callgraph/vta/propagation_test.go
+++ b/go/callgraph/vta/propagation_test.go
@@ -98,10 +98,9 @@ func nodeToTypeString(pMap propTypeMap) map[string]string {
nodeToTypeStr := make(map[string]string)
for node := range pMap {
var propStrings []string
- pMap.propTypes(node)(func(prop propType) bool {
+ for prop := range pMap.propTypes(node) {
propStrings = append(propStrings, propTypeString(prop))
- return true
- })
+ }
sort.Strings(propStrings)
nodeToTypeStr[node.String()] = strings.Join(propStrings, ";")
}
@@ -124,17 +123,14 @@ func sccEqual(sccs1 []string, sccs2 []string) bool {
//
// for every edge x -> y in g, nodeToScc[x] > nodeToScc[y]
func isRevTopSorted(g *vtaGraph, idxToScc []int) bool {
- result := true
- for n := 0; n < len(idxToScc); n++ {
- g.successors(idx(n))(func(s idx) bool {
+ for n := range idxToScc {
+ for s := range g.successors(idx(n)) {
if idxToScc[n] < idxToScc[s] {
- result = false
return false
}
- return true
- })
+ }
}
- return result
+ return true
}
func sccMapsConsistent(sccs [][]idx, idxToSccID []int) bool {
@@ -203,7 +199,7 @@ func testSuite() map[string]*vtaGraph {
a := newNamedType("A")
b := newNamedType("B")
c := newNamedType("C")
- sig := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false)
+ sig := types.NewSignatureType(nil, nil, nil, types.NewTuple(), types.NewTuple(), false)
f1 := &ssa.Function{Signature: sig}
setName(f1, "F1")
diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go
index bbd8400ec9b..3a708f220a7 100644
--- a/go/callgraph/vta/utils.go
+++ b/go/callgraph/vta/utils.go
@@ -6,6 +6,7 @@ package vta
import (
"go/types"
+ "iter"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/internal/typeparams"
@@ -147,10 +148,8 @@ func sliceArrayElem(t types.Type) types.Type {
}
}
-// siteCallees returns a go1.23 iterator for the callees for call site `c`.
-func siteCallees(c ssa.CallInstruction, callees calleesFunc) func(yield func(*ssa.Function) bool) {
- // TODO: when x/tools uses go1.23, change callers to use range-over-func
- // (https://go.dev/issue/65237).
+// siteCallees returns an iterator for the callees for call site `c`.
+func siteCallees(c ssa.CallInstruction, callees calleesFunc) iter.Seq[*ssa.Function] {
return func(yield func(*ssa.Function) bool) {
for _, callee := range callees(c) {
if !yield(callee) {
diff --git a/go/callgraph/vta/vta.go b/go/callgraph/vta/vta.go
index 56fce13725f..ed12001fdb2 100644
--- a/go/callgraph/vta/vta.go
+++ b/go/callgraph/vta/vta.go
@@ -126,12 +126,11 @@ func (c *constructor) resolves(call ssa.CallInstruction) []*ssa.Function {
// Cover the case of dynamic higher-order and interface calls.
var res []*ssa.Function
resolved := resolve(call, c.types, c.cache)
- siteCallees(call, c.callees)(func(f *ssa.Function) bool {
+ for f := range siteCallees(call, c.callees) {
if _, ok := resolved[f]; ok {
res = append(res, f)
}
- return true
- })
+ }
return res
}
@@ -140,12 +139,11 @@ func (c *constructor) resolves(call ssa.CallInstruction) []*ssa.Function {
func resolve(c ssa.CallInstruction, types propTypeMap, cache methodCache) map[*ssa.Function]empty {
fns := make(map[*ssa.Function]empty)
n := local{val: c.Common().Value}
- types.propTypes(n)(func(p propType) bool {
+ for p := range types.propTypes(n) {
for _, f := range propFunc(p, c, cache) {
fns[f] = empty{}
}
- return true
- })
+ }
return fns
}
diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go
index ea7d584d2d9..42610abb139 100644
--- a/go/callgraph/vta/vta_test.go
+++ b/go/callgraph/vta/vta_test.go
@@ -118,7 +118,7 @@ func TestVTAProgVsFuncSet(t *testing.T) {
// available, which can happen when using analysis package. A successful
// test simply does not panic.
func TestVTAPanicMissingDefinitions(t *testing.T) {
- run := func(pass *analysis.Pass) (interface{}, error) {
+ run := func(pass *analysis.Pass) (any, error) {
s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
CallGraph(ssautil.AllFunctions(s.Pkg.Prog), cha.CallGraph(s.Pkg.Prog))
return nil, nil
diff --git a/go/expect/expect.go b/go/expect/expect.go
index be0e1dd23e6..1c002d91b60 100644
--- a/go/expect/expect.go
+++ b/go/expect/expect.go
@@ -66,9 +66,9 @@ import (
// It knows the position of the start of the comment, and the name and
// arguments that make up the note.
type Note struct {
- Pos token.Pos // The position at which the note identifier appears
- Name string // the name associated with the note
- Args []interface{} // the arguments for the note
+ Pos token.Pos // The position at which the note identifier appears
+ Name string // the name associated with the note
+ Args []any // the arguments for the note
}
// ReadFile is the type of a function that can provide file contents for a
@@ -85,7 +85,7 @@ type ReadFile func(filename string) ([]byte, error)
// MatchBefore returns the range of the line that matched the pattern, and
// invalid positions if there was no match, or an error if the line could not be
// found.
-func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern interface{}) (token.Pos, token.Pos, error) {
+func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern any) (token.Pos, token.Pos, error) {
f := fset.File(end)
content, err := readFile(f.Name())
if err != nil {
diff --git a/go/expect/expect_test.go b/go/expect/expect_test.go
index cc585418d1b..d1ce96b868e 100644
--- a/go/expect/expect_test.go
+++ b/go/expect/expect_test.go
@@ -18,7 +18,7 @@ func TestMarker(t *testing.T) {
filename string
expectNotes int
expectMarkers map[string]string
- expectChecks map[string][]interface{}
+ expectChecks map[string][]any
}{
{
filename: "testdata/test.go",
@@ -36,7 +36,7 @@ func TestMarker(t *testing.T) {
"NonIdentifier": "+",
"StringMarker": "\"hello\"",
},
- expectChecks: map[string][]interface{}{
+ expectChecks: map[string][]any{
"αSimpleMarker": nil,
"StringAndInt": {"Number %d", int64(12)},
"Bool": {true},
@@ -140,7 +140,7 @@ func TestMarker(t *testing.T) {
}
}
-func checkMarker(t *testing.T, fset *token.FileSet, readFile expect.ReadFile, markers map[string]token.Pos, pos token.Pos, name string, pattern interface{}) {
+func checkMarker(t *testing.T, fset *token.FileSet, readFile expect.ReadFile, markers map[string]token.Pos, pos token.Pos, name string, pattern any) {
start, end, err := expect.MatchBefore(fset, readFile, pos, pattern)
if err != nil {
t.Errorf("%v: MatchBefore failed: %v", fset.Position(pos), err)
diff --git a/go/expect/extract.go b/go/expect/extract.go
index 902b1e806e4..9cc5c8171fd 100644
--- a/go/expect/extract.go
+++ b/go/expect/extract.go
@@ -32,7 +32,7 @@ type Identifier string
// See the package documentation for details about the syntax of those
// notes.
func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error) {
- var src interface{}
+ var src any
if content != nil {
src = content
}
@@ -220,7 +220,7 @@ func (t *tokens) Pos() token.Pos {
return t.base + token.Pos(t.scanner.Position.Offset)
}
-func (t *tokens) Errorf(msg string, args ...interface{}) {
+func (t *tokens) Errorf(msg string, args ...any) {
if t.err != nil {
return
}
@@ -280,9 +280,9 @@ func parseNote(t *tokens) *Note {
}
}
-func parseArgumentList(t *tokens) []interface{} {
- args := []interface{}{} // @name() is represented by a non-nil empty slice.
- t.Consume() // '('
+func parseArgumentList(t *tokens) []any {
+ args := []any{} // @name() is represented by a non-nil empty slice.
+ t.Consume() // '('
t.Skip('\n')
for t.Token() != ')' {
args = append(args, parseArgument(t))
@@ -300,7 +300,7 @@ func parseArgumentList(t *tokens) []interface{} {
return args
}
-func parseArgument(t *tokens) interface{} {
+func parseArgument(t *tokens) any {
switch t.Token() {
case scanner.Ident:
v := t.Consume()
diff --git a/go/gcexportdata/example_test.go b/go/gcexportdata/example_test.go
index 9574f30d32b..d6d69a8aa54 100644
--- a/go/gcexportdata/example_test.go
+++ b/go/gcexportdata/example_test.go
@@ -3,11 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.7 && gc && !android && !ios && (unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || plan9 || windows)
-// +build go1.7
-// +build gc
-// +build !android
-// +build !ios
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris plan9 windows
package gcexportdata_test
@@ -20,6 +15,7 @@ import (
"log"
"os"
"path/filepath"
+ "slices"
"strings"
"golang.org/x/tools/go/gcexportdata"
@@ -56,13 +52,7 @@ func ExampleRead() {
// We can see all the names in Names.
members := pkg.Scope().Names()
- foundPrintln := false
- for _, member := range members {
- if member == "Println" {
- foundPrintln = true
- break
- }
- }
+ foundPrintln := slices.Contains(members, "Println")
fmt.Print("Package members: ")
if foundPrintln {
fmt.Println("Println found")
diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go
index 65fe2628e90..7b90bc92353 100644
--- a/go/gcexportdata/gcexportdata.go
+++ b/go/gcexportdata/gcexportdata.go
@@ -193,10 +193,7 @@ func Read(in io.Reader, fset *token.FileSet, imports map[string]*types.Package,
return pkg, err
default:
- l := len(data)
- if l > 10 {
- l = 10
- }
+ l := min(len(data), 10)
return nil, fmt.Errorf("unexpected export data with prefix %q for path %s", string(data[:l]), path)
}
}
diff --git a/go/gcexportdata/main.go b/go/gcexportdata/main.go
index e9df4e9a9a5..0b267e33867 100644
--- a/go/gcexportdata/main.go
+++ b/go/gcexportdata/main.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build ignore
-// +build ignore
// The gcexportdata command is a diagnostic tool that displays the
// contents of gc export data files.
diff --git a/go/internal/cgo/cgo.go b/go/internal/cgo/cgo.go
index 697974bb9b2..735efeb531d 100644
--- a/go/internal/cgo/cgo.go
+++ b/go/internal/cgo/cgo.go
@@ -203,7 +203,7 @@ func envList(key, def string) []string {
// stringList's arguments should be a sequence of string or []string values.
// stringList flattens them into a single []string.
-func stringList(args ...interface{}) []string {
+func stringList(args ...any) []string {
var x []string
for _, arg := range args {
switch arg := arg.(type) {
diff --git a/go/internal/gccgoimporter/newInterface10.go b/go/internal/gccgoimporter/newInterface10.go
index 1b449ef9886..f49c9b067dd 100644
--- a/go/internal/gccgoimporter/newInterface10.go
+++ b/go/internal/gccgoimporter/newInterface10.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !go1.11
-// +build !go1.11
package gccgoimporter
diff --git a/go/internal/gccgoimporter/newInterface11.go b/go/internal/gccgoimporter/newInterface11.go
index 631546ec66f..c7d5edb4858 100644
--- a/go/internal/gccgoimporter/newInterface11.go
+++ b/go/internal/gccgoimporter/newInterface11.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.11
-// +build go1.11
package gccgoimporter
diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go
index f315ec41004..7b0702892c4 100644
--- a/go/internal/gccgoimporter/parser.go
+++ b/go/internal/gccgoimporter/parser.go
@@ -86,7 +86,7 @@ func (e importError) Error() string {
return fmt.Sprintf("import error %s (byte offset = %d): %s", e.pos, e.pos.Offset, e.err)
}
-func (p *parser) error(err interface{}) {
+func (p *parser) error(err any) {
if s, ok := err.(string); ok {
err = errors.New(s)
}
@@ -94,7 +94,7 @@ func (p *parser) error(err interface{}) {
panic(importError{p.scanner.Pos(), err.(error)})
}
-func (p *parser) errorf(format string, args ...interface{}) {
+func (p *parser) errorf(format string, args ...any) {
p.error(fmt.Errorf(format, args...))
}
@@ -492,7 +492,7 @@ func (p *parser) reserve(n int) {
// used to resolve named types, or it can be a *types.Pointer,
// used to resolve pointers to named types in case they are referenced
// by embedded fields.
-func (p *parser) update(t types.Type, nlist []interface{}) {
+func (p *parser) update(t types.Type, nlist []any) {
if t == reserved {
p.errorf("internal error: update(%v) invoked on reserved", nlist)
}
@@ -529,7 +529,7 @@ func (p *parser) update(t types.Type, nlist []interface{}) {
// NamedType = TypeName [ "=" ] Type { Method } .
// TypeName = ExportedName .
// Method = "func" "(" Param ")" Name ParamList ResultList [InlineBody] ";" .
-func (p *parser) parseNamedType(nlist []interface{}) types.Type {
+func (p *parser) parseNamedType(nlist []any) types.Type {
pkg, name := p.parseExportedName()
scope := pkg.Scope()
obj := scope.Lookup(name)
@@ -619,7 +619,7 @@ func (p *parser) parseNamedType(nlist []interface{}) types.Type {
p.skipInlineBody()
p.expectEOL()
- sig := types.NewSignature(receiver, params, results, isVariadic)
+ sig := types.NewSignatureType(receiver, nil, nil, params, results, isVariadic)
nt.AddMethod(types.NewFunc(token.NoPos, pkg, name, sig))
}
}
@@ -648,7 +648,7 @@ func (p *parser) parseInt() int {
// parseArrayOrSliceType parses an ArrayOrSliceType:
//
// ArrayOrSliceType = "[" [ int ] "]" Type .
-func (p *parser) parseArrayOrSliceType(pkg *types.Package, nlist []interface{}) types.Type {
+func (p *parser) parseArrayOrSliceType(pkg *types.Package, nlist []any) types.Type {
p.expect('[')
if p.tok == ']' {
p.next()
@@ -673,7 +673,7 @@ func (p *parser) parseArrayOrSliceType(pkg *types.Package, nlist []interface{})
// parseMapType parses a MapType:
//
// MapType = "map" "[" Type "]" Type .
-func (p *parser) parseMapType(pkg *types.Package, nlist []interface{}) types.Type {
+func (p *parser) parseMapType(pkg *types.Package, nlist []any) types.Type {
p.expectKeyword("map")
t := new(types.Map)
@@ -691,7 +691,7 @@ func (p *parser) parseMapType(pkg *types.Package, nlist []interface{}) types.Typ
// parseChanType parses a ChanType:
//
// ChanType = "chan" ["<-" | "-<"] Type .
-func (p *parser) parseChanType(pkg *types.Package, nlist []interface{}) types.Type {
+func (p *parser) parseChanType(pkg *types.Package, nlist []any) types.Type {
p.expectKeyword("chan")
t := new(types.Chan)
@@ -720,7 +720,7 @@ func (p *parser) parseChanType(pkg *types.Package, nlist []interface{}) types.Ty
// parseStructType parses a StructType:
//
// StructType = "struct" "{" { Field } "}" .
-func (p *parser) parseStructType(pkg *types.Package, nlist []interface{}) types.Type {
+func (p *parser) parseStructType(pkg *types.Package, nlist []any) types.Type {
p.expectKeyword("struct")
t := new(types.Struct)
@@ -793,14 +793,14 @@ func (p *parser) parseResultList(pkg *types.Package) *types.Tuple {
// parseFunctionType parses a FunctionType:
//
// FunctionType = ParamList ResultList .
-func (p *parser) parseFunctionType(pkg *types.Package, nlist []interface{}) *types.Signature {
+func (p *parser) parseFunctionType(pkg *types.Package, nlist []any) *types.Signature {
t := new(types.Signature)
p.update(t, nlist)
params, isVariadic := p.parseParamList(pkg)
results := p.parseResultList(pkg)
- *t = *types.NewSignature(nil, params, results, isVariadic)
+ *t = *types.NewSignatureType(nil, nil, nil, params, results, isVariadic)
return t
}
@@ -837,7 +837,7 @@ func (p *parser) parseFunc(pkg *types.Package) *types.Func {
// parseInterfaceType parses an InterfaceType:
//
// InterfaceType = "interface" "{" { ("?" Type | Func) ";" } "}" .
-func (p *parser) parseInterfaceType(pkg *types.Package, nlist []interface{}) types.Type {
+func (p *parser) parseInterfaceType(pkg *types.Package, nlist []any) types.Type {
p.expectKeyword("interface")
t := new(types.Interface)
@@ -868,7 +868,7 @@ func (p *parser) parseInterfaceType(pkg *types.Package, nlist []interface{}) typ
// parsePointerType parses a PointerType:
//
// PointerType = "*" ("any" | Type) .
-func (p *parser) parsePointerType(pkg *types.Package, nlist []interface{}) types.Type {
+func (p *parser) parsePointerType(pkg *types.Package, nlist []any) types.Type {
p.expect('*')
if p.tok == scanner.Ident {
p.expectKeyword("any")
@@ -888,7 +888,7 @@ func (p *parser) parsePointerType(pkg *types.Package, nlist []interface{}) types
// parseTypeSpec parses a TypeSpec:
//
// TypeSpec = NamedType | MapType | ChanType | StructType | InterfaceType | PointerType | ArrayOrSliceType | FunctionType .
-func (p *parser) parseTypeSpec(pkg *types.Package, nlist []interface{}) types.Type {
+func (p *parser) parseTypeSpec(pkg *types.Package, nlist []any) types.Type {
switch p.tok {
case scanner.String:
return p.parseNamedType(nlist)
@@ -980,14 +980,14 @@ func lookupBuiltinType(typ int) types.Type {
// Type = "<" "type" ( "-" int | int [ TypeSpec ] ) ">" .
//
// parseType updates the type map to t for all type numbers n.
-func (p *parser) parseType(pkg *types.Package, n ...interface{}) types.Type {
+func (p *parser) parseType(pkg *types.Package, n ...any) types.Type {
p.expect('<')
t, _ := p.parseTypeAfterAngle(pkg, n...)
return t
}
// (*parser).Type after reading the "<".
-func (p *parser) parseTypeAfterAngle(pkg *types.Package, n ...interface{}) (t types.Type, n1 int) {
+func (p *parser) parseTypeAfterAngle(pkg *types.Package, n ...any) (t types.Type, n1 int) {
p.expectKeyword("type")
n1 = 0
@@ -1030,7 +1030,7 @@ func (p *parser) parseTypeAfterAngle(pkg *types.Package, n ...interface{}) (t ty
// parseTypeExtended is identical to parseType, but if the type in
// question is a saved type, returns the index as well as the type
// pointer (index returned is zero if we parsed a builtin).
-func (p *parser) parseTypeExtended(pkg *types.Package, n ...interface{}) (t types.Type, n1 int) {
+func (p *parser) parseTypeExtended(pkg *types.Package, n ...any) (t types.Type, n1 int) {
p.expect('<')
t, n1 = p.parseTypeAfterAngle(pkg, n...)
return
@@ -1119,7 +1119,7 @@ func (p *parser) parseTypes(pkg *types.Package) {
}
// parseSavedType parses one saved type definition.
-func (p *parser) parseSavedType(pkg *types.Package, i int, nlist []interface{}) {
+func (p *parser) parseSavedType(pkg *types.Package, i int, nlist []any) {
defer func(s *scanner.Scanner, tok rune, lit string) {
p.scanner = s
p.tok = tok
diff --git a/go/loader/loader.go b/go/loader/loader.go
index 2d4865f664f..d06f95ad76c 100644
--- a/go/loader/loader.go
+++ b/go/loader/loader.go
@@ -215,7 +215,7 @@ func (conf *Config) fset() *token.FileSet {
// src specifies the parser input as a string, []byte, or io.Reader, and
// filename is its apparent name. If src is nil, the contents of
// filename are read from the file system.
-func (conf *Config) ParseFile(filename string, src interface{}) (*ast.File, error) {
+func (conf *Config) ParseFile(filename string, src any) (*ast.File, error) {
// TODO(adonovan): use conf.build() etc like parseFiles does.
return parser.ParseFile(conf.fset(), filename, src, conf.ParserMode)
}
diff --git a/go/loader/loader_test.go b/go/loader/loader_test.go
index 2276b49ad6f..eb9feb221f0 100644
--- a/go/loader/loader_test.go
+++ b/go/loader/loader_test.go
@@ -5,7 +5,6 @@
// No testdata on Android.
//go:build !android
-// +build !android
package loader_test
diff --git a/go/packages/external.go b/go/packages/external.go
index 91bd62e83b1..f37bc651009 100644
--- a/go/packages/external.go
+++ b/go/packages/external.go
@@ -90,7 +90,7 @@ func findExternalDriver(cfg *Config) driver {
const toolPrefix = "GOPACKAGESDRIVER="
tool := ""
for _, env := range cfg.Env {
- if val := strings.TrimPrefix(env, toolPrefix); val != env {
+ if val, ok := strings.CutPrefix(env, toolPrefix); ok {
tool = val
}
}
diff --git a/go/packages/golist.go b/go/packages/golist.go
index 0458b4f9c43..96e43cd8093 100644
--- a/go/packages/golist.go
+++ b/go/packages/golist.go
@@ -851,8 +851,6 @@ func (state *golistState) cfgInvocation() gocommand.Invocation {
cfg := state.cfg
return gocommand.Invocation{
BuildFlags: cfg.BuildFlags,
- ModFile: cfg.modFile,
- ModFlag: cfg.modFlag,
CleanEnv: cfg.Env != nil,
Env: cfg.Env,
Logf: cfg.Logf,
diff --git a/go/packages/gopackages/gotypesalias.go b/go/packages/gopackages/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/packages/gopackages/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go
index 3841ac3410b..7ec0bdc7bdd 100644
--- a/go/packages/gopackages/main.go
+++ b/go/packages/gopackages/main.go
@@ -248,7 +248,7 @@ func (app *application) print(lpkg *packages.Package) {
// e.g. --flag=one --flag=two would produce []string{"one", "two"}.
type stringListValue []string
-func (ss *stringListValue) Get() interface{} { return []string(*ss) }
+func (ss *stringListValue) Get() any { return []string(*ss) }
func (ss *stringListValue) String() string { return fmt.Sprintf("%q", *ss) }
diff --git a/go/packages/internal/nodecount/gotypesalias.go b/go/packages/internal/nodecount/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/packages/internal/nodecount/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/packages/overlay_test.go b/go/packages/overlay_test.go
index 9edd0d646ed..4a7cc68f4c7 100644
--- a/go/packages/overlay_test.go
+++ b/go/packages/overlay_test.go
@@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"reflect"
+ "slices"
"sort"
"testing"
@@ -32,7 +33,7 @@ func testOverlayChangesPackageName(t *testing.T, exporter packagestest.Exporter)
log.SetFlags(log.Lshortfile)
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a.go": "package foo\nfunc f(){}\n",
},
Overlay: map[string][]byte{
@@ -62,7 +63,7 @@ func testOverlayChangesBothPackageNames(t *testing.T, exporter packagestest.Expo
log.SetFlags(log.Lshortfile)
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a.go": "package foo\nfunc g(){}\n",
"a_test.go": "package foo\nfunc f(){}\n",
},
@@ -93,7 +94,7 @@ func testOverlayChangesBothPackageNames(t *testing.T, exporter packagestest.Expo
if len(initial) != 3 {
t.Fatalf("expected 3 packages, got %v", len(initial))
}
- for i := 0; i < 3; i++ {
+ for i := range 3 {
if ok := checkPkg(t, initial[i], want[i].id, want[i].name, want[i].count); !ok {
t.Errorf("%d: got {%s %s %d}, expected %v", i, initial[i].ID,
initial[i].Name, len(initial[i].Syntax), want[i])
@@ -110,7 +111,7 @@ func TestOverlayChangesTestPackageName(t *testing.T) {
func testOverlayChangesTestPackageName(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a_test.go": "package foo\nfunc f(){}\n",
},
Overlay: map[string][]byte{
@@ -139,7 +140,7 @@ func testOverlayChangesTestPackageName(t *testing.T, exporter packagestest.Expor
if len(initial) != 3 {
t.Fatalf("expected 3 packages, got %v", len(initial))
}
- for i := 0; i < 3; i++ {
+ for i := range 3 {
if ok := checkPkg(t, initial[i], want[i].id, want[i].name, want[i].count); !ok {
t.Errorf("got {%s %s %d}, expected %v", initial[i].ID,
initial[i].Name, len(initial[i].Syntax), want[i])
@@ -194,7 +195,7 @@ func TestHello(t *testing.T) {
// First, get the source of truth by loading the package, all on disk.
onDisk := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": aFile,
"a/a_test.go": aTestVariant,
"a/a_x_test.go": aXTest,
@@ -213,7 +214,7 @@ func TestHello(t *testing.T) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": aFile,
"a/a_test.go": aTestVariant,
"a/a_x_test.go": ``, // empty x test on disk
@@ -248,7 +249,7 @@ func TestOverlay(t *testing.T) { testAllOrModulesParallel(t, testOverlay) }
func testOverlay(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`,
"b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`,
"c/c.go": `package c; const C = "c"`,
@@ -316,7 +317,7 @@ func TestOverlayDeps(t *testing.T) { testAllOrModulesParallel(t, testOverlayDeps
func testOverlayDeps(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"c/c.go": `package c; const C = "c"`,
"c/c_test.go": `package c; import "testing"; func TestC(t *testing.T) {}`,
},
@@ -366,7 +367,7 @@ func testNewPackagesInOverlay(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`,
"b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`,
"c/c.go": `package c; const C = "c"`,
@@ -375,7 +376,7 @@ func testNewPackagesInOverlay(t *testing.T, exporter packagestest.Exporter) {
},
{
Name: "example.com/extramodule",
- Files: map[string]interface{}{
+ Files: map[string]any{
"pkg/x.go": "package pkg\n",
},
},
@@ -471,7 +472,7 @@ func testOverlayNewPackageAndTest(t *testing.T, exporter packagestest.Exporter)
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"foo.txt": "placeholder",
},
},
@@ -623,7 +624,7 @@ func TestOverlayGOPATHVendoring(t *testing.T) {
exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"vendor/vendor.com/foo/foo.go": `package foo; const X = "hi"`,
"user/user.go": `package user`,
},
@@ -652,7 +653,7 @@ func TestContainsOverlay(t *testing.T) { testAllOrModulesParallel(t, testContain
func testContainsOverlay(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"`,
"b/b.go": `package b; import "golang.org/fake/c"`,
"c/c.go": `package c`,
@@ -681,7 +682,7 @@ func TestContainsOverlayXTest(t *testing.T) { testAllOrModulesParallel(t, testCo
func testContainsOverlayXTest(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"`,
"b/b.go": `package b; import "golang.org/fake/c"`,
"c/c.go": `package c`,
@@ -717,7 +718,7 @@ func testInvalidFilesBeforeOverlay(t *testing.T, exporter packagestest.Exporter)
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"d/d.go": ``,
"main.go": ``,
},
@@ -754,7 +755,7 @@ func testInvalidFilesBeforeOverlayContains(t *testing.T, exporter packagestest.E
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"d/d.go": `package d; import "net/http"; const Get = http.MethodGet; const Hello = "hello";`,
"d/util.go": ``,
"d/d_test.go": ``,
@@ -824,11 +825,8 @@ func testInvalidFilesBeforeOverlayContains(t *testing.T, exporter packagestest.E
t.Fatalf("expected package ID %q, got %q", tt.wantID, pkg.ID)
}
var containsFile bool
- for _, goFile := range pkg.CompiledGoFiles {
- if f == goFile {
- containsFile = true
- break
- }
+ if slices.Contains(pkg.CompiledGoFiles, f) {
+ containsFile = true
}
if !containsFile {
t.Fatalf("expected %s in CompiledGoFiles, got %v", f, pkg.CompiledGoFiles)
@@ -861,7 +859,7 @@ func testInvalidXTestInGOPATH(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"x/x.go": `package x`,
"x/x_test.go": ``,
},
@@ -892,7 +890,7 @@ func testAddImportInOverlay(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a
import (
@@ -961,7 +959,7 @@ func testLoadDifferentPatterns(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"foo.txt": "placeholder",
"b/b.go": `package b
import "golang.org/fake/a"
@@ -1054,7 +1052,7 @@ func TestOverlaysInReplace(t *testing.T) {
if err := os.Mkdir(dirB, 0775); err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(filepath.Join(dirB, "go.mod"), []byte(fmt.Sprintf("module %s.com", dirB)), 0775); err != nil {
+ if err := os.WriteFile(filepath.Join(dirB, "go.mod"), fmt.Appendf(nil, "module %s.com", dirB), 0775); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(dirB, "inner"), 0775); err != nil {
diff --git a/go/packages/packages.go b/go/packages/packages.go
index c3a59b8ebf4..060ab08efbc 100644
--- a/go/packages/packages.go
+++ b/go/packages/packages.go
@@ -141,6 +141,8 @@ const (
LoadAllSyntax = LoadSyntax | NeedDeps
// Deprecated: NeedExportsFile is a historical misspelling of NeedExportFile.
+ //
+ //go:fix inline
NeedExportsFile = NeedExportFile
)
@@ -161,7 +163,7 @@ type Config struct {
// If the user provides a logger, debug logging is enabled.
// If the GOPACKAGESDEBUG environment variable is set to true,
// but the logger is nil, default to log.Printf.
- Logf func(format string, args ...interface{})
+ Logf func(format string, args ...any)
// Dir is the directory in which to run the build system's query tool
// that provides information about the packages.
@@ -227,14 +229,6 @@ type Config struct {
// consistent package metadata about unsaved files. However,
// drivers may vary in their level of support for overlays.
Overlay map[string][]byte
-
- // -- Hidden configuration fields only for use in x/tools --
-
- // modFile will be used for -modfile in go command invocations.
- modFile string
-
- // modFlag will be used for -modfile in go command invocations.
- modFlag string
}
// Load loads and returns the Go packages named by the given patterns.
@@ -564,15 +558,9 @@ type ModuleError struct {
}
func init() {
- packagesinternal.GetDepsErrors = func(p interface{}) []*packagesinternal.PackageError {
+ packagesinternal.GetDepsErrors = func(p any) []*packagesinternal.PackageError {
return p.(*Package).depsErrors
}
- packagesinternal.SetModFile = func(config interface{}, value string) {
- config.(*Config).modFile = value
- }
- packagesinternal.SetModFlag = func(config interface{}, value string) {
- config.(*Config).modFlag = value
- }
packagesinternal.TypecheckCgo = int(typecheckCgo)
packagesinternal.DepsErrors = int(needInternalDepsErrors)
}
@@ -739,7 +727,7 @@ func newLoader(cfg *Config) *loader {
if debug {
ld.Config.Logf = log.Printf
} else {
- ld.Config.Logf = func(format string, args ...interface{}) {}
+ ld.Config.Logf = func(format string, args ...any) {}
}
}
if ld.Config.Mode == 0 {
diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go
index 06fa488d1ed..2623aa5a03b 100644
--- a/go/packages/packages_test.go
+++ b/go/packages/packages_test.go
@@ -20,6 +20,7 @@ import (
"path/filepath"
"reflect"
"runtime"
+ "slices"
"sort"
"strings"
"testing"
@@ -27,7 +28,9 @@ import (
"time"
"github.com/google/go-cmp/cmp"
+ "golang.org/x/sync/errgroup"
"golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/packagestest"
"golang.org/x/tools/internal/testenv"
@@ -129,7 +132,7 @@ func TestLoadImportsGraph(t *testing.T) { testAllOrModulesParallel(t, testLoadIm
func testLoadImportsGraph(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; const A = 1`,
"b/b.go": `package b; import ("golang.org/fake/a"; _ "container/list"); var B = a.A`,
"c/c.go": `package c; import (_ "golang.org/fake/b"; _ "unsafe")`,
@@ -305,7 +308,7 @@ func TestLoadImportsTestVariants(t *testing.T) {
func testLoadImportsTestVariants(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import _ "golang.org/fake/b"`,
"b/b.go": `package b`,
"b/b_test.go": `package b`,
@@ -346,11 +349,11 @@ func TestLoadAbsolutePath(t *testing.T) {
exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{
Name: "golang.org/gopatha",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a`,
}}, {
Name: "golang.org/gopathb",
- Files: map[string]interface{}{
+ Files: map[string]any{
"b/b.go": `package b`,
}}})
defer exported.Cleanup()
@@ -381,13 +384,13 @@ func TestLoadArgumentListIsNotTooLong(t *testing.T) {
argMax := 1_000_000
exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{
Name: "golang.org/mod",
- Files: map[string]interface{}{
+ Files: map[string]any{
"main.go": `package main"`,
}}})
defer exported.Cleanup()
numOfPatterns := argMax/16 + 1 // the pattern below is approx. 16 chars
patterns := make([]string, numOfPatterns)
- for i := 0; i < numOfPatterns; i++ {
+ for i := range numOfPatterns {
patterns[i] = fmt.Sprintf("golang.org/mod/p%d", i)
} // patterns have more than argMax number of chars combined with whitespaces b/w patterns
@@ -402,7 +405,7 @@ func TestVendorImports(t *testing.T) {
exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import _ "b"; import _ "golang.org/fake/c";`,
"a/vendor/b/b.go": `package b; import _ "golang.org/fake/c"`,
"c/c.go": `package c; import _ "b"`,
@@ -463,7 +466,7 @@ func TestConfigDir(t *testing.T) { testAllOrModulesParallel(t, testConfigDir) }
func testConfigDir(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; const Name = "a" `,
"a/b/b.go": `package b; const Name = "a/b"`,
"b/b.go": `package b; const Name = "b"`,
@@ -522,7 +525,7 @@ func testConfigFlags(t *testing.T, exporter packagestest.Exporter) {
// Test satisfying +build line tags, with -tags flag.
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
// package a
"a/a.go": `package a; import _ "golang.org/fake/a/b"`,
"a/b.go": `// +build tag
@@ -587,7 +590,7 @@ func testLoadTypes(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"; import "golang.org/fake/c"; const A = "a" + b.B + c.C`,
"b/b.go": `package b; const B = "b"`,
"c/c.go": `package c; const C = "c" + 1`,
@@ -640,7 +643,7 @@ func TestLoadTypesBits(t *testing.T) { testAllOrModulesParallel(t, testLoadTypes
func testLoadTypesBits(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`,
"b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`,
"c/c.go": `package c; import "golang.org/fake/d"; const C = "c" + d.D`,
@@ -716,7 +719,7 @@ func TestLoadSyntaxOK(t *testing.T) { testAllOrModulesParallel(t, testLoadSyntax
func testLoadSyntaxOK(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`,
"b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`,
"c/c.go": `package c; import "golang.org/fake/d"; const C = "c" + d.D`,
@@ -807,7 +810,7 @@ func testLoadDiamondTypes(t *testing.T, exporter packagestest.Exporter) {
// We make a diamond dependency and check the type d.D is the same through both paths
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import ("golang.org/fake/b"; "golang.org/fake/c"); var _ = b.B == c.C`,
"b/b.go": `package b; import "golang.org/fake/d"; var B d.D`,
"c/c.go": `package c; import "golang.org/fake/d"; var C d.D`,
@@ -850,7 +853,7 @@ func testLoadSyntaxError(t *testing.T, exporter packagestest.Exporter) {
// should be IllTyped.
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`,
"b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`,
"c/c.go": `package c; import "golang.org/fake/d"; const C = "c" + d.D`,
@@ -922,7 +925,7 @@ func TestParseFileModifyAST(t *testing.T) { testAllOrModulesParallel(t, testPars
func testParseFileModifyAST(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; const A = "a" `,
}}})
defer exported.Cleanup()
@@ -1010,7 +1013,7 @@ func testLoadAllSyntaxImportErrors(t *testing.T, exporter packagestest.Exporter)
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"unicycle/unicycle.go": `package unicycle; import _ "unicycle"`,
"bicycle1/bicycle1.go": `package bicycle1; import _ "bicycle2"`,
"bicycle2/bicycle2.go": `package bicycle2; import _ "bicycle1"`,
@@ -1090,7 +1093,7 @@ func TestAbsoluteFilenames(t *testing.T) { testAllOrModulesParallel(t, testAbsol
func testAbsoluteFilenames(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; const A = 1`,
"b/b.go": `package b; import ("golang.org/fake/a"; _ "errors"); var B = a.A`,
"b/vendor/a/a.go": `package a; const A = 1`,
@@ -1180,7 +1183,7 @@ func TestContains(t *testing.T) { testAllOrModulesParallel(t, testContains) }
func testContains(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"`,
"b/b.go": `package b; import "golang.org/fake/c"`,
"c/c.go": `package c`,
@@ -1219,7 +1222,7 @@ func testSizes(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "unsafe"; const WordSize = 8*unsafe.Sizeof(int(0))`,
}}})
defer exported.Cleanup()
@@ -1257,7 +1260,7 @@ func TestNeedTypeSizesWithBadGOARCH(t *testing.T) {
testAllOrModulesParallel(t, func(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "testdata",
- Files: map[string]interface{}{"a/a.go": `package a`}}})
+ Files: map[string]any{"a/a.go": `package a`}}})
defer exported.Cleanup()
exported.Config.Mode = packages.NeedTypesSizes // or {,Info,Sizes}
@@ -1280,7 +1283,7 @@ func TestContainsFallbackSticks(t *testing.T) {
func testContainsFallbackSticks(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import "golang.org/fake/b"`,
"b/b.go": `package b; import "golang.org/fake/c"`,
"c/c.go": `package c`,
@@ -1313,7 +1316,7 @@ func TestNoPatterns(t *testing.T) { testAllOrModulesParallel(t, testNoPatterns)
func testNoPatterns(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a;`,
"a/b/b.go": `package b;`,
}}})
@@ -1336,7 +1339,7 @@ func testJSON(t *testing.T, exporter packagestest.Exporter) {
// TODO: add in some errors
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; const A = 1`,
"b/b.go": `package b; import "golang.org/fake/a"; var B = a.A`,
"c/c.go": `package c; import "golang.org/fake/b" ; var C = b.B`,
@@ -1503,7 +1506,7 @@ func TestPatternPassthrough(t *testing.T) { testAllOrModulesParallel(t, testPatt
func testPatternPassthrough(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a;`,
}}})
defer exported.Cleanup()
@@ -1563,7 +1566,7 @@ EOF
}
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"bin/gopackagesdriver": driverScript,
"golist/golist.go": "package golist",
}}})
@@ -1610,7 +1613,7 @@ EOF
defer os.Setenv(pathKey, oldPath)
// Clone exported.Config
config := exported.Config
- config.Env = append([]string{}, exported.Config.Env...)
+ config.Env = slices.Clone(exported.Config.Env)
config.Env = append(config.Env, "GOPACKAGESDRIVER="+test.driver)
pkgs, err := packages.Load(exported.Config, "golist")
if err != nil {
@@ -1639,7 +1642,7 @@ func TestBasicXTest(t *testing.T) { testAllOrModulesParallel(t, testBasicXTest)
func testBasicXTest(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a;`,
"a/a_test.go": `package a_test;`,
}}})
@@ -1657,7 +1660,7 @@ func TestErrorMissingFile(t *testing.T) { testAllOrModulesParallel(t, testErrorM
func testErrorMissingFile(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a_test.go": `package a;`,
}}})
defer exported.Cleanup()
@@ -1685,11 +1688,11 @@ func TestReturnErrorWhenUsingNonGoFiles(t *testing.T) {
func testReturnErrorWhenUsingNonGoFiles(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/gopatha",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a`,
}}, {
Name: "golang.org/gopathb",
- Files: map[string]interface{}{
+ Files: map[string]any{
"b/b.c": `package b`,
}}})
defer exported.Cleanup()
@@ -1713,7 +1716,7 @@ func TestReturnErrorWhenUsingGoFilesInMultipleDirectories(t *testing.T) {
func testReturnErrorWhenUsingGoFilesInMultipleDirectories(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/gopatha",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a`,
"b/b.go": `package b`,
}}})
@@ -1745,7 +1748,7 @@ func TestReturnErrorForUnexpectedDirectoryLayout(t *testing.T) {
func testReturnErrorForUnexpectedDirectoryLayout(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/gopatha",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/testdata/a.go": `package a; import _ "b"`,
"a/vendor/b/b.go": `package b; import _ "fmt"`,
}}})
@@ -1774,7 +1777,7 @@ func TestMissingDependency(t *testing.T) { testAllOrModulesParallel(t, testMissi
func testMissingDependency(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import _ "this/package/doesnt/exist"`,
}}})
defer exported.Cleanup()
@@ -1796,7 +1799,7 @@ func TestAdHocContains(t *testing.T) { testAllOrModulesParallel(t, testAdHocCont
func testAdHocContains(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a;`,
}}})
defer exported.Cleanup()
@@ -1839,7 +1842,7 @@ func testCgoNoCcompiler(t *testing.T, exporter packagestest.Exporter) {
testenv.NeedsTool(t, "cgo")
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a
import "net/http"
const A = http.MethodGet
@@ -1873,7 +1876,7 @@ func testCgoMissingFile(t *testing.T, exporter packagestest.Exporter) {
testenv.NeedsTool(t, "cgo")
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a
// #include "foo.h"
@@ -1962,7 +1965,7 @@ func testCgoNoSyntax(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"c/c.go": `package c; import "C"`,
},
}})
@@ -1978,7 +1981,6 @@ func testCgoNoSyntax(t *testing.T, exporter packagestest.Exporter) {
packages.NeedName | packages.NeedImports,
}
for _, mode := range modes {
- mode := mode
t.Run(fmt.Sprint(mode), func(t *testing.T) {
exported.Config.Mode = mode
pkgs, err := packages.Load(exported.Config, "golang.org/fake/c")
@@ -2005,7 +2007,7 @@ func testCgoBadPkgConfig(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"c/c.go": `package c
// #cgo pkg-config: --cflags -- foo
@@ -2074,7 +2076,7 @@ func TestIssue32814(t *testing.T) { testAllOrModulesParallel(t, testIssue32814)
func testIssue32814(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{}}})
+ Files: map[string]any{}}})
defer exported.Cleanup()
exported.Config.Mode = packages.NeedName | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypesSizes
@@ -2103,7 +2105,7 @@ func TestLoadTypesInfoWithoutNeedDeps(t *testing.T) {
func testLoadTypesInfoWithoutNeedDeps(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import _ "golang.org/fake/b"`,
"b/b.go": `package b`,
}}})
@@ -2130,7 +2132,7 @@ func TestLoadWithNeedDeps(t *testing.T) {
func testLoadWithNeedDeps(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import _ "golang.org/fake/b"`,
"b/b.go": `package b; import _ "golang.org/fake/c"`,
"c/c.go": `package c`,
@@ -2174,7 +2176,7 @@ func TestImpliedLoadMode(t *testing.T) {
func testImpliedLoadMode(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import _ "golang.org/fake/b"`,
"b/b.go": `package b`,
}}})
@@ -2243,7 +2245,7 @@ func TestMultiplePackageVersionsIssue36188(t *testing.T) {
func testMultiplePackageVersionsIssue36188(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import _ "golang.org/fake/b"`,
"b/b.go": `package main`,
}}})
@@ -2363,7 +2365,7 @@ func TestCycleImportStack(t *testing.T) {
func testCycleImportStack(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; import _ "golang.org/fake/b"`,
"b/b.go": `package b; import _ "golang.org/fake/a"`,
}}})
@@ -2393,7 +2395,7 @@ func TestForTestField(t *testing.T) {
func testForTestField(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a; func hello() {};`,
"a/a_test.go": `package a; import "testing"; func TestA1(t *testing.T) {};`,
"a/x_test.go": `package a_test; import "testing"; func TestA2(t *testing.T) {};`,
@@ -2499,7 +2501,7 @@ func testIssue37098(t *testing.T, exporter packagestest.Exporter) {
// file.
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
// The "package" statement must be included for SWIG sources to
// be generated.
"a/a.go": "package a",
@@ -2550,7 +2552,7 @@ func TestIssue56632(t *testing.T) {
exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{
Name: "golang.org/issue56632",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a`,
"a/a_cgo.go": `package a
@@ -2593,7 +2595,7 @@ func testInvalidFilesInXTest(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"d/d.go": `package d; import "net/http"; const d = http.MethodGet; func Get() string { return d; }`,
"d/d2.go": ``, // invalid file
"d/d_test.go": `package d_test; import "testing"; import "golang.org/fake/d"; func TestD(t *testing.T) { d.Get(); }`,
@@ -2628,7 +2630,7 @@ func testTypecheckCgo(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"cgo/cgo.go": cgo,
},
},
@@ -2662,7 +2664,7 @@ func testIssue48226(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "golang.org/fake/syntax",
- Files: map[string]interface{}{
+ Files: map[string]any{
"syntax.go": `package test`,
},
},
@@ -2697,7 +2699,7 @@ func TestModule(t *testing.T) {
func testModule(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{"a/a.go": `package a`}}})
+ Files: map[string]any{"a/a.go": `package a`}}})
exported.Config.Mode = packages.NeedModule
rootDir := filepath.Dir(filepath.Dir(exported.File("golang.org/fake", "a/a.go")))
@@ -2746,7 +2748,7 @@ func testExternal_NotHandled(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a`,
"empty_driver/main.go": `package main
@@ -2787,7 +2789,7 @@ func main() {
t.Fatal(err)
}
- exported.Config.Env = append(append([]string{}, baseEnv...), "GOPACKAGESDRIVER="+emptyDriverPath)
+ exported.Config.Env = append(slices.Clone(baseEnv), "GOPACKAGESDRIVER="+emptyDriverPath)
initial, err := packages.Load(exported.Config, "golang.org/fake/a")
if err != nil {
t.Fatal(err)
@@ -2807,7 +2809,7 @@ func main() {
t.Fatal(err)
}
- exported.Config.Env = append(append([]string{}, baseEnv...), "GOPACKAGESDRIVER="+notHandledDriverPath)
+ exported.Config.Env = append(slices.Clone(baseEnv), "GOPACKAGESDRIVER="+notHandledDriverPath)
initial, err = packages.Load(exported.Config, "golang.org/fake/a")
if err != nil {
t.Fatal(err)
@@ -2825,7 +2827,7 @@ func TestInvalidPackageName(t *testing.T) {
func testInvalidPackageName(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"main.go": `package default
func main() {
@@ -3206,7 +3208,7 @@ func TestLoadTypesInfoWithoutSyntaxOrTypes(t *testing.T) {
func testLoadTypesInfoWithoutSyntaxOrTypes(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a/a.go": `package a;
func foo() int {
@@ -3400,3 +3402,89 @@ func writeTree(t *testing.T, archive string) string {
}
return root
}
+
+// This is not a test of go/packages at all: it's a test of whether it
+// is possible to delete the directory used by go list once it has
+// finished. It is intended to evaluate the hypothesis (to explain
+// issue #71544) that the go command, on Windows, occasionally fails
+// to release all its handles to the temporary directory even when it
+// should have finished.
+//
+// If this test ever fails, the combination of the gocommand package
+// and the go command itself has a bug.
+func TestRmdirAfterGoList_Runner(t *testing.T) {
+ t.Skip("golang/go#73503: this test is frequently flaky")
+
+ testRmdirAfterGoList(t, func(ctx context.Context, dir string) {
+ var runner gocommand.Runner
+ stdout, stderr, friendlyErr, err := runner.RunRaw(ctx, gocommand.Invocation{
+ Verb: "list",
+ Args: []string{"-json", "example.com/p"},
+ WorkingDir: dir,
+ })
+ if ctx.Err() != nil {
+ return // don't report error if canceled
+ }
+ if err != nil || friendlyErr != nil {
+ t.Fatalf("go list failed: %v, %v (stdout=%s stderr=%s)",
+ err, friendlyErr, stdout, stderr)
+ }
+ })
+}
+
+// TestRmdirAfterGoList_Direct is a variant of
+// TestRmdirAfterGoList_Runner that executes go list directly, to
+// control for the substantial logic of the gocommand package.
+//
+// If this test ever fails, the go command itself has a bug.
+func TestRmdirAfterGoList_Direct(t *testing.T) {
+ testRmdirAfterGoList(t, func(ctx context.Context, dir string) {
+ cmd := exec.Command("go", "list", "-json", "example.com/p")
+ cmd.Dir = dir
+ cmd.Stdout = new(strings.Builder)
+ cmd.Stderr = new(strings.Builder)
+ err := cmd.Run()
+ if ctx.Err() != nil {
+ return // don't report error if canceled
+ }
+ if err != nil {
+ t.Fatalf("go list failed: %v (stdout=%s stderr=%s)",
+ err, cmd.Stdout, cmd.Stderr)
+ }
+ })
+}
+
+func testRmdirAfterGoList(t *testing.T, f func(ctx context.Context, dir string)) {
+ testenv.NeedsExec(t)
+
+ dir := t.TempDir()
+ if err := os.Mkdir(filepath.Join(dir, "p"), 0777); err != nil {
+ t.Fatalf("mkdir p: %v", err)
+ }
+
+ // Create a go.mod file and 100 trivial Go files for the go command to read.
+ if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com"), 0666); err != nil {
+ t.Fatal(err)
+ }
+ for i := range 100 {
+ filename := filepath.Join(dir, fmt.Sprintf("p/%d.go", i))
+ if err := os.WriteFile(filename, []byte("package p"), 0666); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ g, ctx := errgroup.WithContext(context.Background())
+ for range 10 {
+ g.Go(func() error {
+ f(ctx, dir)
+ // Return an error so that concurrent invocations are canceled.
+ return fmt.Errorf("oops")
+ })
+ }
+ g.Wait() // ignore expected error
+
+ // This is the critical operation.
+ if err := os.RemoveAll(dir); err != nil {
+ t.Fatalf("failed to remove temp dir: %v", err)
+ }
+}
diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go
index dc41894a6ed..4be34191e62 100644
--- a/go/packages/packagestest/expect.go
+++ b/go/packages/packagestest/expect.go
@@ -72,7 +72,7 @@ const (
//
// It is safe to call this repeatedly with different method sets, but it is
// not safe to call it concurrently.
-func (e *Exported) Expect(methods map[string]interface{}) error {
+func (e *Exported) Expect(methods map[string]any) error {
if err := e.getNotes(); err != nil {
return err
}
@@ -98,7 +98,7 @@ func (e *Exported) Expect(methods map[string]interface{}) error {
n = &expect.Note{
Pos: n.Pos,
Name: markMethod,
- Args: []interface{}{n.Name, n.Name},
+ Args: []any{n.Name, n.Name},
}
}
mi, ok := ms[n.Name]
@@ -222,7 +222,7 @@ func (e *Exported) getMarkers() error {
}
// set markers early so that we don't call getMarkers again from Expect
e.markers = make(map[string]Range)
- return e.Expect(map[string]interface{}{
+ return e.Expect(map[string]any{
markMethod: e.Mark,
})
}
@@ -243,7 +243,7 @@ var (
// It takes the args remaining, and returns the args it did not consume.
// This allows a converter to consume 0 args for well known types, or multiple
// args for compound types.
-type converter func(*expect.Note, []interface{}) (reflect.Value, []interface{}, error)
+type converter func(*expect.Note, []any) (reflect.Value, []any, error)
// method is used to track information about Invoke methods that is expensive to
// calculate so that we can work it out once rather than per marker.
@@ -259,19 +259,19 @@ type method struct {
func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
switch {
case pt == noteType:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
return reflect.ValueOf(n), args, nil
}, nil
case pt == fsetType:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
return reflect.ValueOf(e.ExpectFileSet), args, nil
}, nil
case pt == exportedType:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
return reflect.ValueOf(e), args, nil
}, nil
case pt == posType:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
r, remains, err := e.rangeConverter(n, args)
if err != nil {
return reflect.Value{}, nil, err
@@ -279,7 +279,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
return reflect.ValueOf(r.Start), remains, nil
}, nil
case pt == positionType:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
r, remains, err := e.rangeConverter(n, args)
if err != nil {
return reflect.Value{}, nil, err
@@ -287,7 +287,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
return reflect.ValueOf(e.ExpectFileSet.Position(r.Start)), remains, nil
}, nil
case pt == rangeType:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
r, remains, err := e.rangeConverter(n, args)
if err != nil {
return reflect.Value{}, nil, err
@@ -295,7 +295,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
return reflect.ValueOf(r), remains, nil
}, nil
case pt == identifierType:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
if len(args) < 1 {
return reflect.Value{}, nil, fmt.Errorf("missing argument")
}
@@ -310,7 +310,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
}, nil
case pt == regexType:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
if len(args) < 1 {
return reflect.Value{}, nil, fmt.Errorf("missing argument")
}
@@ -323,7 +323,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
}, nil
case pt.Kind() == reflect.String:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
if len(args) < 1 {
return reflect.Value{}, nil, fmt.Errorf("missing argument")
}
@@ -339,7 +339,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
}
}, nil
case pt.Kind() == reflect.Int64:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
if len(args) < 1 {
return reflect.Value{}, nil, fmt.Errorf("missing argument")
}
@@ -353,7 +353,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
}
}, nil
case pt.Kind() == reflect.Bool:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
if len(args) < 1 {
return reflect.Value{}, nil, fmt.Errorf("missing argument")
}
@@ -366,7 +366,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
return reflect.ValueOf(b), args, nil
}, nil
case pt.Kind() == reflect.Slice:
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
converter, err := e.buildConverter(pt.Elem())
if err != nil {
return reflect.Value{}, nil, err
@@ -384,7 +384,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
}, nil
default:
if pt.Kind() == reflect.Interface && pt.NumMethod() == 0 {
- return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
+ return func(n *expect.Note, args []any) (reflect.Value, []any, error) {
if len(args) < 1 {
return reflect.Value{}, nil, fmt.Errorf("missing argument")
}
@@ -395,7 +395,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
}
}
-func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (Range, []interface{}, error) {
+func (e *Exported) rangeConverter(n *expect.Note, args []any) (Range, []any, error) {
tokFile := e.ExpectFileSet.File(n.Pos)
if len(args) < 1 {
return Range{}, nil, fmt.Errorf("missing argument")
diff --git a/go/packages/packagestest/expect_test.go b/go/packages/packagestest/expect_test.go
index 46d96d61fb9..70ff6656012 100644
--- a/go/packages/packagestest/expect_test.go
+++ b/go/packages/packagestest/expect_test.go
@@ -19,7 +19,7 @@ func TestExpect(t *testing.T) {
}})
defer exported.Cleanup()
checkCount := 0
- if err := exported.Expect(map[string]interface{}{
+ if err := exported.Expect(map[string]any{
"check": func(src, target token.Position) {
checkCount++
},
diff --git a/go/packages/packagestest/export.go b/go/packages/packagestest/export.go
index 47e6d11b94b..86da99ecdf3 100644
--- a/go/packages/packagestest/export.go
+++ b/go/packages/packagestest/export.go
@@ -101,7 +101,7 @@ type Module struct {
// The keys are the file fragment that follows the module name, the value can
// be a string or byte slice, in which case it is the contents of the
// file, otherwise it must be a Writer function.
- Files map[string]interface{}
+ Files map[string]any
// Overlay is the set of source file overlays for the module.
// The keys are the file fragment as in the Files configuration.
@@ -159,7 +159,6 @@ var All = []Exporter{GOPATH, Modules}
func TestAll(t *testing.T, f func(*testing.T, Exporter)) {
t.Helper()
for _, e := range All {
- e := e // in case f calls t.Parallel
t.Run(e.Name(), func(t *testing.T) {
t.Helper()
f(t, e)
@@ -173,7 +172,6 @@ func TestAll(t *testing.T, f func(*testing.T, Exporter)) {
func BenchmarkAll(b *testing.B, f func(*testing.B, Exporter)) {
b.Helper()
for _, e := range All {
- e := e // in case f calls t.Parallel
b.Run(e.Name(), func(b *testing.B) {
b.Helper()
f(b, e)
@@ -483,7 +481,7 @@ func GroupFilesByModules(root string) ([]Module, error) {
primarymod := &Module{
Name: root,
- Files: make(map[string]interface{}),
+ Files: make(map[string]any),
Overlay: make(map[string][]byte),
}
mods := map[string]*Module{
@@ -573,7 +571,7 @@ func GroupFilesByModules(root string) ([]Module, error) {
}
mods[path] = &Module{
Name: filepath.ToSlash(module),
- Files: make(map[string]interface{}),
+ Files: make(map[string]any),
Overlay: make(map[string][]byte),
}
currentModule = path
@@ -591,8 +589,8 @@ func GroupFilesByModules(root string) ([]Module, error) {
// This is to enable the common case in tests where you have a full copy of the
// package in your testdata.
// This will panic if there is any kind of error trying to walk the file tree.
-func MustCopyFileTree(root string) map[string]interface{} {
- result := map[string]interface{}{}
+func MustCopyFileTree(root string) map[string]any {
+ result := map[string]any{}
if err := filepath.Walk(filepath.FromSlash(root), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
diff --git a/go/packages/packagestest/export_test.go b/go/packages/packagestest/export_test.go
index eb13f560916..e3e4658efb6 100644
--- a/go/packages/packagestest/export_test.go
+++ b/go/packages/packagestest/export_test.go
@@ -16,7 +16,7 @@ import (
var testdata = []packagestest.Module{{
Name: "golang.org/fake1",
- Files: map[string]interface{}{
+ Files: map[string]any{
"a.go": packagestest.Symlink("testdata/a.go"), // broken symlink
"b.go": "invalid file contents",
},
@@ -26,22 +26,22 @@ var testdata = []packagestest.Module{{
},
}, {
Name: "golang.org/fake2",
- Files: map[string]interface{}{
+ Files: map[string]any{
"other/a.go": "package fake2",
},
}, {
Name: "golang.org/fake2/v2",
- Files: map[string]interface{}{
+ Files: map[string]any{
"other/a.go": "package fake2",
},
}, {
Name: "golang.org/fake3@v1.0.0",
- Files: map[string]interface{}{
+ Files: map[string]any{
"other/a.go": "package fake3",
},
}, {
Name: "golang.org/fake3@v1.1.0",
- Files: map[string]interface{}{
+ Files: map[string]any{
"other/a.go": "package fake3",
},
}}
@@ -97,13 +97,13 @@ func TestGroupFilesByModules(t *testing.T) {
want: []packagestest.Module{
{
Name: "testdata/groups/one",
- Files: map[string]interface{}{
+ Files: map[string]any{
"main.go": true,
},
},
{
Name: "example.com/extra",
- Files: map[string]interface{}{
+ Files: map[string]any{
"help.go": true,
},
},
@@ -114,7 +114,7 @@ func TestGroupFilesByModules(t *testing.T) {
want: []packagestest.Module{
{
Name: "testdata/groups/two",
- Files: map[string]interface{}{
+ Files: map[string]any{
"main.go": true,
"expect/yo.go": true,
"expect/yo_test.go": true,
@@ -122,33 +122,33 @@ func TestGroupFilesByModules(t *testing.T) {
},
{
Name: "example.com/extra",
- Files: map[string]interface{}{
+ Files: map[string]any{
"yo.go": true,
"geez/help.go": true,
},
},
{
Name: "example.com/extra/v2",
- Files: map[string]interface{}{
+ Files: map[string]any{
"me.go": true,
"geez/help.go": true,
},
},
{
Name: "example.com/tempmod",
- Files: map[string]interface{}{
+ Files: map[string]any{
"main.go": true,
},
},
{
Name: "example.com/what@v1.0.0",
- Files: map[string]interface{}{
+ Files: map[string]any{
"main.go": true,
},
},
{
Name: "example.com/what@v1.1.0",
- Files: map[string]interface{}{
+ Files: map[string]any{
"main.go": true,
},
},
diff --git a/go/ssa/builder.go b/go/ssa/builder.go
index 4cd71260b61..84ccbc0927a 100644
--- a/go/ssa/builder.go
+++ b/go/ssa/builder.go
@@ -82,6 +82,8 @@ import (
"runtime"
"sync"
+ "slices"
+
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/versions"
)
@@ -559,7 +561,7 @@ func (sb *storebuf) emit(fn *Function) {
// literal that may reference parts of the LHS.
func (b *builder) assign(fn *Function, loc lvalue, e ast.Expr, isZero bool, sb *storebuf) {
// Can we initialize it in place?
- if e, ok := unparen(e).(*ast.CompositeLit); ok {
+ if e, ok := ast.Unparen(e).(*ast.CompositeLit); ok {
// A CompositeLit never evaluates to a pointer,
// so if the type of the location is a pointer,
// an &-operation is implied.
@@ -614,7 +616,7 @@ func (b *builder) assign(fn *Function, loc lvalue, e ast.Expr, isZero bool, sb *
// expr lowers a single-result expression e to SSA form, emitting code
// to fn and returning the Value defined by the expression.
func (b *builder) expr(fn *Function, e ast.Expr) Value {
- e = unparen(e)
+ e = ast.Unparen(e)
tv := fn.info.Types[e]
@@ -704,7 +706,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value {
return y
}
// Call to "intrinsic" built-ins, e.g. new, make, panic.
- if id, ok := unparen(e.Fun).(*ast.Ident); ok {
+ if id, ok := ast.Unparen(e.Fun).(*ast.Ident); ok {
if obj, ok := fn.info.Uses[id].(*types.Builtin); ok {
if v := b.builtin(fn, obj, e.Args, fn.typ(tv.Type), e.Lparen); v != nil {
return v
@@ -721,7 +723,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value {
switch e.Op {
case token.AND: // &X --- potentially escaping.
addr := b.addr(fn, e.X, true)
- if _, ok := unparen(e.X).(*ast.StarExpr); ok {
+ if _, ok := ast.Unparen(e.X).(*ast.StarExpr); ok {
// &*p must panic if p is nil (http://golang.org/s/go12nil).
// For simplicity, we'll just (suboptimally) rely
// on the side effects of a load.
@@ -1002,7 +1004,7 @@ func (b *builder) setCallFunc(fn *Function, e *ast.CallExpr, c *CallCommon) {
c.pos = e.Lparen
// Is this a method call?
- if selector, ok := unparen(e.Fun).(*ast.SelectorExpr); ok {
+ if selector, ok := ast.Unparen(e.Fun).(*ast.SelectorExpr); ok {
sel := fn.selection(selector)
if sel != nil && sel.kind == types.MethodVal {
obj := sel.obj.(*types.Func)
@@ -1372,7 +1374,7 @@ func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero
// An &-operation may be implied:
// map[*struct{}]bool{&struct{}{}: true}
wantAddr := false
- if _, ok := unparen(e.Key).(*ast.CompositeLit); ok {
+ if _, ok := ast.Unparen(e.Key).(*ast.CompositeLit); ok {
wantAddr = isPointerCore(t.Key())
}
@@ -1547,9 +1549,9 @@ func (b *builder) typeSwitchStmt(fn *Function, s *ast.TypeSwitchStmt, label *lbl
var x Value
switch ass := s.Assign.(type) {
case *ast.ExprStmt: // x.(type)
- x = b.expr(fn, unparen(ass.X).(*ast.TypeAssertExpr).X)
+ x = b.expr(fn, ast.Unparen(ass.X).(*ast.TypeAssertExpr).X)
case *ast.AssignStmt: // y := x.(type)
- x = b.expr(fn, unparen(ass.Rhs[0]).(*ast.TypeAssertExpr).X)
+ x = b.expr(fn, ast.Unparen(ass.Rhs[0]).(*ast.TypeAssertExpr).X)
}
done := fn.newBasicBlock("typeswitch.done")
@@ -1667,7 +1669,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) {
}
case *ast.AssignStmt: // x := <-ch
- recv := unparen(comm.Rhs[0]).(*ast.UnaryExpr)
+ recv := ast.Unparen(comm.Rhs[0]).(*ast.UnaryExpr)
st = &SelectState{
Dir: types.RecvOnly,
Chan: b.expr(fn, recv.X),
@@ -1678,7 +1680,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) {
}
case *ast.ExprStmt: // <-ch
- recv := unparen(comm.X).(*ast.UnaryExpr)
+ recv := ast.Unparen(comm.X).(*ast.UnaryExpr)
st = &SelectState{
Dir: types.RecvOnly,
Chan: b.expr(fn, recv.X),
@@ -2021,8 +2023,8 @@ func (b *builder) forStmtGo122(fn *Function, s *ast.ForStmt, label *lblock) {
// Remove instructions for phi, load, and store.
// lift() will remove the unused i_next *Alloc.
isDead := func(i Instruction) bool { return dead[i] }
- loop.Instrs = removeInstrsIf(loop.Instrs, isDead)
- post.Instrs = removeInstrsIf(post.Instrs, isDead)
+ loop.Instrs = slices.DeleteFunc(loop.Instrs, isDead)
+ post.Instrs = slices.DeleteFunc(post.Instrs, isDead)
}
}
diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go
index 2589cc82bb6..a48723bd271 100644
--- a/go/ssa/builder_test.go
+++ b/go/ssa/builder_test.go
@@ -613,7 +613,6 @@ var indirect = R[int].M
"(p.S[int]).M[int]",
},
} {
- entry := entry
t.Run(entry.name, func(t *testing.T) {
v := p.Var(entry.name)
if v == nil {
@@ -1011,7 +1010,6 @@ func TestGo117Builtins(t *testing.T) {
}
for _, tc := range tests {
- tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fset := token.NewFileSet()
@@ -1466,7 +1464,6 @@ func TestBuildPackageGo120(t *testing.T) {
}
for _, tc := range tests {
- tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fset := token.NewFileSet()
diff --git a/go/ssa/const_test.go b/go/ssa/const_test.go
index 6738f07b2ef..6097bd93757 100644
--- a/go/ssa/const_test.go
+++ b/go/ssa/const_test.go
@@ -39,9 +39,9 @@ func TestConstString(t *testing.T) {
}
for _, test := range []struct {
- expr string // type expression
- constant interface{} // constant value
- want string // expected String() value
+ expr string // type expression
+ constant any // constant value
+ want string // expected String() value
}{
{"int", int64(0), "0:int"},
{"int64", int64(0), "0:int64"},
diff --git a/go/ssa/dom.go b/go/ssa/dom.go
index f490986140c..78f651c8ee9 100644
--- a/go/ssa/dom.go
+++ b/go/ssa/dom.go
@@ -22,6 +22,7 @@ import (
"fmt"
"math/big"
"os"
+ "slices"
"sort"
)
@@ -43,7 +44,7 @@ func (b *BasicBlock) Dominates(c *BasicBlock) bool {
// DomPreorder returns a new slice containing the blocks of f
// in a preorder traversal of the dominator tree.
func (f *Function) DomPreorder() []*BasicBlock {
- slice := append([]*BasicBlock(nil), f.Blocks...)
+ slice := slices.Clone(f.Blocks)
sort.Slice(slice, func(i, j int) bool {
return slice[i].dom.pre < slice[j].dom.pre
})
@@ -54,7 +55,7 @@ func (f *Function) DomPreorder() []*BasicBlock {
// in a postorder traversal of the dominator tree.
// (This is not the same as a postdominance order.)
func (f *Function) DomPostorder() []*BasicBlock {
- slice := append([]*BasicBlock(nil), f.Blocks...)
+ slice := slices.Clone(f.Blocks)
sort.Slice(slice, func(i, j int) bool {
return slice[i].dom.post < slice[j].dom.post
})
@@ -277,8 +278,8 @@ func sanityCheckDomTree(f *Function) {
// Check the entire relation. O(n^2).
// The Recover block (if any) must be treated specially so we skip it.
ok := true
- for i := 0; i < n; i++ {
- for j := 0; j < n; j++ {
+ for i := range n {
+ for j := range n {
b, c := f.Blocks[i], f.Blocks[j]
if c == f.Recover {
continue
diff --git a/go/ssa/emit.go b/go/ssa/emit.go
index a3d41ad95a4..e53ebf5a7fd 100644
--- a/go/ssa/emit.go
+++ b/go/ssa/emit.go
@@ -81,7 +81,7 @@ func emitDebugRef(f *Function, e ast.Expr, v Value, isAddr bool) {
panic("nil")
}
var obj types.Object
- e = unparen(e)
+ e = ast.Unparen(e)
if id, ok := e.(*ast.Ident); ok {
if isBlankIdent(id) {
return
@@ -496,7 +496,7 @@ func emitTailCall(f *Function, call *Call) {
case 1:
ret.Results = []Value{tuple}
default:
- for i := 0; i < nr; i++ {
+ for i := range nr {
v := emitExtract(f, tuple, i)
// TODO(adonovan): in principle, this is required:
// v = emitConv(f, o.Type, f.Signature.Results[i].Type)
diff --git a/go/ssa/example_test.go b/go/ssa/example_test.go
index e0fba0be681..03775414df2 100644
--- a/go/ssa/example_test.go
+++ b/go/ssa/example_test.go
@@ -3,9 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !android && !ios && (unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || plan9 || windows)
-// +build !android
-// +build !ios
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris plan9 windows
package ssa_test
diff --git a/go/ssa/func.go b/go/ssa/func.go
index 010c128a9ec..2d52309b623 100644
--- a/go/ssa/func.go
+++ b/go/ssa/func.go
@@ -13,6 +13,7 @@ import (
"go/token"
"go/types"
"io"
+ "iter"
"os"
"strings"
@@ -187,8 +188,7 @@ func targetedBlock(f *Function, tok token.Token) *BasicBlock {
}
// instrs returns an iterator that returns each reachable instruction of the SSA function.
-// TODO: return an iter.Seq once x/tools is on 1.23
-func (f *Function) instrs() func(yield func(i Instruction) bool) {
+func (f *Function) instrs() iter.Seq[Instruction] {
return func(yield func(i Instruction) bool) {
for _, block := range f.Blocks {
for _, instr := range block.Instrs {
@@ -817,7 +817,7 @@ func blockExit(fn *Function, block *BasicBlock, pos token.Pos) *exit {
return e
}
-// blockExit creates a new exit to a yield fn that returns the source function.
+// returnExit creates a new exit to a yield fn that returns the source function.
func returnExit(fn *Function, pos token.Pos) *exit {
e := &exit{
id: unique(fn),
diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go
index 2512f32976c..20a0986e6d3 100644
--- a/go/ssa/instantiate.go
+++ b/go/ssa/instantiate.go
@@ -7,6 +7,7 @@ package ssa
import (
"fmt"
"go/types"
+ "slices"
"sync"
)
@@ -122,10 +123,5 @@ func (prog *Program) isParameterized(ts ...types.Type) bool {
// handle the most common but shallow cases such as T, pkg.T,
// *T without consulting the cache under the lock.
- for _, t := range ts {
- if prog.hasParams.Has(t) {
- return true
- }
- }
- return false
+ return slices.ContainsFunc(ts, prog.hasParams.Has)
}
diff --git a/go/ssa/interp/external.go b/go/ssa/interp/external.go
index 2a3a7e5b79e..2fb683c07fe 100644
--- a/go/ssa/interp/external.go
+++ b/go/ssa/interp/external.go
@@ -9,6 +9,7 @@ package interp
import (
"bytes"
+ "maps"
"math"
"os"
"runtime"
@@ -30,7 +31,7 @@ var externals = make(map[string]externalFn)
func init() {
// That little dot ۰ is an Arabic zero numeral (U+06F0), categories [Nd].
- for k, v := range map[string]externalFn{
+ maps.Copy(externals, map[string]externalFn{
"(reflect.Value).Bool": ext۰reflect۰Value۰Bool,
"(reflect.Value).CanAddr": ext۰reflect۰Value۰CanAddr,
"(reflect.Value).CanInterface": ext۰reflect۰Value۰CanInterface,
@@ -111,9 +112,7 @@ func init() {
"strings.ToLower": ext۰strings۰ToLower,
"time.Sleep": ext۰time۰Sleep,
"unicode/utf8.DecodeRuneInString": ext۰unicode۰utf8۰DecodeRuneInString,
- } {
- externals[k] = v
- }
+ })
}
func ext۰bytes۰Equal(fr *frame, args []value) value {
diff --git a/go/ssa/interp/interp.go b/go/ssa/interp/interp.go
index f80db0676c7..7bd06120f6c 100644
--- a/go/ssa/interp/interp.go
+++ b/go/ssa/interp/interp.go
@@ -109,7 +109,7 @@ type frame struct {
defers *deferred
result value
panicking bool
- panic interface{}
+ panic any
phitemps []value // temporaries for parallel phi assignment
}
diff --git a/go/ssa/interp/map.go b/go/ssa/interp/map.go
index f5d5f230b73..e96e44df2b9 100644
--- a/go/ssa/interp/map.go
+++ b/go/ssa/interp/map.go
@@ -17,7 +17,7 @@ import (
type hashable interface {
hash(t types.Type) int
- eq(t types.Type, x interface{}) bool
+ eq(t types.Type, x any) bool
}
type entry struct {
diff --git a/go/ssa/interp/reflect.go b/go/ssa/interp/reflect.go
index 8259e56d860..22f8cde89c0 100644
--- a/go/ssa/interp/reflect.go
+++ b/go/ssa/interp/reflect.go
@@ -231,7 +231,7 @@ func reflectKind(t types.Type) reflect.Kind {
case *types.Map:
return reflect.Map
case *types.Pointer:
- return reflect.Ptr
+ return reflect.Pointer
case *types.Slice:
return reflect.Slice
case *types.Struct:
@@ -510,7 +510,7 @@ func newMethod(pkg *ssa.Package, recvType types.Type, name string) *ssa.Function
// that is needed is the "pointerness" of Recv.Type, and for
// now, we'll set it to always be false since we're only
// concerned with rtype. Encapsulate this better.
- sig := types.NewSignature(types.NewParam(token.NoPos, nil, "recv", recvType), nil, nil, false)
+ sig := types.NewSignatureType(types.NewParam(token.NoPos, nil, "recv", recvType), nil, nil, nil, nil, false)
fn := pkg.Prog.NewFunction(name, sig, "fake reflect method")
fn.Pkg = pkg
return fn
diff --git a/go/ssa/interp/value.go b/go/ssa/interp/value.go
index bd681cb6152..4d65aa6c83e 100644
--- a/go/ssa/interp/value.go
+++ b/go/ssa/interp/value.go
@@ -48,7 +48,7 @@ import (
"golang.org/x/tools/go/types/typeutil"
)
-type value interface{}
+type value any
type tuple []value
@@ -123,7 +123,7 @@ func usesBuiltinMap(t types.Type) bool {
panic(fmt.Sprintf("invalid map key type: %T", t))
}
-func (x array) eq(t types.Type, _y interface{}) bool {
+func (x array) eq(t types.Type, _y any) bool {
y := _y.(array)
tElt := t.Underlying().(*types.Array).Elem()
for i, xi := range x {
@@ -143,7 +143,7 @@ func (x array) hash(t types.Type) int {
return h
}
-func (x structure) eq(t types.Type, _y interface{}) bool {
+func (x structure) eq(t types.Type, _y any) bool {
y := _y.(structure)
tStruct := t.Underlying().(*types.Struct)
for i, n := 0, tStruct.NumFields(); i < n; i++ {
@@ -175,7 +175,7 @@ func sameType(x, y types.Type) bool {
return y != nil && types.Identical(x, y)
}
-func (x iface) eq(t types.Type, _y interface{}) bool {
+func (x iface) eq(t types.Type, _y any) bool {
y := _y.(iface)
return sameType(x.t, y.t) && (x.t == nil || equals(x.t, x.v, y.v))
}
@@ -188,7 +188,7 @@ func (x rtype) hash(_ types.Type) int {
return hashType(x.t)
}
-func (x rtype) eq(_ types.Type, y interface{}) bool {
+func (x rtype) eq(_ types.Type, y any) bool {
return types.Identical(x.t, y.(rtype).t)
}
diff --git a/go/ssa/lift.go b/go/ssa/lift.go
index aada3dc3227..d7c1bf5063e 100644
--- a/go/ssa/lift.go
+++ b/go/ssa/lift.go
@@ -43,6 +43,7 @@ import (
"go/token"
"math/big"
"os"
+ "slices"
"golang.org/x/tools/internal/typeparams"
)
@@ -105,23 +106,7 @@ func buildDomFrontier(fn *Function) domFrontier {
}
func removeInstr(refs []Instruction, instr Instruction) []Instruction {
- return removeInstrsIf(refs, func(i Instruction) bool { return i == instr })
-}
-
-func removeInstrsIf(refs []Instruction, p func(Instruction) bool) []Instruction {
- // TODO(taking): replace with go1.22 slices.DeleteFunc.
- i := 0
- for _, ref := range refs {
- if p(ref) {
- continue
- }
- refs[i] = ref
- i++
- }
- for j := i; j != len(refs); j++ {
- refs[j] = nil // aid GC
- }
- return refs[:i]
+ return slices.DeleteFunc(refs, func(i Instruction) bool { return i == instr })
}
// lift replaces local and new Allocs accessed only with
@@ -389,7 +374,7 @@ func (s *blockSet) add(b *BasicBlock) bool {
// returns its index, or returns -1 if empty.
func (s *blockSet) take() int {
l := s.BitLen()
- for i := 0; i < l; i++ {
+ for i := range l {
if s.Bit(i) == 1 {
s.SetBit(&s.Int, i, 0)
return i
@@ -418,10 +403,8 @@ func liftAlloc(df domFrontier, alloc *Alloc, newPhis newPhiMap, fresh *int) bool
// Don't lift result values in functions that defer
// calls that may recover from panic.
if fn := alloc.Parent(); fn.Recover != nil {
- for _, nr := range fn.results {
- if nr == alloc {
- return false
- }
+ if slices.Contains(fn.results, alloc) {
+ return false
}
}
diff --git a/go/ssa/mode.go b/go/ssa/mode.go
index 8381639a585..61c91452ce2 100644
--- a/go/ssa/mode.go
+++ b/go/ssa/mode.go
@@ -108,4 +108,4 @@ func (m *BuilderMode) Set(s string) error {
}
// Get returns m.
-func (m BuilderMode) Get() interface{} { return m }
+func (m BuilderMode) Get() any { return m }
diff --git a/go/ssa/print.go b/go/ssa/print.go
index 432c4b05b6d..8b92d08463a 100644
--- a/go/ssa/print.go
+++ b/go/ssa/print.go
@@ -387,7 +387,7 @@ func (s *MapUpdate) String() string {
func (s *DebugRef) String() string {
p := s.Parent().Prog.Fset.Position(s.Pos())
- var descr interface{}
+ var descr any
if s.object != nil {
descr = s.object // e.g. "var x int"
} else {
diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go
index e35e4d79357..b11680a1e1d 100644
--- a/go/ssa/sanity.go
+++ b/go/ssa/sanity.go
@@ -14,6 +14,7 @@ import (
"go/types"
"io"
"os"
+ "slices"
"strings"
)
@@ -48,7 +49,7 @@ func mustSanityCheck(fn *Function, reporter io.Writer) {
}
}
-func (s *sanity) diagnostic(prefix, format string, args ...interface{}) {
+func (s *sanity) diagnostic(prefix, format string, args ...any) {
fmt.Fprintf(s.reporter, "%s: function %s", prefix, s.fn)
if s.block != nil {
fmt.Fprintf(s.reporter, ", block %s", s.block)
@@ -58,12 +59,12 @@ func (s *sanity) diagnostic(prefix, format string, args ...interface{}) {
io.WriteString(s.reporter, "\n")
}
-func (s *sanity) errorf(format string, args ...interface{}) {
+func (s *sanity) errorf(format string, args ...any) {
s.insane = true
s.diagnostic("Error", format, args...)
}
-func (s *sanity) warnf(format string, args ...interface{}) {
+func (s *sanity) warnf(format string, args ...any) {
s.diagnostic("Warning", format, args...)
}
@@ -119,13 +120,7 @@ func (s *sanity) checkInstr(idx int, instr Instruction) {
case *Alloc:
if !instr.Heap {
- found := false
- for _, l := range s.fn.Locals {
- if l == instr {
- found = true
- break
- }
- }
+ found := slices.Contains(s.fn.Locals, instr)
if !found {
s.errorf("local alloc %s = %s does not appear in Function.Locals", instr.Name(), instr)
}
@@ -282,13 +277,7 @@ func (s *sanity) checkBlock(b *BasicBlock, index int) {
// Check predecessor and successor relations are dual,
// and that all blocks in CFG belong to same function.
for _, a := range b.Preds {
- found := false
- for _, bb := range a.Succs {
- if bb == b {
- found = true
- break
- }
- }
+ found := slices.Contains(a.Succs, b)
if !found {
s.errorf("expected successor edge in predecessor %s; found only: %s", a, a.Succs)
}
@@ -297,13 +286,7 @@ func (s *sanity) checkBlock(b *BasicBlock, index int) {
}
}
for _, c := range b.Succs {
- found := false
- for _, bb := range c.Preds {
- if bb == b {
- found = true
- break
- }
- }
+ found := slices.Contains(c.Preds, b)
if !found {
s.errorf("expected predecessor edge in successor %s; found only: %s", c, c.Preds)
}
@@ -529,12 +512,10 @@ func (s *sanity) checkFunction(fn *Function) bool {
// Build the set of valid referrers.
s.instrs = make(map[Instruction]unit)
- // TODO: switch to range-over-func when x/tools updates to 1.23.
// instrs are the instructions that are present in the function.
- fn.instrs()(func(instr Instruction) bool {
+ for instr := range fn.instrs() {
s.instrs[instr] = unit{}
- return true
- })
+ }
// Check all Locals allocations appear in the function instruction.
for i, l := range fn.Locals {
diff --git a/go/ssa/source.go b/go/ssa/source.go
index 055a6b1ef5f..d0cc1f4861a 100644
--- a/go/ssa/source.go
+++ b/go/ssa/source.go
@@ -153,7 +153,7 @@ func findNamedFunc(pkg *Package, pos token.Pos) *Function {
// the ssa.Value.)
func (f *Function) ValueForExpr(e ast.Expr) (value Value, isAddr bool) {
if f.debugInfo() { // (opt)
- e = unparen(e)
+ e = ast.Unparen(e)
for _, b := range f.Blocks {
for _, instr := range b.Instrs {
if ref, ok := instr.(*DebugRef); ok {
diff --git a/go/ssa/ssautil/load_test.go b/go/ssa/ssautil/load_test.go
index 10375a3227f..cf157fe4401 100644
--- a/go/ssa/ssautil/load_test.go
+++ b/go/ssa/ssautil/load_test.go
@@ -154,7 +154,7 @@ func TestIssue53604(t *testing.T) {
e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
{
Name: "golang.org/fake",
- Files: map[string]interface{}{
+ Files: map[string]any{
"x/x.go": `package x; import "golang.org/fake/y"; var V = y.F()`,
"y/y.go": `package y; import "golang.org/fake/z"; var F = func () *int { return &z.Z } `,
"z/z.go": `package z; var Z int`,
diff --git a/go/ssa/ssautil/switch_test.go b/go/ssa/ssautil/switch_test.go
index 081b09010ee..6ff5c9b92c3 100644
--- a/go/ssa/ssautil/switch_test.go
+++ b/go/ssa/ssautil/switch_test.go
@@ -5,7 +5,6 @@
// No testdata on Android.
//go:build !android
-// +build !android
package ssautil_test
diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go
index 9b78cfbf839..08df50b9eeb 100644
--- a/go/ssa/stdlib_test.go
+++ b/go/ssa/stdlib_test.go
@@ -5,7 +5,6 @@
// Incomplete source tree on Android.
//go:build !android
-// +build !android
package ssa_test
diff --git a/go/ssa/subst.go b/go/ssa/subst.go
index bbe5796d703..b4ea16854ea 100644
--- a/go/ssa/subst.go
+++ b/go/ssa/subst.go
@@ -266,7 +266,7 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface {
var methods []*types.Func
initMethods := func(n int) { // copy first n explicit methods
methods = make([]*types.Func, iface.NumExplicitMethods())
- for i := 0; i < n; i++ {
+ for i := range n {
f := iface.ExplicitMethod(i)
norecv := changeRecv(f.Type().(*types.Signature), nil)
methods[i] = types.NewFunc(f.Pos(), f.Pkg(), f.Name(), norecv)
@@ -290,7 +290,7 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface {
var embeds []types.Type
initEmbeds := func(n int) { // copy first n embedded types
embeds = make([]types.Type, iface.NumEmbeddeds())
- for i := 0; i < n; i++ {
+ for i := range n {
embeds[i] = iface.EmbeddedType(i)
}
}
diff --git a/go/ssa/util.go b/go/ssa/util.go
index 4a056cbe0bd..e53b31ff3bb 100644
--- a/go/ssa/util.go
+++ b/go/ssa/util.go
@@ -35,8 +35,6 @@ func assert(p bool, msg string) {
//// AST utilities
-func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) }
-
// isBlankIdent returns true iff e is an Ident with name "_".
// They have no associated types.Object, and thus no type.
func isBlankIdent(e ast.Expr) bool {
@@ -168,7 +166,7 @@ func declaredWithin(obj types.Object, fn *types.Func) bool {
// returns a closure that prints the corresponding "end" message.
// Call using 'defer logStack(...)()' to show builder stack on panic.
// Don't forget trailing parens!
-func logStack(format string, args ...interface{}) func() {
+func logStack(format string, args ...any) func() {
msg := fmt.Sprintf(format, args...)
io.WriteString(os.Stderr, msg)
io.WriteString(os.Stderr, "\n")
@@ -195,7 +193,7 @@ func makeLen(T types.Type) *Builtin {
lenParams := types.NewTuple(anonVar(T))
return &Builtin{
name: "len",
- sig: types.NewSignature(nil, lenParams, lenResults, false),
+ sig: types.NewSignatureType(nil, nil, nil, lenParams, lenResults, false),
}
}
@@ -387,7 +385,7 @@ func (m *typeListMap) hash(ts []types.Type) uint32 {
// Some smallish prime far away from typeutil.Hash.
n := len(ts)
h := uint32(13619) + 2*uint32(n)
- for i := 0; i < n; i++ {
+ for i := range n {
h += 3 * m.hasher.Hash(ts[i])
}
return h
diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go
index d09b4f250ee..aeb160eff23 100644
--- a/go/ssa/wrappers.go
+++ b/go/ssa/wrappers.go
@@ -106,9 +106,7 @@ func (b *builder) buildWrapper(fn *Function) {
var c Call
c.Call.Value = &Builtin{
name: "ssa:wrapnilchk",
- sig: types.NewSignature(nil,
- types.NewTuple(anonVar(fn.method.recv), anonVar(tString), anonVar(tString)),
- types.NewTuple(anonVar(fn.method.recv)), false),
+ sig: types.NewSignatureType(nil, nil, nil, types.NewTuple(anonVar(fn.method.recv), anonVar(tString), anonVar(tString)), types.NewTuple(anonVar(fn.method.recv)), false),
}
c.Call.Args = []Value{
v,
@@ -262,7 +260,7 @@ func createThunk(prog *Program, sel *selection) *Function {
}
func changeRecv(s *types.Signature, recv *types.Var) *types.Signature {
- return types.NewSignature(recv, s.Params(), s.Results(), s.Variadic())
+ return types.NewSignatureType(recv, nil, nil, s.Params(), s.Results(), s.Variadic())
}
// A local version of *types.Selection.
diff --git a/go/types/internal/play/gotypesalias.go b/go/types/internal/play/gotypesalias.go
deleted file mode 100644
index 288c10c2d0a..00000000000
--- a/go/types/internal/play/gotypesalias.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-//go:build go1.23
-
-//go:debug gotypesalias=1
-
-package main
-
-// Materialize aliases whenever the go toolchain version is after 1.23 (#69772).
-// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1).
diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go
index eb9e5794b94..77a90502135 100644
--- a/go/types/internal/play/play.go
+++ b/go/types/internal/play/play.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+//go:build go1.23
+
// The play program is a playground for go/types: a simple web-based
// text editor into which the user can enter a Go program, select a
// region, and see type information about it.
@@ -24,18 +26,20 @@ import (
"os"
"path/filepath"
"reflect"
+ "slices"
"strconv"
"strings"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/typeparams"
)
// TODO(adonovan):
// - show line numbers next to textarea.
-// - show a (tree) breakdown of the representation of the expression's type.
// - mention this in the go/types tutorial.
// - display versions of go/types and go command.
@@ -160,6 +164,16 @@ func handleSelectJSON(w http.ResponseWriter, req *http.Request) {
innermostExpr = e
}
}
+ // Show the cursor stack too.
+ // It's usually the same, but may differ in edge
+ // cases (e.g. around FuncType.Func).
+ inspect := inspector.New([]*ast.File{file})
+ if cur, ok := cursor.Root(inspect).FindByPos(startPos, endPos); ok {
+ fmt.Fprintf(out, "Cursor.FindPos().Enclosing() = %v\n",
+ slices.Collect(cur.Enclosing()))
+ } else {
+ fmt.Fprintf(out, "Cursor.FindPos() failed\n")
+ }
fmt.Fprintf(out, "\n")
// Expression type information
@@ -297,6 +311,10 @@ func formatObj(out *strings.Builder, fset *token.FileSet, ref string, obj types.
}
fmt.Fprintf(out, "\n\n")
+ fmt.Fprintf(out, "Type:\n")
+ describeType(out, obj.Type())
+ fmt.Fprintf(out, "\n")
+
// method set
if methods := typeutil.IntuitiveMethodSet(obj.Type(), nil); len(methods) > 0 {
fmt.Fprintf(out, "Methods:\n")
@@ -318,6 +336,65 @@ func formatObj(out *strings.Builder, fset *token.FileSet, ref string, obj types.
}
}
+// describeType formats t to out in a way that makes it clear what methods to call on t to
+// get at its parts.
+// describeType assumes t was constructed by the type checker, so it doesn't check
+// for recursion. The type checker replaces recursive alias types, which are illegal,
+// with a BasicType that says as much. Other types that it constructs are recursive
+// only via a name, and this function does not traverse names.
+func describeType(out *strings.Builder, t types.Type) {
+ depth := -1
+
+ var ft func(string, types.Type)
+ ft = func(prefix string, t types.Type) {
+ depth++
+ defer func() { depth-- }()
+
+ for range depth {
+ fmt.Fprint(out, ". ")
+ }
+
+ fmt.Fprintf(out, "%s%T:", prefix, t)
+ switch t := t.(type) {
+ case *types.Basic:
+ fmt.Fprintf(out, " Name: %q\n", t.Name())
+ case *types.Pointer:
+ fmt.Fprintln(out)
+ ft("Elem: ", t.Elem())
+ case *types.Slice:
+ fmt.Fprintln(out)
+ ft("Elem: ", t.Elem())
+ case *types.Array:
+ fmt.Fprintf(out, " Len: %d\n", t.Len())
+ ft("Elem: ", t.Elem())
+ case *types.Map:
+ fmt.Fprintln(out)
+ ft("Key: ", t.Key())
+ ft("Elem: ", t.Elem())
+ case *types.Chan:
+ fmt.Fprintf(out, " Dir: %s\n", chanDirs[t.Dir()])
+ ft("Elem: ", t.Elem())
+ case *types.Alias:
+ fmt.Fprintf(out, " Name: %q\n", t.Obj().Name())
+ ft("Rhs: ", t.Rhs())
+ default:
+ // For types we may have missed or which have too much to bother with,
+ // print their string representation.
+ // TODO(jba): print more about struct types (their fields) and interface and named
+ // types (their methods).
+ fmt.Fprintf(out, " %s\n", t)
+ }
+ }
+
+ ft("", t)
+}
+
+var chanDirs = []string{
+ "SendRecv",
+ "SendOnly",
+ "RecvOnly",
+}
+
func handleRoot(w http.ResponseWriter, req *http.Request) { io.WriteString(w, mainHTML) }
func handleJS(w http.ResponseWriter, req *http.Request) { io.WriteString(w, mainJS) }
func handleCSS(w http.ResponseWriter, req *http.Request) { io.WriteString(w, mainCSS) }
@@ -366,12 +443,3 @@ textarea { width: 6in; }
body { color: gray; }
div#out { font-family: monospace; font-size: 80%; }
`
-
-// TODO(adonovan): use go1.21 built-in.
-func min(x, y int) int {
- if x < y {
- return x
- } else {
- return y
- }
-}
diff --git a/go/types/objectpath/objectpath_test.go b/go/types/objectpath/objectpath_test.go
index 0805c9d919a..642d6da4926 100644
--- a/go/types/objectpath/objectpath_test.go
+++ b/go/types/objectpath/objectpath_test.go
@@ -308,7 +308,7 @@ func (unreachable) F() {} // not reachable in export data
if err != nil {
t.Fatal(err)
}
- conf := types.Config{Importer: importer.For("source", nil)}
+ conf := types.Config{Importer: importer.ForCompiler(token.NewFileSet(), "source", nil)}
info := &types.Info{
Defs: make(map[*ast.Ident]types.Object),
}
diff --git a/go/types/typeutil/callee.go b/go/types/typeutil/callee.go
index 754380351e8..5f10f56cbaf 100644
--- a/go/types/typeutil/callee.go
+++ b/go/types/typeutil/callee.go
@@ -7,45 +7,23 @@ package typeutil
import (
"go/ast"
"go/types"
-
- "golang.org/x/tools/internal/typeparams"
+ _ "unsafe" // for linkname
)
// Callee returns the named target of a function call, if any:
// a function, method, builtin, or variable.
//
// Functions and methods may potentially have type parameters.
+//
+// Note: for calls of instantiated functions and methods, Callee returns
+// the corresponding generic function or method on the generic type.
func Callee(info *types.Info, call *ast.CallExpr) types.Object {
- fun := ast.Unparen(call.Fun)
-
- // Look through type instantiation if necessary.
- isInstance := false
- switch fun.(type) {
- case *ast.IndexExpr, *ast.IndexListExpr:
- // When extracting the callee from an *IndexExpr, we need to check that
- // it is a *types.Func and not a *types.Var.
- // Example: Don't match a slice m within the expression `m[0]()`.
- isInstance = true
- fun, _, _, _ = typeparams.UnpackIndexExpr(fun)
- }
-
- var obj types.Object
- switch fun := fun.(type) {
- case *ast.Ident:
- obj = info.Uses[fun] // type, var, builtin, or declared func
- case *ast.SelectorExpr:
- if sel, ok := info.Selections[fun]; ok {
- obj = sel.Obj() // method or field
- } else {
- obj = info.Uses[fun.Sel] // qualified identifier?
- }
+ obj := info.Uses[usedIdent(info, call.Fun)]
+ if obj == nil {
+ return nil
}
if _, ok := obj.(*types.TypeName); ok {
- return nil // T(x) is a conversion, not a call
- }
- // A Func is required to match instantiations.
- if _, ok := obj.(*types.Func); isInstance && !ok {
- return nil // Was not a Func.
+ return nil
}
return obj
}
@@ -56,13 +34,52 @@ func Callee(info *types.Info, call *ast.CallExpr) types.Object {
// Note: for calls of instantiated functions and methods, StaticCallee returns
// the corresponding generic function or method on the generic type.
func StaticCallee(info *types.Info, call *ast.CallExpr) *types.Func {
- if f, ok := Callee(info, call).(*types.Func); ok && !interfaceMethod(f) {
- return f
+ obj := info.Uses[usedIdent(info, call.Fun)]
+ fn, _ := obj.(*types.Func)
+ if fn == nil || interfaceMethod(fn) {
+ return nil
+ }
+ return fn
+}
+
+// usedIdent is the implementation of [internal/typesinternal.UsedIdent].
+// It returns the identifier associated with e.
+// See typesinternal.UsedIdent for a fuller description.
+// This function should live in typesinternal, but cannot because it would
+// create an import cycle.
+//
+//go:linkname usedIdent golang.org/x/tools/go/types/typeutil.usedIdent
+func usedIdent(info *types.Info, e ast.Expr) *ast.Ident {
+ if info.Types == nil || info.Uses == nil {
+ panic("one of info.Types or info.Uses is nil; both must be populated")
+ }
+ // Look through type instantiation if necessary.
+ switch d := ast.Unparen(e).(type) {
+ case *ast.IndexExpr:
+ if info.Types[d.Index].IsType() {
+ e = d.X
+ }
+ case *ast.IndexListExpr:
+ e = d.X
+ }
+
+ switch e := ast.Unparen(e).(type) {
+ // info.Uses always has the object we want, even for selector expressions.
+ // We don't need info.Selections.
+ // See go/types/recording.go:recordSelection.
+ case *ast.Ident:
+ return e
+ case *ast.SelectorExpr:
+ return e.Sel
}
return nil
}
+// interfaceMethod reports whether its argument is a method of an interface.
+// This function should live in typesinternal, but cannot because it would create an import cycle.
+//
+//go:linkname interfaceMethod golang.org/x/tools/go/types/typeutil.interfaceMethod
func interfaceMethod(f *types.Func) bool {
- recv := f.Type().(*types.Signature).Recv()
+ recv := f.Signature().Recv()
return recv != nil && types.IsInterface(recv.Type())
}
diff --git a/go/types/typeutil/callee_test.go b/go/types/typeutil/callee_test.go
index 1d48bc743a9..3f96533ffff 100644
--- a/go/types/typeutil/callee_test.go
+++ b/go/types/typeutil/callee_test.go
@@ -122,6 +122,7 @@ func testStaticCallee(t *testing.T, contents []string) {
cfg := &types.Config{Importer: closure(packages)}
info := &types.Info{
Instances: make(map[*ast.Ident]types.Instance),
+ Types: make(map[ast.Expr]types.TypeAndValue),
Uses: make(map[*ast.Ident]types.Object),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
FileVersions: make(map[*ast.File]string),
diff --git a/go/types/typeutil/map.go b/go/types/typeutil/map.go
index 43261147c05..b6d542c64ee 100644
--- a/go/types/typeutil/map.go
+++ b/go/types/typeutil/map.go
@@ -389,8 +389,13 @@ func (hasher) hashTypeName(tname *types.TypeName) uint32 {
// path, and whether or not it is a package-level typename. It
// is rare for a package to define multiple local types with
// the same name.)
- hash := uintptr(unsafe.Pointer(tname))
- return uint32(hash ^ (hash >> 32))
+ ptr := uintptr(unsafe.Pointer(tname))
+ if unsafe.Sizeof(ptr) == 8 {
+ hash := uint64(ptr)
+ return uint32(hash ^ (hash >> 32))
+ } else {
+ return uint32(ptr)
+ }
}
// shallowHash computes a hash of t without looking at any of its
diff --git a/godoc/godoc17_test.go b/godoc/godoc17_test.go
index 82e23e64775..c8bf2d96d42 100644
--- a/godoc/godoc17_test.go
+++ b/godoc/godoc17_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.7
-// +build go1.7
package godoc
diff --git a/godoc/index.go b/godoc/index.go
index 05a1a9441ee..853337715c1 100644
--- a/godoc/index.go
+++ b/godoc/index.go
@@ -65,6 +65,7 @@ import (
"golang.org/x/tools/godoc/util"
"golang.org/x/tools/godoc/vfs"
+ "maps"
)
// ----------------------------------------------------------------------------
@@ -862,9 +863,7 @@ func (x *Indexer) indexGoFile(dirname string, filename string, file *token.File,
dest = make(map[string]SpotKind)
x.exports[pkgPath] = dest
}
- for k, v := range x.curPkgExports {
- dest[k] = v
- }
+ maps.Copy(dest, x.curPkgExports)
}
}
@@ -1069,7 +1068,7 @@ func (c *Corpus) NewIndex() *Index {
// convert alist into a map of alternative spellings
alts := make(map[string]*AltWords)
- for i := 0; i < len(alist); i++ {
+ for i := range alist {
a := alist[i].(*AltWords)
alts[a.Canon] = a
}
diff --git a/godoc/snippet.go b/godoc/snippet.go
index 1750478606e..43c1899a093 100644
--- a/godoc/snippet.go
+++ b/godoc/snippet.go
@@ -14,6 +14,7 @@ import (
"fmt"
"go/ast"
"go/token"
+ "slices"
)
type Snippet struct {
@@ -41,10 +42,8 @@ func findSpec(list []ast.Spec, id *ast.Ident) ast.Spec {
return s
}
case *ast.ValueSpec:
- for _, n := range s.Names {
- if n == id {
- return s
- }
+ if slices.Contains(s.Names, id) {
+ return s
}
case *ast.TypeSpec:
if s.Name == id {
diff --git a/godoc/static/gen_test.go b/godoc/static/gen_test.go
index 1f1c62e0e9c..7b7668a558c 100644
--- a/godoc/static/gen_test.go
+++ b/godoc/static/gen_test.go
@@ -39,7 +39,7 @@ to see the differences.`)
// TestAppendQuote ensures that AppendQuote produces a valid literal.
func TestAppendQuote(t *testing.T) {
var in, out bytes.Buffer
- for r := rune(0); r < unicode.MaxRune; r++ {
+ for r := range unicode.MaxRune {
in.WriteRune(r)
}
appendQuote(&out, in.Bytes())
diff --git a/godoc/static/makestatic.go b/godoc/static/makestatic.go
index a8a652f8ed5..5a7337290ff 100644
--- a/godoc/static/makestatic.go
+++ b/godoc/static/makestatic.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build ignore
-// +build ignore
// Command makestatic writes the generated file buffer to "static.go".
// It is intended to be invoked via "go generate" (directive in "gen.go").
diff --git a/godoc/versions_test.go b/godoc/versions_test.go
index a021616ba11..7b822f69b51 100644
--- a/godoc/versions_test.go
+++ b/godoc/versions_test.go
@@ -6,6 +6,7 @@ package godoc
import (
"go/build"
+ "slices"
"testing"
"golang.org/x/tools/internal/testenv"
@@ -102,12 +103,7 @@ func TestParseVersionRow(t *testing.T) {
// hasTag checks whether a given release tag is contained in the current version
// of the go binary.
func hasTag(t string) bool {
- for _, v := range build.Default.ReleaseTags {
- if t == v {
- return true
- }
- }
- return false
+ return slices.Contains(build.Default.ReleaseTags, t)
}
func TestAPIVersion(t *testing.T) {
diff --git a/godoc/vfs/fs.go b/godoc/vfs/fs.go
index f12d653fef2..2bec5886052 100644
--- a/godoc/vfs/fs.go
+++ b/godoc/vfs/fs.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.16
-// +build go1.16
package vfs
diff --git a/godoc/vfs/os.go b/godoc/vfs/os.go
index 35d050946e6..fe21a58662e 100644
--- a/godoc/vfs/os.go
+++ b/godoc/vfs/os.go
@@ -12,6 +12,7 @@ import (
pathpkg "path"
"path/filepath"
"runtime"
+ "slices"
)
// We expose a new variable because otherwise we need to copy the findGOROOT logic again
@@ -45,10 +46,8 @@ type osFS struct {
func isGoPath(path string) bool {
for _, bp := range filepath.SplitList(build.Default.GOPATH) {
- for _, gp := range filepath.SplitList(path) {
- if bp == gp {
- return true
- }
+ if slices.Contains(filepath.SplitList(path), bp) {
+ return true
}
}
return false
diff --git a/godoc/vfs/zipfs/zipfs_test.go b/godoc/vfs/zipfs/zipfs_test.go
index b6f2431b0b5..3e5a8034a5b 100644
--- a/godoc/vfs/zipfs/zipfs_test.go
+++ b/godoc/vfs/zipfs/zipfs_test.go
@@ -59,7 +59,7 @@ func TestMain(t *testing.M) {
os.Exit(t.Run())
}
-// setups state each of the tests uses
+// setup state each of the tests uses
func setup() error {
// create zipfs
b := new(bytes.Buffer)
@@ -172,7 +172,7 @@ func TestZipFSOpenSeek(t *testing.T) {
defer f.Close()
// test Seek() multiple times
- for i := 0; i < 3; i++ {
+ for range 3 {
all, err := io.ReadAll(f)
if err != nil {
t.Error(err)
diff --git a/gopls/README.md b/gopls/README.md
index 6602e0c27a7..e17184e0d51 100644
--- a/gopls/README.md
+++ b/gopls/README.md
@@ -20,8 +20,10 @@ supported in each client editor.
To get started with `gopls`, install an LSP plugin in your editor of choice.
+
* [VS Code](https://github.com/golang/vscode-go/blob/master/README.md)
* [Vim / Neovim](doc/vim.md)
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index 68465f9809d..e18a7c7efda 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -22,15 +22,3207 @@ which aggregates analyzers from a variety of sources:
To enable or disable analyzers, use the [analyses](settings.md#analyses) setting.
-In addition, gopls includes the [`staticcheck` suite](https://staticcheck.dev/docs/checks),
-though these analyzers are off by default.
-Use the [`staticcheck`](settings.md#staticcheck`) setting to enable them,
-and consult staticcheck's documentation for analyzer details.
+In addition, gopls includes the [`staticcheck` suite](https://staticcheck.dev/docs/checks).
+When the [`staticcheck`](settings.md#staticcheck`) boolean option is
+unset, slightly more than half of these analyzers are enabled by
+default; this subset has been chosen for precision and efficiency. Set
+`staticcheck` to `true` to enable the complete set, or to `false` to
+disable the complete set.
-
+Staticcheck analyzers, like all other analyzers, can be explicitly
+enabled or disabled using the `analyzers` configuration setting; this
+setting takes precedence over the `staticcheck` setting, so,
+regardless of what value of `staticcheck` you use (true/false/unset),
+you can make adjustments to your preferred set of analyzers.
+
+## `QF1001`: Apply De Morgan's law
+
+
+Available since
+ 2021.1
+
+
+Default: off. Enable by setting `"analyses": {"QF1001": true}`.
+
+Package documentation: [QF1001](https://staticcheck.dev/docs/checks/#QF1001)
+
+
+## `QF1002`: Convert untagged switch to tagged switch
+
+
+An untagged switch that compares a single variable against a series of
+values can be replaced with a tagged switch.
+
+Before:
+
+ switch {
+ case x == 1 || x == 2, x == 3:
+ ...
+ case x == 4:
+ ...
+ default:
+ ...
+ }
+
+After:
+
+ switch x {
+ case 1, 2, 3:
+ ...
+ case 4:
+ ...
+ default:
+ ...
+ }
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [QF1002](https://staticcheck.dev/docs/checks/#QF1002)
+
+
+## `QF1003`: Convert if/else-if chain to tagged switch
+
+
+A series of if/else-if checks comparing the same variable against
+values can be replaced with a tagged switch.
+
+Before:
+
+ if x == 1 || x == 2 {
+ ...
+ } else if x == 3 {
+ ...
+ } else {
+ ...
+ }
+
+After:
+
+ switch x {
+ case 1, 2:
+ ...
+ case 3:
+ ...
+ default:
+ ...
+ }
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [QF1003](https://staticcheck.dev/docs/checks/#QF1003)
+
+
+## `QF1004`: Use strings.ReplaceAll instead of strings.Replace with n == -1
+
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [QF1004](https://staticcheck.dev/docs/checks/#QF1004)
+
+
+## `QF1005`: Expand call to math.Pow
+
+
+Some uses of math.Pow can be simplified to basic multiplication.
+
+Before:
+
+ math.Pow(x, 2)
+
+After:
+
+ x * x
+
+Available since
+ 2021.1
+
+
+Default: off. Enable by setting `"analyses": {"QF1005": true}`.
+
+Package documentation: [QF1005](https://staticcheck.dev/docs/checks/#QF1005)
+
+
+## `QF1006`: Lift if+break into loop condition
+
+
+Before:
+
+ for {
+ if done {
+ break
+ }
+ ...
+ }
+
+After:
+
+ for !done {
+ ...
+ }
+
+Available since
+ 2021.1
+
+
+Default: off. Enable by setting `"analyses": {"QF1006": true}`.
+
+Package documentation: [QF1006](https://staticcheck.dev/docs/checks/#QF1006)
+
+
+## `QF1007`: Merge conditional assignment into variable declaration
+
+
+Before:
+
+ x := false
+ if someCondition {
+ x = true
+ }
+
+After:
+
+ x := someCondition
+
+Available since
+ 2021.1
+
+
+Default: off. Enable by setting `"analyses": {"QF1007": true}`.
+
+Package documentation: [QF1007](https://staticcheck.dev/docs/checks/#QF1007)
+
+
+## `QF1008`: Omit embedded fields from selector expression
+
+
+Available since
+ 2021.1
+
+
+Default: off. Enable by setting `"analyses": {"QF1008": true}`.
+
+Package documentation: [QF1008](https://staticcheck.dev/docs/checks/#QF1008)
+
+
+## `QF1009`: Use time.Time.Equal instead of == operator
+
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [QF1009](https://staticcheck.dev/docs/checks/#QF1009)
+
+
+## `QF1010`: Convert slice of bytes to string when printing it
+
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [QF1010](https://staticcheck.dev/docs/checks/#QF1010)
+
+
+## `QF1011`: Omit redundant type from variable declaration
+
+
+Available since
+ 2021.1
+
+
+Default: off. Enable by setting `"analyses": {"QF1011": true}`.
+
+Package documentation: [QF1011](https://staticcheck.dev/docs/checks/#)
+
+
+## `QF1012`: Use fmt.Fprintf(x, ...) instead of x.Write(fmt.Sprintf(...))
+
+
+Available since
+ 2022.1
+
+
+Default: on.
+
+Package documentation: [QF1012](https://staticcheck.dev/docs/checks/#QF1012)
+
+
+## `S1000`: Use plain channel send or receive instead of single-case select
+
+
+Select statements with a single case can be replaced with a simple
+send or receive.
+
+Before:
+
+ select {
+ case x := <-ch:
+ fmt.Println(x)
+ }
+
+After:
+
+ x := <-ch
+ fmt.Println(x)
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1000](https://staticcheck.dev/docs/checks/#S1000)
+
+
+## `S1001`: Replace for loop with call to copy
+
+
+Use copy() for copying elements from one slice to another. For
+arrays of identical size, you can use simple assignment.
+
+Before:
+
+ for i, x := range src {
+ dst[i] = x
+ }
+
+After:
+
+ copy(dst, src)
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1001](https://staticcheck.dev/docs/checks/#S1001)
+
+
+## `S1002`: Omit comparison with boolean constant
+
+
+Before:
+
+ if x == true {}
+
+After:
+
+ if x {}
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1002": true}`.
+
+Package documentation: [S1002](https://staticcheck.dev/docs/checks/#S1002)
+
+
+## `S1003`: Replace call to strings.Index with strings.Contains
+
+
+Before:
+
+ if strings.Index(x, y) != -1 {}
+
+After:
+
+ if strings.Contains(x, y) {}
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1003](https://staticcheck.dev/docs/checks/#S1003)
+
+
+## `S1004`: Replace call to bytes.Compare with bytes.Equal
+
+
+Before:
+
+ if bytes.Compare(x, y) == 0 {}
+
+After:
+
+ if bytes.Equal(x, y) {}
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1004](https://staticcheck.dev/docs/checks/#S1004)
+
+
+## `S1005`: Drop unnecessary use of the blank identifier
+
+
+In many cases, assigning to the blank identifier is unnecessary.
+
+Before:
+
+ for _ = range s {}
+ x, _ = someMap[key]
+ _ = <-ch
+
+After:
+
+ for range s{}
+ x = someMap[key]
+ <-ch
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1005": true}`.
+
+Package documentation: [S1005](https://staticcheck.dev/docs/checks/#S1005)
+
+
+## `S1006`: Use 'for { ... }' for infinite loops
+
+
+For infinite loops, using for { ... } is the most idiomatic choice.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1006": true}`.
+
+Package documentation: [S1006](https://staticcheck.dev/docs/checks/#S1006)
+
+
+## `S1007`: Simplify regular expression by using raw string literal
+
+
+Raw string literals use backticks instead of quotation marks and do not support
+any escape sequences. This means that the backslash can be used
+freely, without the need of escaping.
+
+Since regular expressions have their own escape sequences, raw strings
+can improve their readability.
+
+Before:
+
+ regexp.Compile("\\A(\\w+) profile: total \\d+\\n\\z")
+
+After:
+
+ regexp.Compile(`\A(\w+) profile: total \d+\n\z`)
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1007](https://staticcheck.dev/docs/checks/#S1007)
+
+
+## `S1008`: Simplify returning boolean expression
+
+
+Before:
+
+ if {
+ return true
+ }
+ return false
+
+After:
+
+ return
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1008": true}`.
+
+Package documentation: [S1008](https://staticcheck.dev/docs/checks/#S1008)
+
+
+## `S1009`: Omit redundant nil check on slices, maps, and channels
+
+
+The len function is defined for all slices, maps, and
+channels, even nil ones, which have a length of zero. It is not necessary to
+check for nil before checking that their length is not zero.
+
+Before:
+
+ if x != nil && len(x) != 0 {}
+
+After:
+
+ if len(x) != 0 {}
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1009](https://staticcheck.dev/docs/checks/#S1009)
+
+
+## `S1010`: Omit default slice index
+
+
+When slicing, the second index defaults to the length of the value,
+making s[n:len(s)] and s[n:] equivalent.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1010](https://staticcheck.dev/docs/checks/#S1010)
+
+
+## `S1011`: Use a single append to concatenate two slices
+
+
+Before:
+
+ for _, e := range y {
+ x = append(x, e)
+ }
+
+ for i := range y {
+ x = append(x, y[i])
+ }
+
+ for i := range y {
+ v := y[i]
+ x = append(x, v)
+ }
+
+After:
+
+ x = append(x, y...)
+ x = append(x, y...)
+ x = append(x, y...)
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1011": true}`.
+
+Package documentation: [S1011](https://staticcheck.dev/docs/checks/#S1011)
+
+
+## `S1012`: Replace time.Now().Sub(x) with time.Since(x)
+
+
+The time.Since helper has the same effect as using time.Now().Sub(x)
+but is easier to read.
+
+Before:
+
+ time.Now().Sub(x)
+
+After:
+
+ time.Since(x)
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1012](https://staticcheck.dev/docs/checks/#S1012)
+
+
+## `S1016`: Use a type conversion instead of manually copying struct fields
+
+
+Two struct types with identical fields can be converted between each
+other. In older versions of Go, the fields had to have identical
+struct tags. Since Go 1.8, however, struct tags are ignored during
+conversions. It is thus not necessary to manually copy every field
+individually.
+
+Before:
+
+ var x T1
+ y := T2{
+ Field1: x.Field1,
+ Field2: x.Field2,
+ }
+
+After:
+
+ var x T1
+ y := T2(x)
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1016": true}`.
+
+Package documentation: [S1016](https://staticcheck.dev/docs/checks/#S1016)
+
+
+## `S1017`: Replace manual trimming with strings.TrimPrefix
+
+
+Instead of using strings.HasPrefix and manual slicing, use the
+strings.TrimPrefix function. If the string doesn't start with the
+prefix, the original string will be returned. Using strings.TrimPrefix
+reduces complexity, and avoids common bugs, such as off-by-one
+mistakes.
+
+Before:
+
+ if strings.HasPrefix(str, prefix) {
+ str = str[len(prefix):]
+ }
+
+After:
+
+ str = strings.TrimPrefix(str, prefix)
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1017](https://staticcheck.dev/docs/checks/#S1017)
+
+
+## `S1018`: Use 'copy' for sliding elements
+
+
+copy() permits using the same source and destination slice, even with
+overlapping ranges. This makes it ideal for sliding elements in a
+slice.
+
+Before:
+
+ for i := 0; i < n; i++ {
+ bs[i] = bs[offset+i]
+ }
+
+After:
+
+ copy(bs[:n], bs[offset:])
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1018](https://staticcheck.dev/docs/checks/#S1018)
+
+
+## `S1019`: Simplify 'make' call by omitting redundant arguments
+
+
+The 'make' function has default values for the length and capacity
+arguments. For channels, the length defaults to zero, and for slices,
+the capacity defaults to the length.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1019](https://staticcheck.dev/docs/checks/#S1019)
+
+
+## `S1020`: Omit redundant nil check in type assertion
+
+
+Before:
+
+ if _, ok := i.(T); ok && i != nil {}
+
+After:
+
+ if _, ok := i.(T); ok {}
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1020](https://staticcheck.dev/docs/checks/#S1020)
+
+
+## `S1021`: Merge variable declaration and assignment
+
+
+Before:
+
+ var x uint
+ x = 1
+
+After:
+
+ var x uint = 1
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1021": true}`.
+
+Package documentation: [S1021](https://staticcheck.dev/docs/checks/#S1021)
+
+
+## `S1023`: Omit redundant control flow
+
+
+Functions that have no return value do not need a return statement as
+the final statement of the function.
+
+Switches in Go do not have automatic fallthrough, unlike languages
+like C. It is not necessary to have a break statement as the final
+statement in a case block.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1023](https://staticcheck.dev/docs/checks/#S1023)
+
+
+## `S1024`: Replace x.Sub(time.Now()) with time.Until(x)
+
+
+The time.Until helper has the same effect as using x.Sub(time.Now())
+but is easier to read.
+
+Before:
+
+ x.Sub(time.Now())
+
+After:
+
+ time.Until(x)
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1024](https://staticcheck.dev/docs/checks/#S1024)
+
+
+## `S1025`: Don't use fmt.Sprintf("%s", x) unnecessarily
+
+
+In many instances, there are easier and more efficient ways of getting
+a value's string representation. Whenever a value's underlying type is
+a string already, or the type has a String method, they should be used
+directly.
+
+Given the following shared definitions
+
+ type T1 string
+ type T2 int
+
+ func (T2) String() string { return "Hello, world" }
+
+ var x string
+ var y T1
+ var z T2
+
+we can simplify
+
+ fmt.Sprintf("%s", x)
+ fmt.Sprintf("%s", y)
+ fmt.Sprintf("%s", z)
+
+to
+
+ x
+ string(y)
+ z.String()
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1025": true}`.
+
+Package documentation: [S1025](https://staticcheck.dev/docs/checks/#S1025)
+
+
+## `S1028`: Simplify error construction with fmt.Errorf
+
+
+Before:
+
+ errors.New(fmt.Sprintf(...))
+
+After:
+
+ fmt.Errorf(...)
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1028](https://staticcheck.dev/docs/checks/#S1028)
+
+
+## `S1029`: Range over the string directly
+
+
+Ranging over a string will yield byte offsets and runes. If the offset
+isn't used, this is functionally equivalent to converting the string
+to a slice of runes and ranging over that. Ranging directly over the
+string will be more performant, however, as it avoids allocating a new
+slice, the size of which depends on the length of the string.
+
+Before:
+
+ for _, r := range []rune(s) {}
+
+After:
+
+ for _, r := range s {}
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"S1029": true}`.
+
+Package documentation: [S1029](https://staticcheck.dev/docs/checks/#S1029)
+
+
+## `S1030`: Use bytes.Buffer.String or bytes.Buffer.Bytes
+
+
+bytes.Buffer has both a String and a Bytes method. It is almost never
+necessary to use string(buf.Bytes()) or []byte(buf.String()) – simply
+use the other method.
+
+The only exception to this are map lookups. Due to a compiler optimization,
+m[string(buf.Bytes())] is more efficient than m[buf.String()].
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1030](https://staticcheck.dev/docs/checks/#S1030)
+
+
+## `S1031`: Omit redundant nil check around loop
+
+
+You can use range on nil slices and maps, the loop will simply never
+execute. This makes an additional nil check around the loop
+unnecessary.
+
+Before:
+
+ if s != nil {
+ for _, x := range s {
+ ...
+ }
+ }
+
+After:
+
+ for _, x := range s {
+ ...
+ }
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [S1031](https://staticcheck.dev/docs/checks/#S1031)
+
+
+## `S1032`: Use sort.Ints(x), sort.Float64s(x), and sort.Strings(x)
+
+
+The sort.Ints, sort.Float64s and sort.Strings functions are easier to
+read than sort.Sort(sort.IntSlice(x)), sort.Sort(sort.Float64Slice(x))
+and sort.Sort(sort.StringSlice(x)).
+
+Before:
+
+ sort.Sort(sort.StringSlice(x))
+
+After:
+
+ sort.Strings(x)
+
+Available since
+ 2019.1
+
+
+Default: on.
+
+Package documentation: [S1032](https://staticcheck.dev/docs/checks/#S1032)
+
+
+## `S1033`: Unnecessary guard around call to 'delete'
+
+
+Calling delete on a nil map is a no-op.
+
+Available since
+ 2019.2
+
+
+Default: on.
+
+Package documentation: [S1033](https://staticcheck.dev/docs/checks/#S1033)
+
+
+## `S1034`: Use result of type assertion to simplify cases
+
+
+Available since
+ 2019.2
+
+
+Default: on.
+
+Package documentation: [S1034](https://staticcheck.dev/docs/checks/#S1034)
+
+
+## `S1035`: Redundant call to net/http.CanonicalHeaderKey in method call on net/http.Header
+
+
+The methods on net/http.Header, namely Add, Del, Get
+and Set, already canonicalize the given header name.
+
+Available since
+ 2020.1
+
+
+Default: on.
+
+Package documentation: [S1035](https://staticcheck.dev/docs/checks/#S1035)
+
+
+## `S1036`: Unnecessary guard around map access
+
+
+When accessing a map key that doesn't exist yet, one receives a zero
+value. Often, the zero value is a suitable value, for example when
+using append or doing integer math.
+
+The following
+
+ if _, ok := m["foo"]; ok {
+ m["foo"] = append(m["foo"], "bar")
+ } else {
+ m["foo"] = []string{"bar"}
+ }
+
+can be simplified to
+
+ m["foo"] = append(m["foo"], "bar")
+
+and
+
+ if _, ok := m2["k"]; ok {
+ m2["k"] += 4
+ } else {
+ m2["k"] = 4
+ }
+
+can be simplified to
+
+ m["k"] += 4
+
+Available since
+ 2020.1
+
+
+Default: on.
+
+Package documentation: [S1036](https://staticcheck.dev/docs/checks/#S1036)
+
+
+## `S1037`: Elaborate way of sleeping
+
+
+Using a select statement with a single case receiving
+from the result of time.After is a very elaborate way of sleeping that
+can much simpler be expressed with a simple call to time.Sleep.
+
+Available since
+ 2020.1
+
+
+Default: on.
+
+Package documentation: [S1037](https://staticcheck.dev/docs/checks/#S1037)
+
+
+## `S1038`: Unnecessarily complex way of printing formatted string
+
+
+Instead of using fmt.Print(fmt.Sprintf(...)), one can use fmt.Printf(...).
+
+Available since
+ 2020.1
+
+
+Default: on.
+
+Package documentation: [S1038](https://staticcheck.dev/docs/checks/#S1038)
+
+
+## `S1039`: Unnecessary use of fmt.Sprint
+
+
+Calling fmt.Sprint with a single string argument is unnecessary
+and identical to using the string directly.
+
+Available since
+ 2020.1
+
+
+Default: on.
+
+Package documentation: [S1039](https://staticcheck.dev/docs/checks/#S1039)
+
+
+## `S1040`: Type assertion to current type
+
+
+The type assertion x.(SomeInterface), when x already has type
+SomeInterface, can only fail if x is nil. Usually, this is
+left-over code from when x had a different type and you can safely
+delete the type assertion. If you want to check that x is not nil,
+consider being explicit and using an actual if x == nil comparison
+instead of relying on the type assertion panicking.
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [S1040](https://staticcheck.dev/docs/checks/#S1040)
+
+
+## `SA1000`: Invalid regular expression
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1000": true}`.
+
+Package documentation: [SA1000](https://staticcheck.dev/docs/checks/#SA1000)
+
+
+## `SA1001`: Invalid template
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA1001](https://staticcheck.dev/docs/checks/#SA1001)
+
+
+## `SA1002`: Invalid format in time.Parse
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1002": true}`.
+
+Package documentation: [SA1002](https://staticcheck.dev/docs/checks/#SA1002)
+
+
+## `SA1003`: Unsupported argument to functions in encoding/binary
+
+
+The encoding/binary package can only serialize types with known sizes.
+This precludes the use of the int and uint types, as their sizes
+differ on different architectures. Furthermore, it doesn't support
+serializing maps, channels, strings, or functions.
+
+Before Go 1.8, bool wasn't supported, either.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1003": true}`.
+
+Package documentation: [SA1003](https://staticcheck.dev/docs/checks/#SA1003)
+
+
+## `SA1004`: Suspiciously small untyped constant in time.Sleep
+
+
+The time.Sleep function takes a time.Duration as its only argument.
+Durations are expressed in nanoseconds. Thus, calling time.Sleep(1)
+will sleep for 1 nanosecond. This is a common source of bugs, as sleep
+functions in other languages often accept seconds or milliseconds.
+
+The time package provides constants such as time.Second to express
+large durations. These can be combined with arithmetic to express
+arbitrary durations, for example 5 * time.Second for 5 seconds.
+
+If you truly meant to sleep for a tiny amount of time, use
+n * time.Nanosecond to signal to Staticcheck that you did mean to sleep
+for some amount of nanoseconds.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA1004](https://staticcheck.dev/docs/checks/#SA1004)
+
+
+## `SA1005`: Invalid first argument to exec.Command
+
+
+os/exec runs programs directly (using variants of the fork and exec
+system calls on Unix systems). This shouldn't be confused with running
+a command in a shell. The shell will allow for features such as input
+redirection, pipes, and general scripting. The shell is also
+responsible for splitting the user's input into a program name and its
+arguments. For example, the equivalent to
+
+ ls / /tmp
+
+would be
+
+ exec.Command("ls", "/", "/tmp")
+
+If you want to run a command in a shell, consider using something like
+the following – but be aware that not all systems, particularly
+Windows, will have a /bin/sh program:
+
+ exec.Command("/bin/sh", "-c", "ls | grep Awesome")
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA1005](https://staticcheck.dev/docs/checks/#SA1005)
+
+
+## `SA1007`: Invalid URL in net/url.Parse
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1007": true}`.
+
+Package documentation: [SA1007](https://staticcheck.dev/docs/checks/#SA1007)
+
+
+## `SA1008`: Non-canonical key in http.Header map
+
+
+Keys in http.Header maps are canonical, meaning they follow a specific
+combination of uppercase and lowercase letters. Methods such as
+http.Header.Add and http.Header.Del convert inputs into this canonical
+form before manipulating the map.
+
+When manipulating http.Header maps directly, as opposed to using the
+provided methods, care should be taken to stick to canonical form in
+order to avoid inconsistencies. The following piece of code
+demonstrates one such inconsistency:
+
+ h := http.Header{}
+ h["etag"] = []string{"1234"}
+ h.Add("etag", "5678")
+ fmt.Println(h)
+
+ // Output:
+ // map[Etag:[5678] etag:[1234]]
+
+The easiest way of obtaining the canonical form of a key is to use
+http.CanonicalHeaderKey.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA1008](https://staticcheck.dev/docs/checks/#SA1008)
+
+
+## `SA1010`: (*regexp.Regexp).FindAll called with n == 0, which will always return zero results
+
+
+If n >= 0, the function returns at most n matches/submatches. To
+return all results, specify a negative number.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1010": true}`.
+
+Package documentation: [SA1010](https://staticcheck.dev/docs/checks/#SA1010)
+
+
+## `SA1011`: Various methods in the 'strings' package expect valid UTF-8, but invalid input is provided
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1011": true}`.
+
+Package documentation: [SA1011](https://staticcheck.dev/docs/checks/#SA1011)
+
+
+## `SA1012`: A nil context.Context is being passed to a function, consider using context.TODO instead
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA1012](https://staticcheck.dev/docs/checks/#SA1012)
+
+
+## `SA1013`: io.Seeker.Seek is being called with the whence constant as the first argument, but it should be the second
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA1013](https://staticcheck.dev/docs/checks/#SA1013)
+
+
+## `SA1014`: Non-pointer value passed to Unmarshal or Decode
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1014": true}`.
+
+Package documentation: [SA1014](https://staticcheck.dev/docs/checks/#SA1014)
+
+
+## `SA1015`: Using time.Tick in a way that will leak. Consider using time.NewTicker, and only use time.Tick in tests, commands and endless functions
+
+
+Before Go 1.23, time.Tickers had to be closed to be able to be garbage
+collected. Since time.Tick doesn't make it possible to close the underlying
+ticker, using it repeatedly would leak memory.
+
+Go 1.23 fixes this by allowing tickers to be collected even if they weren't closed.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1015": true}`.
+
+Package documentation: [SA1015](https://staticcheck.dev/docs/checks/#SA1015)
+
+
+## `SA1016`: Trapping a signal that cannot be trapped
+
+
+Not all signals can be intercepted by a process. Specifically, on
+UNIX-like systems, the syscall.SIGKILL and syscall.SIGSTOP signals are
+never passed to the process, but instead handled directly by the
+kernel. It is therefore pointless to try and handle these signals.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA1016](https://staticcheck.dev/docs/checks/#SA1016)
+
+
+## `SA1017`: Channels used with os/signal.Notify should be buffered
+
+
+The os/signal package uses non-blocking channel sends when delivering
+signals. If the receiving end of the channel isn't ready and the
+channel is either unbuffered or full, the signal will be dropped. To
+avoid missing signals, the channel should be buffered and of the
+appropriate size. For a channel used for notification of just one
+signal value, a buffer of size 1 is sufficient.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1017": true}`.
+
+Package documentation: [SA1017](https://staticcheck.dev/docs/checks/#SA1017)
+
+
+## `SA1018`: strings.Replace called with n == 0, which does nothing
+
+
+With n == 0, zero instances will be replaced. To replace all
+instances, use a negative number, or use strings.ReplaceAll.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1018": true}`.
+
+Package documentation: [SA1018](https://staticcheck.dev/docs/checks/#SA1018)
+
+
+## `SA1020`: Using an invalid host:port pair with a net.Listen-related function
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1020": true}`.
+
+Package documentation: [SA1020](https://staticcheck.dev/docs/checks/#SA1020)
+
+
+## `SA1021`: Using bytes.Equal to compare two net.IP
+
+
+A net.IP stores an IPv4 or IPv6 address as a slice of bytes. The
+length of the slice for an IPv4 address, however, can be either 4 or
+16 bytes long, using different ways of representing IPv4 addresses. In
+order to correctly compare two net.IPs, the net.IP.Equal method should
+be used, as it takes both representations into account.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1021": true}`.
+
+Package documentation: [SA1021](https://staticcheck.dev/docs/checks/#SA1021)
+
+
+## `SA1023`: Modifying the buffer in an io.Writer implementation
+
+
+Write must not modify the slice data, even temporarily.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1023": true}`.
+
+Package documentation: [SA1023](https://staticcheck.dev/docs/checks/#SA1023)
+
+
+## `SA1024`: A string cutset contains duplicate characters
+
+
+The strings.TrimLeft and strings.TrimRight functions take cutsets, not
+prefixes. A cutset is treated as a set of characters to remove from a
+string. For example,
+
+ strings.TrimLeft("42133word", "1234")
+
+will result in the string "word" – any characters that are 1, 2, 3 or
+4 are cut from the left of the string.
+
+In order to remove one string from another, use strings.TrimPrefix instead.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1024": true}`.
+
+Package documentation: [SA1024](https://staticcheck.dev/docs/checks/#SA1024)
+
+
+## `SA1025`: It is not possible to use (*time.Timer).Reset's return value correctly
+
+
+Available since
+ 2019.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1025": true}`.
+
+Package documentation: [SA1025](https://staticcheck.dev/docs/checks/#SA1025)
+
+
+## `SA1026`: Cannot marshal channels or functions
+
+
+Available since
+ 2019.2
+
+
+Default: off. Enable by setting `"analyses": {"SA1026": true}`.
+
+Package documentation: [SA1026](https://staticcheck.dev/docs/checks/#SA1026)
+
+
+## `SA1027`: Atomic access to 64-bit variable must be 64-bit aligned
+
+
+On ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to
+arrange for 64-bit alignment of 64-bit words accessed atomically. The
+first word in a variable or in an allocated struct, array, or slice
+can be relied upon to be 64-bit aligned.
+
+You can use the structlayout tool to inspect the alignment of fields
+in a struct.
+
+Available since
+ 2019.2
+
+
+Default: off. Enable by setting `"analyses": {"SA1027": true}`.
+
+Package documentation: [SA1027](https://staticcheck.dev/docs/checks/#SA1027)
+
+
+## `SA1028`: sort.Slice can only be used on slices
+
+
+The first argument of sort.Slice must be a slice.
+
+Available since
+ 2020.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1028": true}`.
+
+Package documentation: [SA1028](https://staticcheck.dev/docs/checks/#SA1028)
+
+
+## `SA1029`: Inappropriate key in call to context.WithValue
+
+
+The provided key must be comparable and should not be
+of type string or any other built-in type to avoid collisions between
+packages using context. Users of WithValue should define their own
+types for keys.
+
+To avoid allocating when assigning to an interface{},
+context keys often have concrete type struct{}. Alternatively,
+exported context key variables' static type should be a pointer or
+interface.
+
+Available since
+ 2020.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1029": true}`.
+
+Package documentation: [SA1029](https://staticcheck.dev/docs/checks/#SA1029)
+
+
+## `SA1030`: Invalid argument in call to a strconv function
+
+
+This check validates the format, number base and bit size arguments of
+the various parsing and formatting functions in strconv.
+
+Available since
+ 2021.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1030": true}`.
+
+Package documentation: [SA1030](https://staticcheck.dev/docs/checks/#SA1030)
+
+
+## `SA1031`: Overlapping byte slices passed to an encoder
+
+
+In an encoding function of the form Encode(dst, src), dst and
+src were found to reference the same memory. This can result in
+src bytes being overwritten before they are read, when the encoder
+writes more than one byte per src byte.
+
+Available since
+ 2024.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1031": true}`.
+
+Package documentation: [SA1031](https://staticcheck.dev/docs/checks/#SA1031)
+
+
+## `SA1032`: Wrong order of arguments to errors.Is
+
+
+The first argument of the function errors.Is is the error
+that we have and the second argument is the error we're trying to match against.
+For example:
+
+ if errors.Is(err, io.EOF) { ... }
+
+This check detects some cases where the two arguments have been swapped. It
+flags any calls where the first argument is referring to a package-level error
+variable, such as
+
+ if errors.Is(io.EOF, err) { /* this is wrong */ }
+
+Available since
+ 2024.1
+
+
+Default: off. Enable by setting `"analyses": {"SA1032": true}`.
+
+Package documentation: [SA1032](https://staticcheck.dev/docs/checks/#SA1032)
+
+
+## `SA2001`: Empty critical section, did you mean to defer the unlock?
+
+
+Empty critical sections of the kind
+
+ mu.Lock()
+ mu.Unlock()
+
+are very often a typo, and the following was intended instead:
+
+ mu.Lock()
+ defer mu.Unlock()
+
+Do note that sometimes empty critical sections can be useful, as a
+form of signaling to wait on another goroutine. Many times, there are
+simpler ways of achieving the same effect. When that isn't the case,
+the code should be amply commented to avoid confusion. Combining such
+comments with a //lint:ignore directive can be used to suppress this
+rare false positive.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA2001](https://staticcheck.dev/docs/checks/#SA2001)
+
+
+## `SA2002`: Called testing.T.FailNow or SkipNow in a goroutine, which isn't allowed
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA2002": true}`.
+
+Package documentation: [SA2002](https://staticcheck.dev/docs/checks/#SA2002)
+
+
+## `SA2003`: Deferred Lock right after locking, likely meant to defer Unlock instead
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA2003": true}`.
+
+Package documentation: [SA2003](https://staticcheck.dev/docs/checks/#SA2003)
+
+
+## `SA3000`: TestMain doesn't call os.Exit, hiding test failures
+
+
+Test executables (and in turn 'go test') exit with a non-zero status
+code if any tests failed. When specifying your own TestMain function,
+it is your responsibility to arrange for this, by calling os.Exit with
+the correct code. The correct code is returned by (*testing.M).Run, so
+the usual way of implementing TestMain is to end it with
+os.Exit(m.Run()).
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA3000](https://staticcheck.dev/docs/checks/#SA3000)
+
+
+## `SA3001`: Assigning to b.N in benchmarks distorts the results
+
+
+The testing package dynamically sets b.N to improve the reliability of
+benchmarks and uses it in computations to determine the duration of a
+single operation. Benchmark code must not alter b.N as this would
+falsify results.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA3001](https://staticcheck.dev/docs/checks/#SA3001)
+
+
+## `SA4000`: Binary operator has identical expressions on both sides
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4000](https://staticcheck.dev/docs/checks/#SA4000)
+
+
+## `SA4001`: &*x gets simplified to x, it does not copy x
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4001](https://staticcheck.dev/docs/checks/#SA4001)
+
+
+## `SA4003`: Comparing unsigned values against negative values is pointless
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4003](https://staticcheck.dev/docs/checks/#SA4003)
+
+
+## `SA4004`: The loop exits unconditionally after one iteration
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4004](https://staticcheck.dev/docs/checks/#SA4004)
+
+
+## `SA4005`: Field assignment that will never be observed. Did you mean to use a pointer receiver?
+
+
+Available since
+ 2021.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4005": true}`.
+
+Package documentation: [SA4005](https://staticcheck.dev/docs/checks/#SA4005)
+
+
+## `SA4006`: A value assigned to a variable is never read before being overwritten. Forgotten error check or dead code?
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4006": true}`.
+
+Package documentation: [SA4006](https://staticcheck.dev/docs/checks/#SA4006)
+
+
+## `SA4008`: The variable in the loop condition never changes, are you incrementing the wrong variable?
+
+
+For example:
+
+ for i := 0; i < 10; j++ { ... }
+
+This may also occur when a loop can only execute once because of unconditional
+control flow that terminates the loop. For example, when a loop body contains an
+unconditional break, return, or panic:
+
+ func f() {
+ panic("oops")
+ }
+ func g() {
+ for i := 0; i < 10; i++ {
+ // f unconditionally calls panic, which means "i" is
+ // never incremented.
+ f()
+ }
+ }
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4008": true}`.
+
+Package documentation: [SA4008](https://staticcheck.dev/docs/checks/#SA4008)
+
+
+## `SA4009`: A function argument is overwritten before its first use
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4009": true}`.
+
+Package documentation: [SA4009](https://staticcheck.dev/docs/checks/#SA4009)
+
+
+## `SA4010`: The result of append will never be observed anywhere
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4010": true}`.
+
+Package documentation: [SA4010](https://staticcheck.dev/docs/checks/#SA4010)
+
+
+## `SA4011`: Break statement with no effect. Did you mean to break out of an outer loop?
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4011](https://staticcheck.dev/docs/checks/#SA4011)
+
+
+## `SA4012`: Comparing a value against NaN even though no value is equal to NaN
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4012": true}`.
+
+Package documentation: [SA4012](https://staticcheck.dev/docs/checks/#SA4012)
+
+
+## `SA4013`: Negating a boolean twice (!!b) is the same as writing b. This is either redundant, or a typo.
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4013](https://staticcheck.dev/docs/checks/#SA4013)
+
+
+## `SA4014`: An if/else if chain has repeated conditions and no side-effects; if the condition didn't match the first time, it won't match the second time, either
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4014](https://staticcheck.dev/docs/checks/#SA4014)
+
+
+## `SA4015`: Calling functions like math.Ceil on floats converted from integers doesn't do anything useful
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4015": true}`.
+
+Package documentation: [SA4015](https://staticcheck.dev/docs/checks/#SA4015)
+
+
+## `SA4016`: Certain bitwise operations, such as x ^ 0, do not do anything useful
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4016](https://staticcheck.dev/docs/checks/#SA4016)
+
+
+## `SA4017`: Discarding the return values of a function without side effects, making the call pointless
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4017": true}`.
+
+Package documentation: [SA4017](https://staticcheck.dev/docs/checks/#SA4017)
+
+
+## `SA4018`: Self-assignment of variables
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4018": true}`.
+
+Package documentation: [SA4018](https://staticcheck.dev/docs/checks/#SA4018)
+
+
+## `SA4019`: Multiple, identical build constraints in the same file
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA4019](https://staticcheck.dev/docs/checks/#SA4019)
+
+
+## `SA4020`: Unreachable case clause in a type switch
+
+
+In a type switch like the following
+
+ type T struct{}
+ func (T) Read(b []byte) (int, error) { return 0, nil }
+
+ var v interface{} = T{}
+
+ switch v.(type) {
+ case io.Reader:
+ // ...
+ case T:
+ // unreachable
+ }
+
+the second case clause can never be reached because T implements
+io.Reader and case clauses are evaluated in source order.
+
+Another example:
+
+ type T struct{}
+ func (T) Read(b []byte) (int, error) { return 0, nil }
+ func (T) Close() error { return nil }
+
+ var v interface{} = T{}
+
+ switch v.(type) {
+ case io.Reader:
+ // ...
+ case io.ReadCloser:
+ // unreachable
+ }
+
+Even though T has a Close method and thus implements io.ReadCloser,
+io.Reader will always match first. The method set of io.Reader is a
+subset of io.ReadCloser. Thus it is impossible to match the second
+case without matching the first case.
+
+
+Structurally equivalent interfaces
+
+A special case of the previous example are structurally identical
+interfaces. Given these declarations
+
+ type T error
+ type V error
+
+ func doSomething() error {
+ err, ok := doAnotherThing()
+ if ok {
+ return T(err)
+ }
+
+ return U(err)
+ }
+
+the following type switch will have an unreachable case clause:
+
+ switch doSomething().(type) {
+ case T:
+ // ...
+ case V:
+ // unreachable
+ }
+
+T will always match before V because they are structurally equivalent
+and therefore doSomething()'s return value implements both.
+
+Available since
+ 2019.2
+
+
+Default: on.
+
+Package documentation: [SA4020](https://staticcheck.dev/docs/checks/#SA4020)
+
+
+## `SA4022`: Comparing the address of a variable against nil
+
+
+Code such as 'if &x == nil' is meaningless, because taking the address of a variable always yields a non-nil pointer.
+
+Available since
+ 2020.1
+
+
+Default: on.
+
+Package documentation: [SA4022](https://staticcheck.dev/docs/checks/#SA4022)
+
+
+## `SA4023`: Impossible comparison of interface value with untyped nil
+
+
+Under the covers, interfaces are implemented as two elements, a
+type T and a value V. V is a concrete value such as an int,
+struct or pointer, never an interface itself, and has type T. For
+instance, if we store the int value 3 in an interface, the
+resulting interface value has, schematically, (T=int, V=3). The
+value V is also known as the interface's dynamic value, since a
+given interface variable might hold different values V (and
+corresponding types T) during the execution of the program.
+
+An interface value is nil only if the V and T are both
+unset, (T=nil, V is not set), In particular, a nil interface will
+always hold a nil type. If we store a nil pointer of type *int
+inside an interface value, the inner type will be *int regardless
+of the value of the pointer: (T=*int, V=nil). Such an interface
+value will therefore be non-nil even when the pointer value V
+inside is nil.
+
+This situation can be confusing, and arises when a nil value is
+stored inside an interface value such as an error return:
+
+ func returnsError() error {
+ var p *MyError = nil
+ if bad() {
+ p = ErrBad
+ }
+ return p // Will always return a non-nil error.
+ }
+
+If all goes well, the function returns a nil p, so the return
+value is an error interface value holding (T=*MyError, V=nil).
+This means that if the caller compares the returned error to nil,
+it will always look as if there was an error even if nothing bad
+happened. To return a proper nil error to the caller, the
+function must return an explicit nil:
+
+ func returnsError() error {
+ if bad() {
+ return ErrBad
+ }
+ return nil
+ }
+
+It's a good idea for functions that return errors always to use
+the error type in their signature (as we did above) rather than a
+concrete type such as *MyError, to help guarantee the error is
+created correctly. As an example, os.Open returns an error even
+though, if not nil, it's always of concrete type *os.PathError.
+
+Similar situations to those described here can arise whenever
+interfaces are used. Just keep in mind that if any concrete value
+has been stored in the interface, the interface will not be nil.
+For more information, see The Laws of
+Reflection at https://golang.org/doc/articles/laws_of_reflection.html.
+
+This text has been copied from
+https://golang.org/doc/faq#nil_error, licensed under the Creative
+Commons Attribution 3.0 License.
+
+Available since
+ 2020.2
+
+
+Default: off. Enable by setting `"analyses": {"SA4023": true}`.
+
+Package documentation: [SA4023](https://staticcheck.dev/docs/checks/#SA4023)
+
+
+## `SA4024`: Checking for impossible return value from a builtin function
+
+
+Return values of the len and cap builtins cannot be negative.
+
+See https://golang.org/pkg/builtin/#len and https://golang.org/pkg/builtin/#cap.
+
+Example:
+
+ if len(slice) < 0 {
+ fmt.Println("unreachable code")
+ }
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [SA4024](https://staticcheck.dev/docs/checks/#SA4024)
+
+
+## `SA4025`: Integer division of literals that results in zero
+
+
+When dividing two integer constants, the result will
+also be an integer. Thus, a division such as 2 / 3 results in 0.
+This is true for all of the following examples:
+
+ _ = 2 / 3
+ const _ = 2 / 3
+ const _ float64 = 2 / 3
+ _ = float64(2 / 3)
+
+Staticcheck will flag such divisions if both sides of the division are
+integer literals, as it is highly unlikely that the division was
+intended to truncate to zero. Staticcheck will not flag integer
+division involving named constants, to avoid noisy positives.
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [SA4025](https://staticcheck.dev/docs/checks/#SA4025)
+
+
+## `SA4026`: Go constants cannot express negative zero
+
+
+In IEEE 754 floating point math, zero has a sign and can be positive
+or negative. This can be useful in certain numerical code.
+
+Go constants, however, cannot express negative zero. This means that
+the literals -0.0 and 0.0 have the same ideal value (zero) and
+will both represent positive zero at runtime.
+
+To explicitly and reliably create a negative zero, you can use the
+math.Copysign function: math.Copysign(0, -1).
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [SA4026](https://staticcheck.dev/docs/checks/#SA4026)
+
+
+## `SA4027`: (*net/url.URL).Query returns a copy, modifying it doesn't change the URL
+
+
+(*net/url.URL).Query parses the current value of net/url.URL.RawQuery
+and returns it as a map of type net/url.Values. Subsequent changes to
+this map will not affect the URL unless the map gets encoded and
+assigned to the URL's RawQuery.
+
+As a consequence, the following code pattern is an expensive no-op:
+u.Query().Add(key, value).
+
+Available since
+ 2021.1
+
+
+Default: on.
+
+Package documentation: [SA4027](https://staticcheck.dev/docs/checks/#SA4027)
+
+
+## `SA4028`: x % 1 is always zero
+
+
+Available since
+ 2022.1
+
+
+Default: on.
+
+Package documentation: [SA4028](https://staticcheck.dev/docs/checks/#SA4028)
+
+
+## `SA4029`: Ineffective attempt at sorting slice
+
+
+sort.Float64Slice, sort.IntSlice, and sort.StringSlice are
+types, not functions. Doing x = sort.StringSlice(x) does nothing,
+especially not sort any values. The correct usage is
+sort.Sort(sort.StringSlice(x)) or sort.StringSlice(x).Sort(),
+but there are more convenient helpers, namely sort.Float64s,
+sort.Ints, and sort.Strings.
+
+Available since
+ 2022.1
+
+
+Default: on.
+
+Package documentation: [SA4029](https://staticcheck.dev/docs/checks/#SA4029)
+
+
+## `SA4030`: Ineffective attempt at generating random number
+
+
+Functions in the math/rand package that accept upper limits, such
+as Intn, generate random numbers in the half-open interval [0,n). In
+other words, the generated numbers will be >= 0 and < n – they
+don't include n. rand.Intn(1) therefore doesn't generate 0
+or 1, it always generates 0.
+
+Available since
+ 2022.1
+
+
+Default: on.
+
+Package documentation: [SA4030](https://staticcheck.dev/docs/checks/#SA4030)
+
+
+## `SA4031`: Checking never-nil value against nil
+
+
+Available since
+ 2022.1
+
+
+Default: off. Enable by setting `"analyses": {"SA4031": true}`.
+
+Package documentation: [SA4031](https://staticcheck.dev/docs/checks/#SA4031)
+
+
+## `SA4032`: Comparing runtime.GOOS or runtime.GOARCH against impossible value
+
+
+Available since
+ 2024.1
+
+
+Default: on.
+
+Package documentation: [SA4032](https://staticcheck.dev/docs/checks/#SA4032)
+
+
+## `SA5000`: Assignment to nil map
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA5000": true}`.
+
+Package documentation: [SA5000](https://staticcheck.dev/docs/checks/#SA5000)
+
+
+## `SA5001`: Deferring Close before checking for a possible error
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA5001](https://staticcheck.dev/docs/checks/#SA5001)
+
+
+## `SA5002`: The empty for loop ('for {}') spins and can block the scheduler
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA5002": true}`.
+
+Package documentation: [SA5002](https://staticcheck.dev/docs/checks/#SA5002)
+
+
+## `SA5003`: Defers in infinite loops will never execute
+
+
+Defers are scoped to the surrounding function, not the surrounding
+block. In a function that never returns, i.e. one containing an
+infinite loop, defers will never execute.
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA5003](https://staticcheck.dev/docs/checks/#SA5003)
+
+
+## `SA5004`: 'for { select { ...' with an empty default branch spins
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA5004](https://staticcheck.dev/docs/checks/#SA5004)
+
+
+## `SA5005`: The finalizer references the finalized object, preventing garbage collection
+
+
+A finalizer is a function associated with an object that runs when the
+garbage collector is ready to collect said object, that is when the
+object is no longer referenced by anything.
+
+If the finalizer references the object, however, it will always remain
+as the final reference to that object, preventing the garbage
+collector from collecting the object. The finalizer will never run,
+and the object will never be collected, leading to a memory leak. That
+is why the finalizer should instead use its first argument to operate
+on the object. That way, the number of references can temporarily go
+to zero before the object is being passed to the finalizer.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA5005": true}`.
+
+Package documentation: [SA5005](https://staticcheck.dev/docs/checks/#SA5005)
+
+
+## `SA5007`: Infinite recursive call
+
+
+A function that calls itself recursively needs to have an exit
+condition. Otherwise it will recurse forever, until the system runs
+out of memory.
+
+This issue can be caused by simple bugs such as forgetting to add an
+exit condition. It can also happen "on purpose". Some languages have
+tail call optimization which makes certain infinite recursive calls
+safe to use. Go, however, does not implement TCO, and as such a loop
+should be used instead.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA5007": true}`.
+
+Package documentation: [SA5007](https://staticcheck.dev/docs/checks/#SA5007)
+
+
+## `SA5008`: Invalid struct tag
+
+
+Available since
+ 2019.2
+
+
+Default: on.
+
+Package documentation: [SA5008](https://staticcheck.dev/docs/checks/#SA5008)
+
+
+## `SA5010`: Impossible type assertion
+
+
+Some type assertions can be statically proven to be
+impossible. This is the case when the method sets of both
+arguments of the type assertion conflict with each other, for
+example by containing the same method with different
+signatures.
+
+The Go compiler already applies this check when asserting from an
+interface value to a concrete type. If the concrete type misses
+methods from the interface, or if function signatures don't match,
+then the type assertion can never succeed.
+
+This check applies the same logic when asserting from one interface to
+another. If both interface types contain the same method but with
+different signatures, then the type assertion can never succeed,
+either.
+
+Available since
+ 2020.1
+
+
+Default: off. Enable by setting `"analyses": {"SA5010": true}`.
+
+Package documentation: [SA5010](https://staticcheck.dev/docs/checks/#SA5010)
+
+
+## `SA5011`: Possible nil pointer dereference
+
+
+A pointer is being dereferenced unconditionally, while
+also being checked against nil in another place. This suggests that
+the pointer may be nil and dereferencing it may panic. This is
+commonly a result of improperly ordered code or missing return
+statements. Consider the following examples:
+
+ func fn(x *int) {
+ fmt.Println(*x)
+
+ // This nil check is equally important for the previous dereference
+ if x != nil {
+ foo(*x)
+ }
+ }
+
+ func TestFoo(t *testing.T) {
+ x := compute()
+ if x == nil {
+ t.Errorf("nil pointer received")
+ }
+
+ // t.Errorf does not abort the test, so if x is nil, the next line will panic.
+ foo(*x)
+ }
+
+Staticcheck tries to deduce which functions abort control flow.
+For example, it is aware that a function will not continue
+execution after a call to panic or log.Fatal. However, sometimes
+this detection fails, in particular in the presence of
+conditionals. Consider the following example:
+
+ func Log(msg string, level int) {
+ fmt.Println(msg)
+ if level == levelFatal {
+ os.Exit(1)
+ }
+ }
+
+ func Fatal(msg string) {
+ Log(msg, levelFatal)
+ }
+
+ func fn(x *int) {
+ if x == nil {
+ Fatal("unexpected nil pointer")
+ }
+ fmt.Println(*x)
+ }
+
+Staticcheck will flag the dereference of x, even though it is perfectly
+safe. Staticcheck is not able to deduce that a call to
+Fatal will exit the program. For the time being, the easiest
+workaround is to modify the definition of Fatal like so:
+
+ func Fatal(msg string) {
+ Log(msg, levelFatal)
+ panic("unreachable")
+ }
+
+We also hard-code functions from common logging packages such as
+logrus. Please file an issue if we're missing support for a
+popular package.
+
+Available since
+ 2020.1
+
+
+Default: off. Enable by setting `"analyses": {"SA5011": true}`.
+
+Package documentation: [SA5011](https://staticcheck.dev/docs/checks/#SA5011)
+
+
+## `SA5012`: Passing odd-sized slice to function expecting even size
+
+
+Some functions that take slices as parameters expect the slices to have an even number of elements.
+Often, these functions treat elements in a slice as pairs.
+For example, strings.NewReplacer takes pairs of old and new strings,
+and calling it with an odd number of elements would be an error.
+
+Available since
+ 2020.2
+
+
+Default: off. Enable by setting `"analyses": {"SA5012": true}`.
+
+Package documentation: [SA5012](https://staticcheck.dev/docs/checks/#SA5012)
+
+
+## `SA6000`: Using regexp.Match or related in a loop, should use regexp.Compile
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA6000": true}`.
+
+Package documentation: [SA6000](https://staticcheck.dev/docs/checks/#SA6000)
+
+
+## `SA6001`: Missing an optimization opportunity when indexing maps by byte slices
+
+
+Map keys must be comparable, which precludes the use of byte slices.
+This usually leads to using string keys and converting byte slices to
+strings.
+
+Normally, a conversion of a byte slice to a string needs to copy the data and
+causes allocations. The compiler, however, recognizes m[string(b)] and
+uses the data of b directly, without copying it, because it knows that
+the data can't change during the map lookup. This leads to the
+counter-intuitive situation that
+
+ k := string(b)
+ println(m[k])
+ println(m[k])
+
+will be less efficient than
+
+ println(m[string(b)])
+ println(m[string(b)])
+
+because the first version needs to copy and allocate, while the second
+one does not.
+
+For some history on this optimization, check out commit
+f5f5a8b6209f84961687d993b93ea0d397f5d5bf in the Go repository.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA6001": true}`.
+
+Package documentation: [SA6001](https://staticcheck.dev/docs/checks/#SA6001)
+
+
+## `SA6002`: Storing non-pointer values in sync.Pool allocates memory
+
+
+A sync.Pool is used to avoid unnecessary allocations and reduce the
+amount of work the garbage collector has to do.
+
+When passing a value that is not a pointer to a function that accepts
+an interface, the value needs to be placed on the heap, which means an
+additional allocation. Slices are a common thing to put in sync.Pools,
+and they're structs with 3 fields (length, capacity, and a pointer to
+an array). In order to avoid the extra allocation, one should store a
+pointer to the slice instead.
+
+See the comments on https://go-review.googlesource.com/c/go/+/24371
+that discuss this problem.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA6002": true}`.
+
+Package documentation: [SA6002](https://staticcheck.dev/docs/checks/#SA6002)
+
+
+## `SA6003`: Converting a string to a slice of runes before ranging over it
+
+
+You may want to loop over the runes in a string. Instead of converting
+the string to a slice of runes and looping over that, you can loop
+over the string itself. That is,
+
+ for _, r := range s {}
+
+and
+
+ for _, r := range []rune(s) {}
+
+will yield the same values. The first version, however, will be faster
+and avoid unnecessary memory allocations.
+
+Do note that if you are interested in the indices, ranging over a
+string and over a slice of runes will yield different indices. The
+first one yields byte offsets, while the second one yields indices in
+the slice of runes.
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA6003": true}`.
+
+Package documentation: [SA6003](https://staticcheck.dev/docs/checks/#SA6003)
+
+
+## `SA6005`: Inefficient string comparison with strings.ToLower or strings.ToUpper
+
+
+Converting two strings to the same case and comparing them like so
+
+ if strings.ToLower(s1) == strings.ToLower(s2) {
+ ...
+ }
+
+is significantly more expensive than comparing them with
+strings.EqualFold(s1, s2). This is due to memory usage as well as
+computational complexity.
+
+strings.ToLower will have to allocate memory for the new strings, as
+well as convert both strings fully, even if they differ on the very
+first byte. strings.EqualFold, on the other hand, compares the strings
+one character at a time. It doesn't need to create two intermediate
+strings and can return as soon as the first non-matching character has
+been found.
+
+For a more in-depth explanation of this issue, see
+https://blog.digitalocean.com/how-to-efficiently-compare-strings-in-go/
+
+Available since
+ 2019.2
+
+
+Default: on.
+
+Package documentation: [SA6005](https://staticcheck.dev/docs/checks/#SA6005)
+
+
+## `SA6006`: Using io.WriteString to write []byte
+
+
+Using io.WriteString to write a slice of bytes, as in
+
+ io.WriteString(w, string(b))
+
+is both unnecessary and inefficient. Converting from []byte to string
+has to allocate and copy the data, and we could simply use w.Write(b)
+instead.
+
+Available since
+ 2024.1
+
+
+Default: on.
+
+Package documentation: [SA6006](https://staticcheck.dev/docs/checks/#SA6006)
+
+
+## `SA9001`: Defers in range loops may not run when you expect them to
+
+
+Available since
+ 2017.1
+
+
+Default: off. Enable by setting `"analyses": {"SA9001": true}`.
+
+Package documentation: [SA9001](https://staticcheck.dev/docs/checks/#SA9001)
+
+
+## `SA9002`: Using a non-octal os.FileMode that looks like it was meant to be in octal.
+
+
+Available since
+ 2017.1
+
+
+Default: on.
+
+Package documentation: [SA9002](https://staticcheck.dev/docs/checks/#SA9002)
+
+
+## `SA9003`: Empty body in an if or else branch
+
+
+Available since
+ 2017.1, non-default
+
+
+Default: off. Enable by setting `"analyses": {"SA9003": true}`.
+
+Package documentation: [SA9003](https://staticcheck.dev/docs/checks/#SA9003)
+
+
+## `SA9004`: Only the first constant has an explicit type
+
+
+In a constant declaration such as the following:
+
+ const (
+ First byte = 1
+ Second = 2
+ )
+
+the constant Second does not have the same type as the constant First.
+This construct shouldn't be confused with
+
+ const (
+ First byte = iota
+ Second
+ )
+
+where First and Second do indeed have the same type. The type is only
+passed on when no explicit value is assigned to the constant.
+
+When declaring enumerations with explicit values it is therefore
+important not to write
+
+ const (
+ EnumFirst EnumType = 1
+ EnumSecond = 2
+ EnumThird = 3
+ )
+
+This discrepancy in types can cause various confusing behaviors and
+bugs.
+
+
+Wrong type in variable declarations
+
+The most obvious issue with such incorrect enumerations expresses
+itself as a compile error:
+
+ package pkg
+
+ const (
+ EnumFirst uint8 = 1
+ EnumSecond = 2
+ )
+
+ func fn(useFirst bool) {
+ x := EnumSecond
+ if useFirst {
+ x = EnumFirst
+ }
+ }
+
+fails to compile with
+
+ ./const.go:11:5: cannot use EnumFirst (type uint8) as type int in assignment
+
+
+Losing method sets
+
+A more subtle issue occurs with types that have methods and optional
+interfaces. Consider the following:
+
+ package main
+
+ import "fmt"
+
+ type Enum int
+
+ func (e Enum) String() string {
+ return "an enum"
+ }
+
+ const (
+ EnumFirst Enum = 1
+ EnumSecond = 2
+ )
+
+ func main() {
+ fmt.Println(EnumFirst)
+ fmt.Println(EnumSecond)
+ }
+
+This code will output
+
+ an enum
+ 2
+
+as EnumSecond has no explicit type, and thus defaults to int.
+
+Available since
+ 2019.1
+
+
+Default: on.
+
+Package documentation: [SA9004](https://staticcheck.dev/docs/checks/#SA9004)
+
+
+## `SA9005`: Trying to marshal a struct with no public fields nor custom marshaling
+
+
+The encoding/json and encoding/xml packages only operate on exported
+fields in structs, not unexported ones. It is usually an error to try
+to (un)marshal structs that only consist of unexported fields.
+
+This check will not flag calls involving types that define custom
+marshaling behavior, e.g. via MarshalJSON methods. It will also not
+flag empty structs.
+
+Available since
+ 2019.2
+
+
+Default: off. Enable by setting `"analyses": {"SA9005": true}`.
+
+Package documentation: [SA9005](https://staticcheck.dev/docs/checks/#SA9005)
+
+
+## `SA9006`: Dubious bit shifting of a fixed size integer value
+
+
+Bit shifting a value past its size will always clear the value.
+
+For instance:
+
+ v := int8(42)
+ v >>= 8
+
+will always result in 0.
+
+This check flags bit shifting operations on fixed size integer values only.
+That is, int, uint and uintptr are never flagged to avoid potential false
+positives in somewhat exotic but valid bit twiddling tricks:
+
+ // Clear any value above 32 bits if integers are more than 32 bits.
+ func f(i int) int {
+ v := i >> 32
+ v = v << 32
+ return i-v
+ }
+
+Available since
+ 2020.2
+
+
+Default: on.
+
+Package documentation: [SA9006](https://staticcheck.dev/docs/checks/#SA9006)
+
+
+## `SA9007`: Deleting a directory that shouldn't be deleted
+
+
+It is virtually never correct to delete system directories such as
+/tmp or the user's home directory. However, it can be fairly easy to
+do by mistake, for example by mistakenly using os.TempDir instead
+of ioutil.TempDir, or by forgetting to add a suffix to the result
+of os.UserHomeDir.
+
+Writing
+
+ d := os.TempDir()
+ defer os.RemoveAll(d)
+
+in your unit tests will have a devastating effect on the stability of your system.
+
+This check flags attempts at deleting the following directories:
+
+- os.TempDir
+- os.UserCacheDir
+- os.UserConfigDir
+- os.UserHomeDir
+
+Available since
+ 2022.1
+
+
+Default: off. Enable by setting `"analyses": {"SA9007": true}`.
+
+Package documentation: [SA9007](https://staticcheck.dev/docs/checks/#SA9007)
+
+
+## `SA9008`: else branch of a type assertion is probably not reading the right value
+
+
+When declaring variables as part of an if statement (like in 'if
+foo := ...; foo {'), the same variables will also be in the scope of
+the else branch. This means that in the following example
+
+ if x, ok := x.(int); ok {
+ // ...
+ } else {
+ fmt.Printf("unexpected type %T", x)
+ }
+
+x in the else branch will refer to the x from x, ok
+:=; it will not refer to the x that is being type-asserted. The
+result of a failed type assertion is the zero value of the type that
+is being asserted to, so x in the else branch will always have the
+value 0 and the type int.
+
+Available since
+ 2022.1
+
+
+Default: off. Enable by setting `"analyses": {"SA9008": true}`.
+
+Package documentation: [SA9008](https://staticcheck.dev/docs/checks/#SA9008)
+
+
+## `SA9009`: Ineffectual Go compiler directive
+
+
+A potential Go compiler directive was found, but is ineffectual as it begins
+with whitespace.
+
+Available since
+ 2024.1
+
+
+Default: on.
+
+Package documentation: [SA9009](https://staticcheck.dev/docs/checks/#SA9009)
+
+
+## `ST1000`: Incorrect or missing package comment
+
+
+Packages must have a package comment that is formatted according to
+the guidelines laid out in
+https://go.dev/wiki/CodeReviewComments#package-comments.
+
+Available since
+ 2019.1, non-default
+
+
+Default: off. Enable by setting `"analyses": {"ST1000": true}`.
+
+Package documentation: [ST1000](https://staticcheck.dev/docs/checks/#ST1000)
+
+
+## `ST1001`: Dot imports are discouraged
+
+
+Dot imports that aren't in external test packages are discouraged.
+
+The dot_import_whitelist option can be used to whitelist certain
+imports.
+
+Quoting Go Code Review Comments:
+
+> The import . form can be useful in tests that, due to circular
+> dependencies, cannot be made part of the package being tested:
+>
+> package foo_test
+>
+> import (
+> "bar/testutil" // also imports "foo"
+> . "foo"
+> )
+>
+> In this case, the test file cannot be in package foo because it
+> uses bar/testutil, which imports foo. So we use the import .
+> form to let the file pretend to be part of package foo even though
+> it is not. Except for this one case, do not use import . in your
+> programs. It makes the programs much harder to read because it is
+> unclear whether a name like Quux is a top-level identifier in the
+> current package or in an imported package.
+
+Available since
+ 2019.1
+
+Options
+ dot_import_whitelist
+
+
+Default: off. Enable by setting `"analyses": {"ST1001": true}`.
+
+Package documentation: [ST1001](https://staticcheck.dev/docs/checks/#ST1001)
+
+
+## `ST1003`: Poorly chosen identifier
+
+
+Identifiers, such as variable and package names, follow certain rules.
+
+See the following links for details:
+
+- https://go.dev/doc/effective_go#package-names
+- https://go.dev/doc/effective_go#mixed-caps
+- https://go.dev/wiki/CodeReviewComments#initialisms
+- https://go.dev/wiki/CodeReviewComments#variable-names
+
+Available since
+ 2019.1, non-default
+
+Options
+ initialisms
+
+
+Default: off. Enable by setting `"analyses": {"ST1003": true}`.
+
+Package documentation: [ST1003](https://staticcheck.dev/docs/checks/#ST1003)
+
+
+## `ST1005`: Incorrectly formatted error string
+
+
+Error strings follow a set of guidelines to ensure uniformity and good
+composability.
+
+Quoting Go Code Review Comments:
+
+> Error strings should not be capitalized (unless beginning with
+> proper nouns or acronyms) or end with punctuation, since they are
+> usually printed following other context. That is, use
+> fmt.Errorf("something bad") not fmt.Errorf("Something bad"), so
+> that log.Printf("Reading %s: %v", filename, err) formats without a
+> spurious capital letter mid-message.
+
+Available since
+ 2019.1
+
+
+Default: off. Enable by setting `"analyses": {"ST1005": true}`.
+
+Package documentation: [ST1005](https://staticcheck.dev/docs/checks/#ST1005)
+
+
+## `ST1006`: Poorly chosen receiver name
+
+
+Quoting Go Code Review Comments:
+
+> The name of a method's receiver should be a reflection of its
+> identity; often a one or two letter abbreviation of its type
+> suffices (such as "c" or "cl" for "Client"). Don't use generic
+> names such as "me", "this" or "self", identifiers typical of
+> object-oriented languages that place more emphasis on methods as
+> opposed to functions. The name need not be as descriptive as that
+> of a method argument, as its role is obvious and serves no
+> documentary purpose. It can be very short as it will appear on
+> almost every line of every method of the type; familiarity admits
+> brevity. Be consistent, too: if you call the receiver "c" in one
+> method, don't call it "cl" in another.
+
+Available since
+ 2019.1
+
+
+Default: off. Enable by setting `"analyses": {"ST1006": true}`.
+
+Package documentation: [ST1006](https://staticcheck.dev/docs/checks/#ST1006)
+
+
+## `ST1008`: A function's error value should be its last return value
+
+
+A function's error value should be its last return value.
+
+Available since
+ 2019.1
+
+
+Default: off. Enable by setting `"analyses": {"ST1008": true}`.
+
+Package documentation: [ST1008](https://staticcheck.dev/docs/checks/#ST1008)
+
+
+## `ST1011`: Poorly chosen name for variable of type time.Duration
+
+
+time.Duration values represent an amount of time, which is represented
+as a count of nanoseconds. An expression like 5 * time.Microsecond
+yields the value 5000. It is therefore not appropriate to suffix a
+variable of type time.Duration with any time unit, such as Msec or
+Milli.
+
+Available since
+ 2019.1
+
+
+Default: off. Enable by setting `"analyses": {"ST1011": true}`.
+
+Package documentation: [ST1011](https://staticcheck.dev/docs/checks/#ST1011)
+
+
+## `ST1012`: Poorly chosen name for error variable
+
+
+Error variables that are part of an API should be called errFoo or
+ErrFoo.
+
+Available since
+ 2019.1
+
+
+Default: off. Enable by setting `"analyses": {"ST1012": true}`.
+
+Package documentation: [ST1012](https://staticcheck.dev/docs/checks/#ST1012)
+
+
+## `ST1013`: Should use constants for HTTP error codes, not magic numbers
+
+
+HTTP has a tremendous number of status codes. While some of those are
+well known (200, 400, 404, 500), most of them are not. The net/http
+package provides constants for all status codes that are part of the
+various specifications. It is recommended to use these constants
+instead of hard-coding magic numbers, to vastly improve the
+readability of your code.
+
+Available since
+ 2019.1
+
+Options
+ http_status_code_whitelist
+
+
+Default: off. Enable by setting `"analyses": {"ST1013": true}`.
+
+Package documentation: [ST1013](https://staticcheck.dev/docs/checks/#ST1013)
+
+
+## `ST1015`: A switch's default case should be the first or last case
+
+
+Available since
+ 2019.1
+
+
+Default: off. Enable by setting `"analyses": {"ST1015": true}`.
+
+Package documentation: [ST1015](https://staticcheck.dev/docs/checks/#ST1015)
+
+
+## `ST1016`: Use consistent method receiver names
+
+
+Available since
+ 2019.1, non-default
+
+
+Default: off. Enable by setting `"analyses": {"ST1016": true}`.
+
+Package documentation: [ST1016](https://staticcheck.dev/docs/checks/#ST1016)
+
+
+## `ST1017`: Don't use Yoda conditions
+
+
+Yoda conditions are conditions of the kind 'if 42 == x', where the
+literal is on the left side of the comparison. These are a common
+idiom in languages in which assignment is an expression, to avoid bugs
+of the kind 'if (x = 42)'. In Go, which doesn't allow for this kind of
+bug, we prefer the more idiomatic 'if x == 42'.
+
+Available since
+ 2019.2
+
+
+Default: off. Enable by setting `"analyses": {"ST1017": true}`.
+
+Package documentation: [ST1017](https://staticcheck.dev/docs/checks/#ST1017)
+
+
+## `ST1018`: Avoid zero-width and control characters in string literals
+
+
+Available since
+ 2019.2
+
+
+Default: off. Enable by setting `"analyses": {"ST1018": true}`.
+
+Package documentation: [ST1018](https://staticcheck.dev/docs/checks/#ST1018)
+
+
+## `ST1019`: Importing the same package multiple times
+
+
+Go allows importing the same package multiple times, as long as
+different import aliases are being used. That is, the following
+bit of code is valid:
+
+ import (
+ "fmt"
+ fumpt "fmt"
+ format "fmt"
+ _ "fmt"
+ )
+
+However, this is very rarely done on purpose. Usually, it is a
+sign of code that got refactored, accidentally adding duplicate
+import statements. It is also a rarely known feature, which may
+contribute to confusion.
+
+Do note that sometimes, this feature may be used
+intentionally (see for example
+https://github.com/golang/go/commit/3409ce39bfd7584523b7a8c150a310cea92d879d)
+– if you want to allow this pattern in your code base, you're
+advised to disable this check.
+
+Available since
+ 2020.1
+
+
+Default: off. Enable by setting `"analyses": {"ST1019": true}`.
+
+Package documentation: [ST1019](https://staticcheck.dev/docs/checks/#ST1019)
+
+
+## `ST1020`: The documentation of an exported function should start with the function's name
+
+
+Doc comments work best as complete sentences, which
+allow a wide variety of automated presentations. The first sentence
+should be a one-sentence summary that starts with the name being
+declared.
+
+If every doc comment begins with the name of the item it describes,
+you can use the doc subcommand of the go tool and run the output
+through grep.
+
+See https://go.dev/doc/effective_go#commentary for more
+information on how to write good documentation.
+
+Available since
+ 2020.1, non-default
+
+
+Default: off. Enable by setting `"analyses": {"ST1020": true}`.
+
+Package documentation: [ST1020](https://staticcheck.dev/docs/checks/#ST1020)
+
+
+## `ST1021`: The documentation of an exported type should start with type's name
+
+
+Doc comments work best as complete sentences, which
+allow a wide variety of automated presentations. The first sentence
+should be a one-sentence summary that starts with the name being
+declared.
+
+If every doc comment begins with the name of the item it describes,
+you can use the doc subcommand of the go tool and run the output
+through grep.
+
+See https://go.dev/doc/effective_go#commentary for more
+information on how to write good documentation.
+
+Available since
+ 2020.1, non-default
+
+
+Default: off. Enable by setting `"analyses": {"ST1021": true}`.
+
+Package documentation: [ST1021](https://staticcheck.dev/docs/checks/#ST1021)
+
+
+## `ST1022`: The documentation of an exported variable or constant should start with variable's name
+
+
+Doc comments work best as complete sentences, which
+allow a wide variety of automated presentations. The first sentence
+should be a one-sentence summary that starts with the name being
+declared.
+
+If every doc comment begins with the name of the item it describes,
+you can use the doc subcommand of the go tool and run the output
+through grep.
+
+See https://go.dev/doc/effective_go#commentary for more
+information on how to write good documentation.
+
+Available since
+ 2020.1, non-default
+
+
+Default: off. Enable by setting `"analyses": {"ST1022": true}`.
+
+Package documentation: [ST1022](https://staticcheck.dev/docs/checks/#ST1022)
+
+
+## `ST1023`: Redundant type in variable declaration
+
+
+Available since
+ 2021.1, non-default
+
+
+Default: off. Enable by setting `"analyses": {"ST1023": true}`.
+
+Package documentation: [ST1023](https://staticcheck.dev/docs/checks/#)
+
## `appends`: check for missing values after append
@@ -298,7 +3490,7 @@ The gofix analyzer inlines functions and constants that are marked for inlining.
Default: on.
-Package documentation: [gofix](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix)
+Package documentation: [gofix](https://pkg.go.dev/golang.org/x/tools/internal/gofix)
## `hostport`: check format of addresses passed to net.Dial
@@ -323,7 +3515,7 @@ A similar diagnostic and fix are produced for a format string of "%s:%s".
Default: on.
-Package documentation: [hostport](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/hostport)
+Package documentation: [hostport](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/hostport)
## `httpresponse`: check for mistakes using HTTP responses
@@ -476,29 +3668,92 @@ Package documentation: [lostcancel](https://pkg.go.dev/golang.org/x/tools/go/ana
This analyzer reports opportunities for simplifying and clarifying
-existing code by using more modern features of Go, such as:
-
- - replacing an if/else conditional assignment by a call to the
- built-in min or max functions added in go1.21;
- - replacing sort.Slice(x, func(i, j int) bool) { return s[i] < s[j] }
- by a call to slices.Sort(s), added in go1.21;
- - replacing interface{} by the 'any' type added in go1.18;
- - replacing append([]T(nil), s...) by slices.Clone(s) or
- slices.Concat(s), added in go1.21;
- - replacing a loop around an m[k]=v map update by a call
- to one of the Collect, Copy, Clone, or Insert functions
- from the maps package, added in go1.21;
- - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),
- added in go1.19;
- - replacing uses of context.WithCancel in tests with t.Context, added in
- go1.24;
- - replacing omitempty by omitzero on structs, added in go1.24;
- - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),
- added in go1.21
- - replacing a 3-clause for i := 0; i < n; i++ {} loop by
- for i := range n {}, added in go1.22;
- - replacing Split in "for range strings.Split(...)" by go1.24's
- more efficient SplitSeq;
+existing code by using more modern features of Go and its standard
+library.
+
+Each diagnostic provides a fix. Our intent is that these fixes may
+be safely applied en masse without changing the behavior of your
+program. In some cases the suggested fixes are imperfect and may
+lead to (for example) unused imports or unused local variables,
+causing build breakage. However, these problems are generally
+trivial to fix. We regard any modernizer whose fix changes program
+behavior to have a serious bug and will endeavor to fix it.
+
+To apply all modernization fixes en masse, you can use the
+following command:
+
+ $ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...
+
+(Do not use "go get -tool" to add gopls as a dependency of your
+module; gopls commands must be built from their release branch.)
+
+If the tool warns of conflicting fixes, you may need to run it more
+than once until it has applied all fixes cleanly. This command is
+not an officially supported interface and may change in the future.
+
+Changes produced by this tool should be reviewed as usual before
+being merged. In some cases, a loop may be replaced by a simple
+function call, causing comments within the loop to be discarded.
+Human judgment may be required to avoid losing comments of value.
+
+Each diagnostic reported by modernize has a specific category. (The
+categories are listed below.) Diagnostics in some categories, such
+as "efaceany" (which replaces "interface{}" with "any" where it is
+safe to do so) are particularly numerous. It may ease the burden of
+code review to apply fixes in two passes, the first change
+consisting only of fixes of category "efaceany", the second
+consisting of all others. This can be achieved using the -category flag:
+
+ $ modernize -category=efaceany -fix -test ./...
+ $ modernize -category=-efaceany -fix -test ./...
+
+Categories of modernize diagnostic:
+
+ - forvar: remove x := x variable declarations made unnecessary by the new semantics of loops in go1.22.
+
+ - slicescontains: replace 'for i, elem := range s { if elem == needle { ...; break }'
+ by a call to slices.Contains, added in go1.21.
+
+ - minmax: replace an if/else conditional assignment by a call to
+ the built-in min or max functions added in go1.21.
+
+ - sortslice: replace sort.Slice(x, func(i, j int) bool) { return s[i] < s[j] }
+ by a call to slices.Sort(s), added in go1.21.
+
+ - efaceany: replace interface{} by the 'any' type added in go1.18.
+
+ - slicesclone: replace append([]T(nil), s...) by slices.Clone(s) or
+ slices.Concat(s), added in go1.21.
+
+ - mapsloop: replace a loop around an m[k]=v map update by a call
+ to one of the Collect, Copy, Clone, or Insert functions from
+ the maps package, added in go1.21.
+
+ - fmtappendf: replace []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),
+ added in go1.19.
+
+ - testingcontext: replace uses of context.WithCancel in tests
+ with t.Context, added in go1.24.
+
+ - omitzero: replace omitempty by omitzero on structs, added in go1.24.
+
+ - bloop: replace "for i := range b.N" or "for range b.N" in a
+ benchmark with "for b.Loop()", and remove any preceding calls
+ to b.StopTimer, b.StartTimer, and b.ResetTimer.
+
+ - slicesdelete: replace append(s[:i], s[i+1]...) by
+ slices.Delete(s, i, i+1), added in go1.21.
+
+ - rangeint: replace a 3-clause "for i := 0; i < n; i++" loop by
+ "for i := range n", added in go1.22.
+
+ - stringsseq: replace Split in "for range strings.Split(...)" by go1.24's
+ more efficient SplitSeq, or Fields with FieldSeq.
+
+ - stringscutprefix: replace some uses of HasPrefix followed by TrimPrefix with CutPrefix,
+ added to the strings package in go1.20.
+
+ - waitgroup: replace old complex usages of sync.WaitGroup by less complex WaitGroup.Go method in go1.25.
Default: on.
@@ -962,12 +4217,29 @@ A method is considered unused if it is unexported, not referenced
that of any method of an interface type declared within the same
package.
-The tool may report a false positive for a declaration of an
-unexported function that is referenced from another package using
-the go:linkname mechanism, if the declaration's doc comment does
-not also have a go:linkname comment. (Such code is in any case
-strongly discouraged: linkname annotations, if they must be used at
-all, should be used on both the declaration and the alias.)
+The tool may report false positives in some situations, for
+example:
+
+ - For a declaration of an unexported function that is referenced
+ from another package using the go:linkname mechanism, if the
+ declaration's doc comment does not also have a go:linkname
+ comment.
+
+ (Such code is in any case strongly discouraged: linkname
+ annotations, if they must be used at all, should be used on both
+ the declaration and the alias.)
+
+ - For compiler intrinsics in the "runtime" package that, though
+ never referenced, are known to the compiler and are called
+ indirectly by compiled object code.
+
+ - For functions called only from assembly.
+
+ - For functions called only from files whose build tags are not
+ selected in the current build configuration.
+
+See https://github.com/golang/go/issues/71686 for discussion of
+these limitations.
The unusedfunc algorithm is not as precise as the
golang.org/x/tools/cmd/deadcode tool, but it has the advantage that
diff --git a/gopls/doc/assets/go.mod b/gopls/doc/assets/go.mod
index 73f49695726..9b417f19ed8 100644
--- a/gopls/doc/assets/go.mod
+++ b/gopls/doc/assets/go.mod
@@ -4,4 +4,4 @@
module golang.org/x/tools/gopls/doc/assets
-go 1.19
+go 1.23.0
diff --git a/gopls/doc/assets/subtypes.png b/gopls/doc/assets/subtypes.png
new file mode 100644
index 00000000000..9868a56a77d
Binary files /dev/null and b/gopls/doc/assets/subtypes.png differ
diff --git a/gopls/doc/assets/supertypes.png b/gopls/doc/assets/supertypes.png
new file mode 100644
index 00000000000..59e1c79750d
Binary files /dev/null and b/gopls/doc/assets/supertypes.png differ
diff --git a/gopls/doc/codelenses.md b/gopls/doc/codelenses.md
index d8aa8e1f479..fa7c6c68859 100644
--- a/gopls/doc/codelenses.md
+++ b/gopls/doc/codelenses.md
@@ -75,6 +75,8 @@ File type: Go
## `run_govulncheck`: Run govulncheck (legacy)
+**This setting is experimental and may be deleted.**
+
This codelens source annotates the `module` directive in a go.mod file
with a command to run Govulncheck asynchronously.
@@ -134,6 +136,8 @@ File type: go.mod
## `vulncheck`: Run govulncheck
+**This setting is experimental and may be deleted.**
+
This codelens source annotates the `module` directive in a go.mod file
with a command to run govulncheck synchronously.
diff --git a/gopls/doc/features/diagnostics.md b/gopls/doc/features/diagnostics.md
index ceec607c123..75c29d5f795 100644
--- a/gopls/doc/features/diagnostics.md
+++ b/gopls/doc/features/diagnostics.md
@@ -51,7 +51,7 @@ build`. Gopls doesn't actually run the compiler; that would be too
There is an optional third source of diagnostics:
-
+
- **Compiler optimization details** are diagnostics that report
details relevant to optimization decisions made by the Go
@@ -314,12 +314,8 @@ dorky details and deletia:
currently: atomicalign deepequalerrors nilness sortslice unusedwrite embeddirective
-- **staticcheck**: four suites:
-
- add(simple.Analyzers, nil)
- add(staticcheck.Analyzers - SA5009, SA5011
- add(stylecheck.Analyzers, nil)
- add(quickfix.Analyzers, nil)
+- **staticcheck**: four suites (S=simple, SA=static analysis, QF=quickfix, ST=stylecheck)
+ Only a hand-picked subset of them are enabled by default.
- **Experimental analyzers**. Gopls has some analyzers that are not
enabled by default, because they produce too high a rate of false
diff --git a/gopls/doc/features/navigation.md b/gopls/doc/features/navigation.md
index f46f2935683..11b40797cd4 100644
--- a/gopls/doc/features/navigation.md
+++ b/gopls/doc/features/navigation.md
@@ -85,18 +85,43 @@ Client support:
The LSP
[`textDocument/implementation`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_implementation)
-request queries the "implements" relation between interfaces and concrete types:
+request queries the relation between abstract and concrete types and
+their methods.
+
+Interfaces and concrete types are matched using method sets:
- When invoked on a reference to an **interface type**, it returns the
location of the declaration of each type that implements
the interface.
- When invoked on a **concrete type**,
- it returns the locations of the matching interface types.
+ it returns the locations of the matching interface types.
- When invoked on an **interface method**, it returns the corresponding
methods of the types that satisfy the interface.
- When invoked on a **concrete method**,
it returns the locations of the matching interface methods.
+For example:
+- `implementation(io.Reader)` includes subinterfaces such as `io.ReadCloser`,
+ and concrete implementations such as `*os.File`. It also includes
+ other declarations equivalent to `io.Reader`.
+- `implementation(os.File)` includes only interfaces, such as
+ `io.Reader` and `io.ReadCloser`.
+
+The LSP's Implementation feature has a built-in bias towards subtypes,
+possibly because in languages such as Java and C++ the relationship
+between a type and its supertypes is explicit in the syntax, so the
+corresponding "Go to interfaces" operation can be achieved as sequence
+of two or more "Go to definition" steps: the first to visit the type
+declaration, and the rest to sequentially visit ancestors.
+(See https://github.com/microsoft/language-server-protocol/issues/2037.)
+
+In Go, where there is no syntactic relationship between two types, a
+search is required when navigating in either direction between
+subtypes and supertypes. The heuristic above works well in many cases,
+but it is not possible to ask for the superinterfaces of
+`io.ReadCloser`. For more explicit navigation between subtypes and
+supertypes, use the [Type Hierarchy](#Type Hierarchy) feature.
+
Only non-trivial interfaces are considered; no implementations are
reported for type `any`.
@@ -111,6 +136,17 @@ types with methods due to embedding) may be missing from the results.
but that is not consistent with the "scalable" gopls design.
-->
+Functions, `func` types, and dynamic function calls are matched using signatures:
+
+- When invoked on the `func` token of a **function definition**,
+ it returns the locations of the matching signature types
+ and dynamic call expressions.
+- When invoked on the `func` token of a **signature type**,
+ it returns the locations of the matching concrete function definitions.
+- When invoked on the `(` token of a **dynamic function call**,
+ it returns the locations of the matching concrete function
+ definitions.
+
If either the target type or the candidate type are generic, the
results will include the candidate type if there is any instantiation
of the two types that would allow one to implement the other.
@@ -120,6 +156,12 @@ types, without regard to consistency of substitutions across the
method set or even within a single method.
This may lead to occasional spurious matches.)
+Since a type may be both a function type and a named type with methods
+(for example, `http.HandlerFunc`), it may participate in both kinds of
+implementation queries (by method-sets and function signatures).
+Queries using method-sets should be invoked on the type or method name,
+and queries using signatures should be invoked on a `func` or `(` token.
+
Client support:
- **VS Code**: Use [Go to Implementations](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-implementation) (`⌘F12`).
- **Emacs + eglot**: Use `M-x eglot-find-implementation`.
@@ -254,11 +296,40 @@ of `fmt.Stringer` through the guts of `fmt.Sprint:`
-Caveats:
-- In some cases dynamic function calls are (erroneously) included in
- the output; see golang/go#68153.
-
Client support:
- **VS Code**: `Show Call Hierarchy` menu item (`⌥⇧H`) opens [Call hierarchy view](https://code.visualstudio.com/docs/cpp/cpp-ide#_call-hierarchy) (note: docs refer to C++ but the idea is the same for Go).
- **Emacs + eglot**: Not standard; install with `(package-vc-install "https://github.com/dolmens/eglot-hierarchy")`. Use `M-x eglot-hierarchy-call-hierarchy` to show the direct incoming calls to the selected function; use a prefix argument (`C-u`) to show the direct outgoing calls. There is no way to expand the tree.
- **CLI**: `gopls call_hierarchy file.go:#offset` shows outgoing and incoming calls.
+
+
+## Type Hierarchy
+
+The LSP TypeHierarchy mechanism consists of three queries that
+together enable clients to present a hierarchical view of a portion of
+the subtyping relation over named types.
+
+- [`textDocument/prepareTypeHierarchy`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#textDocument_prepareTypeHierarchy) returns an [item](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#typeHierarchyItem) describing the named type at the current position;
+- [`typeHierarchyItem/subtypes`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#typeHierarchy_subtypes) returns the set of subtypes of the selected (interface) type; and
+- [`typeHierarchy/supertypes`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#typeHierarchy_supertypes) returns the set of supertypes (interface types) of the selected type.
+
+Invoke the command while selecting the name of a type.
+
+As with an Implementation query, a type hierarchy query reports
+function-local types only within the same package as the query type.
+Also the result does not include alias types, only defined types.
+
+
+
+
+
+Caveats:
+
+- The type hierarchy supports only named types and their assignability
+ relation. By contrast, the Implementations request also reports the
+ relation between unnamed `func` types and function declarations,
+ function literals, and dynamic calls of values of those types.
+
+Client support:
+- **VS Code**: `Show Type Hierarchy` menu item opens [Type hierarchy view](https://code.visualstudio.com/docs/java/java-editing#_type-hierarchy) (note: docs refer to Java but the idea is the same for Go).
+- **Emacs + eglot**: Support added in March 2025. Use `M-x eglot-show-call-hierarchy`.
+- **CLI**: not yet supported.
diff --git a/gopls/doc/features/transformation.md b/gopls/doc/features/transformation.md
index caf13221cfa..91b6c46b74d 100644
--- a/gopls/doc/features/transformation.md
+++ b/gopls/doc/features/transformation.md
@@ -79,15 +79,17 @@ Gopls supports the following code actions:
- [`refactor.extract.variable`](#extract)
- [`refactor.extract.variable-all`](#extract)
- [`refactor.inline.call`](#refactor.inline.call)
+- [`refactor.rewrite.addTags`](#refactor.rewrite.addTags)
- [`refactor.rewrite.changeQuote`](#refactor.rewrite.changeQuote)
- [`refactor.rewrite.fillStruct`](#refactor.rewrite.fillStruct)
- [`refactor.rewrite.fillSwitch`](#refactor.rewrite.fillSwitch)
- [`refactor.rewrite.invertIf`](#refactor.rewrite.invertIf)
- [`refactor.rewrite.joinLines`](#refactor.rewrite.joinLines)
-- [`refactor.rewrite.removeUnusedParam`](#refactor.rewrite.removeUnusedParam)
-- [`refactor.rewrite.splitLines`](#refactor.rewrite.splitLines)
- [`refactor.rewrite.moveParamLeft`](#refactor.rewrite.moveParamLeft)
- [`refactor.rewrite.moveParamRight`](#refactor.rewrite.moveParamRight)
+- [`refactor.rewrite.removeTags`](#refactor.rewrite.removeTags)
+- [`refactor.rewrite.removeUnusedParam`](#refactor.rewrite.removeUnusedParam)
+- [`refactor.rewrite.splitLines`](#refactor.rewrite.splitLines)
Gopls reports some code actions twice, with two different kinds, so
that they appear in multiple UI elements: simplifications,
@@ -315,11 +317,30 @@ Similar problems may arise with packages that use reflection, such as
`encoding/json` or `text/template`. There is no substitute for good
judgment and testing.
+Special cases:
+
+- When renaming the declaration of a method receiver, the tool also
+ attempts to rename the receivers of all other methods associated
+ with the same named type. Each other receiver that cannot be fully
+ renamed is quietly skipped. Renaming any _use_ of a receiver affects
+ only that variable.
+
+ ```go
+ type Counter struct { x int }
+
+ Rename here to affect only this method
+ ↓
+ func (c *Counter) Inc() { c.x++ }
+ func (c *Counter) Dec() { c.x++ }
+ ↑
+ Rename here to affect all methods
+ ```
+
+- Renaming a package declaration additionally causes the package's
+ directory to be renamed.
+
Some tips for best results:
-- There is currently no special support for renaming all receivers of
- a family of methods at once, so you will need to rename one receiver
- one at a time (golang/go#41892).
- The safety checks performed by the Rename algorithm require type
information. If the program is grossly malformed, there may be
insufficient information for it to run (golang/go#41870),
@@ -814,3 +835,27 @@ which HTML documents are composed:


+
+
+
+### `refactor.rewrite.eliminateDotImport`: Eliminate dot import
+
+When the cursor is on a dot import gopls can offer the "Eliminate dot import"
+code action, which removes the dot from the import and qualifies uses of the
+package throughout the file. This code action is offered only if
+each use of the package can be qualified without collisions with existing names.
+
+
+### `refactor.rewrite.addTags`: Add struct tags
+
+When the cursor is within a struct, this code action adds to each field a `json`
+struct tag that specifies its JSON name, using lower case with underscores
+(e.g. LinkTarget becomes link_target). For a highlighted selection, it only
+adds tags on selected fields.
+
+
+### `refactor.rewrite.removeTags`: Remove struct tags
+
+When the cursor is within a struct, this code action clears struct tags on
+all struct fields. For a highlighted selection, it removes tags from only
+the selected fields.
diff --git a/gopls/doc/release/v0.18.0.md b/gopls/doc/release/v0.18.0.md
index 8d641a2104f..9aa0f9c9d07 100644
--- a/gopls/doc/release/v0.18.0.md
+++ b/gopls/doc/release/v0.18.0.md
@@ -37,16 +37,22 @@ details to be reported as diagnostics. For example, it indicates which
variables escape to the heap, and which array accesses require bounds
checks.
+TODO: add links to the complete manual for each item.
+
## New `modernize` analyzer
Gopls now reports when code could be simplified or clarified by
using more modern features of Go, and provides a quick fix to apply
the change.
-Examples:
+For example, a conditional assignment using an if/else statement may
+be replaced by a call to the `min` or `max` built-in functions added
+in Go 1.18.
-- replacement of conditional assignment using an if/else statement by
- a call to the `min` or `max` built-in functions added in Go 1.18;
+Use this command to apply modernization fixes en masse:
+```
+$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...
+```
## New `unusedfunc` analyzer
@@ -97,6 +103,12 @@ const Ptr = Pointer
```
gopls will suggest replacing `Ptr` in your code with `Pointer`.
+Use this command to apply such fixes en masse:
+
+```
+$ go run golang.org/x/tools/gopls/internal/analysis/gofix/cmd/gofix@latest -test -fix ./...
+```
+
## "Implementations" supports generics
At long last, the "Go to Implementations" feature now fully supports
diff --git a/gopls/doc/release/v0.19.0.md b/gopls/doc/release/v0.19.0.md
new file mode 100644
index 00000000000..b8f53a72304
--- /dev/null
+++ b/gopls/doc/release/v0.19.0.md
@@ -0,0 +1,138 @@
+# Configuration Changes
+
+- The `gopls check` subcommant now accepts a `-severity` flag to set a minimum
+ severity for the diagnostics it reports. By default, the minimum severity
+ is "warning", so `gopls check` may report fewer diagnostics than before. Set
+ `-severity=hint` to reproduce the previous behavior.
+
+# New features
+
+## "Rename" of method receivers
+
+The Rename operation, when applied to the declaration of a method
+receiver, now also attempts to rename the receivers of all other
+methods associated with the same named type. Each other receiver that
+cannot be fully renamed is quietly skipped.
+
+Renaming a _use_ of a method receiver continues to affect only that
+variable.
+
+```go
+type Counter struct { x int }
+
+ Rename here to affect only this method
+ ↓
+func (c *Counter) Inc() { c.x++ }
+func (c *Counter) Dec() { c.x++ }
+ ↑
+ Rename here to affect all methods
+```
+
+## Many `staticcheck` analyzers are enabled by default
+
+Slightly more than half of the analyzers in the
+[Staticcheck](https://staticcheck.dev/docs/checks) suite are now
+enabled by default. This subset has been chosen for precision and
+efficiency.
+
+Prevously, Staticcheck analyzers (all of them) would be run only if
+the experimental `staticcheck` boolean option was set to `true`. This
+value continues to enable the complete set, and a value of `false`
+continues to disable the complete set. Leaving the option unspecified
+enables the preferred subset of analyzers.
+
+Staticcheck analyzers, like all other analyzers, can be explicitly
+enabled or disabled using the `analyzers` configuration setting; this
+setting now takes precedence over the `staticcheck` setting, so,
+regardless of what value of `staticcheck` you use (true/false/unset),
+you can make adjustments to your preferred set of analyzers.
+
+
+## "Implementations" supports signature types
+
+The Implementations query reports the correspondence between abstract
+and concrete types and their methods based on their method sets.
+Now, it also reports the correspondence between function types,
+dynamic function calls, and function definitions, based on their signatures.
+
+To use it, invoke an Implementations query on the `func` token of the
+definition of a named function, named method, or function literal.
+Gopls reports the set of function signature types that abstract this
+function, and the set of dynamic calls through values of such types.
+
+Conversely, an Implementations query on the `func` token of a
+signature type, or on the `(` paren of a dynamic function call,
+reports the set of concrete functions that the signature abstracts
+or that the call dispatches to.
+
+Since a type may be both a function type and a named type with methods
+(for example, `http.HandlerFunc`), it may participate in both kinds of
+Implements queries (method-sets and function signatures).
+Queries using method-sets should be invoked on the type or method name,
+and queries using signatures should be invoked on a `func` or `(` token.
+
+Only the local (same-package) algorithm is currently supported.
+TODO: implement global.
+
+## Go to Implementation
+
+The "Go to Implementation" operation now reports relationships between
+interfaces. Gopls now uses the concreteness of the query type to
+determine whether a query is "downwards" (from an interface to the
+types that implement it) or "upwards" (from a concrete type to the
+interfaces to which it may be assigned). So, for example:
+
+- `implementation(io.Reader)` subinterfaces such as `io.ReadCloser`,
+ and concrete implementations such as `*os.File`.
+
+- `implementation(os.File)` includes only interfaces, such as
+ `io.Reader` and `io.ReadCloser`.
+
+To request an "upwards" query starting from an interface, for example
+to find the superinterfaces of `io.ReadCloser`, use the Type Hierarchy
+feature described below.
+(See https://github.com/microsoft/language-server-protocol/issues/2037.)
+
+## Support for Type Hierarchy
+
+
+
+Gopls now implements the three LSP methods related to the Type
+Hierarchy viewer: `textDocument/prepareTypeHierarchy`,
+`typeHierarchy/supertypes`, `typeHierarchy/subtypes`.
+
+In VS Code, select "Show Type Hierarchy" from the context menu
+to see a tree widget displaying all the supertypes or subtypes
+of the selected named type.
+
+
+
+
+
+## "Eliminate dot import" code action
+
+This code action, available on a dotted import, will offer to replace
+the import with a regular one and qualify each use of the package
+with its name.
+
+### Auto-complete package clause for new Go files
+
+Gopls now automatically adds the appropriate `package` clause to newly created Go files,
+so that you can immediately get started writing the interesting part.
+
+It requires client support for `workspace/didCreateFiles`
+
+## Add/remove tags from struct fields
+
+Gopls now provides two new code actions, available on an entire struct
+or some of its fields, that allow you to add and remove struct tags.
+It adds only 'json' tags with a snakecase naming format, or clears all
+tags within the selection.
+
+Add tags example:
+```go
+type Info struct {
+ LinkTarget string -> LinkTarget string `json:"link_target"`
+ ...
+}
+```
\ No newline at end of file
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index d989b2d19b9..00415bb36f4 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -349,12 +349,54 @@ Default: `{}`.
**This setting is experimental and may be deleted.**
-staticcheck enables additional analyses from staticcheck.io.
+staticcheck configures the default set of analyses staticcheck.io.
These analyses are documented on
[Staticcheck's website](https://staticcheck.io/docs/checks/).
+The "staticcheck" option has three values:
+- false: disable all staticcheck analyzers
+- true: enable all staticcheck analyzers
+- unset: enable a subset of staticcheck analyzers
+ selected by gopls maintainers for runtime efficiency
+ and analytic precision.
+
+Regardless of this setting, individual analyzers can be
+selectively enabled or disabled using the `analyses` setting.
+
+Default: `false`.
+
+
+### `staticcheckProvided bool`
+
+**This setting is experimental and may be deleted.**
+
+
Default: `false`.
+
+### `annotations map[enum]bool`
+
+annotations specifies the various kinds of compiler
+optimization details that should be reported as diagnostics
+when enabled for a package by the "Toggle compiler
+optimization details" (`gopls.gc_details`) command.
+
+(Some users care only about one kind of annotation in their
+profiling efforts. More importantly, in large packages, the
+number of annotations can sometimes overwhelm the user
+interface and exceed the per-file diagnostic limit.)
+
+TODO(adonovan): rename this field to CompilerOptDetail.
+
+Each enum must be one of:
+
+* `"bounds"` controls bounds checking diagnostics.
+* `"escape"` controls diagnostics about escape choices.
+* `"inline"` controls diagnostics about inlining choices.
+* `"nil"` controls nil checks.
+
+Default: `{"bounds":true,"escape":true,"inline":true,"nil":true}`.
+
### `vulncheck enum`
@@ -428,6 +470,9 @@ Must be one of:
* `"FullDocumentation"`
* `"NoDocumentation"`
* `"SingleLine"`
+* `"Structured"` is a misguided experimental setting that returns a JSON
+hover format. This setting should not be used, as it will be removed in a
+future release of gopls.
* `"SynopsisDocumentation"`
Default: `"FullDocumentation"`.
diff --git a/gopls/doc/vim.md b/gopls/doc/vim.md
index e71482115ea..eedac5925f4 100644
--- a/gopls/doc/vim.md
+++ b/gopls/doc/vim.md
@@ -56,7 +56,7 @@ Use [prabirshrestha/vim-lsp], with the following configuration:
augroup LspGo
au!
autocmd User lsp_setup call lsp#register_server({
- \ 'name': 'go-lang',
+ \ 'name': 'gopls',
\ 'cmd': {server_info->['gopls']},
\ 'whitelist': ['go'],
\ })
@@ -230,5 +230,5 @@ require('lspconfig').gopls.setup({
[govim-install]: https://github.com/myitcv/govim/blob/master/README.md#govim---go-development-plugin-for-vim8
[nvim-docs]: https://neovim.io/doc/user/lsp.html
[nvim-install]: https://github.com/neovim/neovim/wiki/Installing-Neovim
-[nvim-lspconfig]: https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#gopls
+[nvim-lspconfig]: https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#gopls
[nvim-lspconfig-imports]: https://github.com/neovim/nvim-lspconfig/issues/115
diff --git a/gopls/go.mod b/gopls/go.mod
index 83620720ae6..96c3fbb127a 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -1,29 +1,30 @@
module golang.org/x/tools/gopls
-// go 1.23.1 fixes some bugs in go/types Alias support (golang/go#68894, golang/go#68905).
-// go 1.23.4 fixes a miscompilation of range-over-func (golang/go#70035).
-go 1.23.4
+go 1.24.2
require (
+ github.com/fatih/gomodifytags v1.17.1-0.20250423142747-f3939df9aa3c
github.com/google/go-cmp v0.6.0
github.com/jba/templatecheck v0.7.1
- golang.org/x/mod v0.23.0
- golang.org/x/sync v0.11.0
- golang.org/x/sys v0.30.0
- golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9
- golang.org/x/text v0.22.0
- golang.org/x/tools v0.28.0
- golang.org/x/vuln v1.1.3
+ golang.org/x/mod v0.24.0
+ golang.org/x/sync v0.14.0
+ golang.org/x/sys v0.33.0
+ golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3
+ golang.org/x/text v0.25.0
+ golang.org/x/tools v0.30.0
+ golang.org/x/vuln v1.1.4
gopkg.in/yaml.v3 v3.0.1
- honnef.co/go/tools v0.5.1
+ honnef.co/go/tools v0.6.0
mvdan.cc/gofumpt v0.7.0
- mvdan.cc/xurls/v2 v2.5.0
+ mvdan.cc/xurls/v2 v2.6.0
)
require (
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
+ github.com/fatih/camelcase v1.0.0 // indirect
+ github.com/fatih/structtag v1.2.0 // indirect
github.com/google/safehtml v0.1.0 // indirect
- golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 // indirect
+ golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)
diff --git a/gopls/go.sum b/gopls/go.sum
index b2b3d925a78..27f999d51a4 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -1,5 +1,11 @@
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
+github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
+github.com/fatih/gomodifytags v1.17.1-0.20250423142747-f3939df9aa3c h1:dDSgAjoOMp8da3egfz0t2S+t8RGOpEmEXZubcGuc0Bg=
+github.com/fatih/gomodifytags v1.17.1-0.20250423142747-f3939df9aa3c/go.mod h1:YVLagR57bBxMai8IAEc7V4E/MWUYi0oUutLrZcTcnI8=
+github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -12,50 +18,50 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
+github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
-golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 h1:1xaZTydL5Gsg78QharTwKfA9FY9CZ1VQj6D/AZEvHR0=
-golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa h1:Br3+0EZZohShrmVVc85znGpxw7Ca8hsUJlrdT/JQGw8=
+golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
-golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
-golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9 h1:L2k9GUV2TpQKVRGMjN94qfUMgUwOFimSQ6gipyJIjKw=
-golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9/go.mod h1:8h4Hgq+jcTvCDv2+i7NrfWwpYHcESleo2nGHxLbFLJ4=
+golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 h1:RXY2+rSHXvxO2Y+gKrPjYVaEoGOqh3VEXFhnWAt1Irg=
+golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3/go.mod h1:RoaXAWDwS90j6FxVKwJdBV+0HCU+llrKUGgJaxiKl6M=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/vuln v1.1.3 h1:NPGnvPOTgnjBc9HTaUx+nj+EaUYxl5SJOWqaDYGaFYw=
-golang.org/x/vuln v1.1.3/go.mod h1:7Le6Fadm5FOqE9C926BCD0g12NWyhg7cxV4BwcPFuNY=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
+golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
-honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs=
+honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8=
+honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
-mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
-mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
+mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
+mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
diff --git a/gopls/internal/analysis/deprecated/deprecated.go b/gopls/internal/analysis/deprecated/deprecated.go
index c6df00b4f50..400041ba088 100644
--- a/gopls/internal/analysis/deprecated/deprecated.go
+++ b/gopls/internal/analysis/deprecated/deprecated.go
@@ -36,7 +36,7 @@ var Analyzer = &analysis.Analyzer{
}
// checkDeprecated is a simplified copy of staticcheck.CheckDeprecated.
-func checkDeprecated(pass *analysis.Pass) (interface{}, error) {
+func checkDeprecated(pass *analysis.Pass) (any, error) {
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
deprs, err := collectDeprecatedNames(pass, inspector)
diff --git a/gopls/internal/analysis/embeddirective/embeddirective.go b/gopls/internal/analysis/embeddirective/embeddirective.go
index e623587cc68..7590cba9ad8 100644
--- a/gopls/internal/analysis/embeddirective/embeddirective.go
+++ b/gopls/internal/analysis/embeddirective/embeddirective.go
@@ -28,7 +28,7 @@ var Analyzer = &analysis.Analyzer{
const FixCategory = "addembedimport" // recognized by gopls ApplyFix
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
for _, f := range pass.Files {
comments := embedDirectiveComments(f)
if len(comments) == 0 {
diff --git a/gopls/internal/analysis/fillreturns/fillreturns.go b/gopls/internal/analysis/fillreturns/fillreturns.go
index 8a602dc2eef..b2cc1caf872 100644
--- a/gopls/internal/analysis/fillreturns/fillreturns.go
+++ b/gopls/internal/analysis/fillreturns/fillreturns.go
@@ -12,12 +12,16 @@ import (
"go/format"
"go/types"
"regexp"
+ "slices"
"strings"
"golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/gopls/internal/fuzzy"
+ "golang.org/x/tools/gopls/internal/util/moreiters"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/typesinternal"
)
@@ -27,105 +31,38 @@ var doc string
var Analyzer = &analysis.Analyzer{
Name: "fillreturns",
Doc: analysisinternal.MustExtractDoc(doc, "fillreturns"),
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
RunDespiteErrors: true,
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns",
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
info := pass.TypesInfo
- if info == nil {
- return nil, fmt.Errorf("nil TypeInfo")
- }
outer:
for _, typeErr := range pass.TypeErrors {
- // Filter out the errors that are not relevant to this analyzer.
- if !FixesError(typeErr) {
- continue
- }
- var file *ast.File
- for _, f := range pass.Files {
- if f.FileStart <= typeErr.Pos && typeErr.Pos <= f.FileEnd {
- file = f
- break
- }
- }
- if file == nil {
- continue
- }
-
- // Get the end position of the error.
- // (This heuristic assumes that the buffer is formatted,
- // at least up to the end position of the error.)
- var buf bytes.Buffer
- if err := format.Node(&buf, pass.Fset, file); err != nil {
- continue
- }
- typeErrEndPos := analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), typeErr.Pos)
-
- // TODO(rfindley): much of the error handling code below returns, when it
- // should probably continue.
-
- // Get the path for the relevant range.
- path, _ := astutil.PathEnclosingInterval(file, typeErr.Pos, typeErrEndPos)
- if len(path) == 0 {
- return nil, nil
- }
-
- // Find the enclosing return statement.
- var ret *ast.ReturnStmt
- var retIdx int
- for i, n := range path {
- if r, ok := n.(*ast.ReturnStmt); ok {
- ret = r
- retIdx = i
- break
- }
- }
- if ret == nil {
- return nil, nil
- }
-
- // Get the function type that encloses the ReturnStmt.
- var enclosingFunc *ast.FuncType
- for _, n := range path[retIdx+1:] {
- switch node := n.(type) {
- case *ast.FuncLit:
- enclosingFunc = node.Type
- case *ast.FuncDecl:
- enclosingFunc = node.Type
- }
- if enclosingFunc != nil {
- break
- }
+ if !fixesError(typeErr) {
+ continue // irrelevant type error
}
- if enclosingFunc == nil || enclosingFunc.Results == nil {
- continue
+ _, start, end, ok := typesinternal.ErrorCodeStartEnd(typeErr)
+ if !ok {
+ continue // no position information
}
-
- // Skip any generic enclosing functions, since type parameters don't
- // have 0 values.
- // TODO(rfindley): We should be able to handle this if the return
- // values are all concrete types.
- if tparams := enclosingFunc.TypeParams; tparams != nil && tparams.NumFields() > 0 {
- return nil, nil
+ curErr, ok := cursor.Root(inspect).FindByPos(start, end)
+ if !ok {
+ continue // can't find node
}
- // Find the function declaration that encloses the ReturnStmt.
- var outer *ast.FuncDecl
- for _, p := range path {
- if p, ok := p.(*ast.FuncDecl); ok {
- outer = p
- break
- }
- }
- if outer == nil {
- return nil, nil
+ // Find cursor for enclosing return statement (which may be curErr itself).
+ curRet, ok := moreiters.First(curErr.Enclosing((*ast.ReturnStmt)(nil)))
+ if !ok {
+ continue // no enclosing return
}
+ ret := curRet.Node().(*ast.ReturnStmt)
- // Skip any return statements that contain function calls with multiple
- // return values.
+ // Skip if any return argument is a tuple-valued function call.
for _, expr := range ret.Results {
e, ok := expr.(*ast.CallExpr)
if !ok {
@@ -136,24 +73,47 @@ outer:
}
}
+ // Get type of innermost enclosing function.
+ var funcType *ast.FuncType
+ curFunc, _ := enclosingFunc(curRet) // can't fail
+ switch fn := curFunc.Node().(type) {
+ case *ast.FuncLit:
+ funcType = fn.Type
+ case *ast.FuncDecl:
+ funcType = fn.Type
+
+ // Skip generic functions since type parameters don't have zero values.
+ // TODO(rfindley): We should be able to handle this if the return
+ // values are all concrete types.
+ if funcType.TypeParams.NumFields() > 0 {
+ continue
+ }
+ }
+ if funcType.Results == nil {
+ continue
+ }
+
// Duplicate the return values to track which values have been matched.
remaining := make([]ast.Expr, len(ret.Results))
copy(remaining, ret.Results)
- fixed := make([]ast.Expr, len(enclosingFunc.Results.List))
+ fixed := make([]ast.Expr, len(funcType.Results.List))
// For each value in the return function declaration, find the leftmost element
// in the return statement that has the desired type. If no such element exists,
// fill in the missing value with the appropriate "zero" value.
// Beware that type information may be incomplete.
var retTyps []types.Type
- for _, ret := range enclosingFunc.Results.List {
+ for _, ret := range funcType.Results.List {
retTyp := info.TypeOf(ret.Type)
if retTyp == nil {
return nil, nil
}
retTyps = append(retTyps, retTyp)
}
+
+ curFile, _ := moreiters.First(curRet.Enclosing((*ast.File)(nil)))
+ file := curFile.Node().(*ast.File)
matches := analysisinternal.MatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg)
qual := typesinternal.FileQualifier(file, pass.Pkg)
for i, retTyp := range retTyps {
@@ -175,7 +135,7 @@ outer:
if match != nil {
fixed[i] = match
- remaining = append(remaining[:idx], remaining[idx+1:]...)
+ remaining = slices.Delete(remaining, idx, idx+1)
} else {
names, ok := matches[retTyp]
if !ok {
@@ -215,8 +175,8 @@ outer:
}
pass.Report(analysis.Diagnostic{
- Pos: typeErr.Pos,
- End: typeErrEndPos,
+ Pos: start,
+ End: end,
Message: typeErr.Msg,
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Fill in return values",
@@ -255,7 +215,7 @@ var wrongReturnNumRegexes = []*regexp.Regexp{
regexp.MustCompile(`not enough return values`),
}
-func FixesError(err types.Error) bool {
+func fixesError(err types.Error) bool {
msg := strings.TrimSpace(err.Msg)
for _, rx := range wrongReturnNumRegexes {
if rx.MatchString(msg) {
@@ -264,3 +224,9 @@ func FixesError(err types.Error) bool {
}
return false
}
+
+// enclosingFunc returns the cursor for the innermost Func{Decl,Lit}
+// that encloses c, if any.
+func enclosingFunc(c cursor.Cursor) (cursor.Cursor, bool) {
+ return moreiters.First(c.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)))
+}
diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go
index a8a861f0651..5a18da9a221 100644
--- a/gopls/internal/analysis/fillstruct/fillstruct.go
+++ b/gopls/internal/analysis/fillstruct/fillstruct.go
@@ -25,6 +25,8 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/fuzzy"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/analysisinternal"
@@ -74,7 +76,7 @@ func Diagnose(f *ast.File, start, end token.Pos, pkg *types.Package, info *types
// Are any fields in need of filling?
var fillableFields []string
- for i := 0; i < fieldCount; i++ {
+ for i := range fieldCount {
field := tStruct.Field(i)
// Ignore fields that are not accessible in the current package.
if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() {
@@ -129,15 +131,15 @@ const FixCategory = "fillstruct" // recognized by gopls ApplyFix
// SuggestedFix computes the suggested fix for the kinds of
// diagnostics produced by the Analyzer above.
-func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- if info == nil {
- return nil, nil, fmt.Errorf("nil types.Info")
- }
-
- pos := start // don't use the end
-
- // TODO(rstambler): Using ast.Inspect would probably be more efficient than
- // calling PathEnclosingInterval. Switch this approach.
+func SuggestedFix(cpkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ var (
+ fset = cpkg.FileSet()
+ pkg = cpkg.Types()
+ info = cpkg.TypesInfo()
+ pos = start // don't use end
+ )
+ // TODO(adonovan): simplify, using Cursor.
+ file := pgf.Cursor.Node().(*ast.File)
path, _ := astutil.PathEnclosingInterval(file, pos, pos)
if len(path) == 0 {
return nil, nil, fmt.Errorf("no enclosing ast.Node")
@@ -180,7 +182,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil
}
var fieldTyps []types.Type
- for i := 0; i < fieldCount; i++ {
+ for i := range fieldCount {
field := tStruct.Field(i)
// Ignore fields that are not accessible in the current package.
if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() {
@@ -234,7 +236,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil
}
// Find the line on which the composite literal is declared.
- split := bytes.Split(content, []byte("\n"))
+ split := bytes.Split(pgf.Src, []byte("\n"))
lineNumber := safetoken.StartPosition(fset, expr.Lbrace).Line
firstLine := split[lineNumber-1] // lines are 1-indexed
diff --git a/gopls/internal/analysis/gofix/directive.go b/gopls/internal/analysis/gofix/directive.go
deleted file mode 100644
index 796feb5189e..00000000000
--- a/gopls/internal/analysis/gofix/directive.go
+++ /dev/null
@@ -1,95 +0,0 @@
-// 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 gofix
-
-import (
- "go/ast"
- "go/token"
- "strings"
-)
-
-// -- plundered from the future (CL 605517, issue #68021) --
-
-// TODO(adonovan): replace with ast.Directive after go1.24 (#68021).
-// Beware of our local mods to handle analysistest
-// "want" comments on the same line.
-
-// A directive is a comment line with special meaning to the Go
-// toolchain or another tool. It has the form:
-//
-// //tool:name args
-//
-// The "tool:" portion is missing for the three directives named
-// line, extern, and export.
-//
-// See https://go.dev/doc/comment#Syntax for details of Go comment
-// syntax and https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives
-// for details of directives used by the Go compiler.
-type directive struct {
- Pos token.Pos // of preceding "//"
- Tool string
- Name string
- Args string // may contain internal spaces
-}
-
-// directives returns the directives within the comment.
-func directives(g *ast.CommentGroup) (res []*directive) {
- if g != nil {
- // Avoid (*ast.CommentGroup).Text() as it swallows directives.
- for _, c := range g.List {
- if len(c.Text) > 2 &&
- c.Text[1] == '/' &&
- c.Text[2] != ' ' &&
- isDirective(c.Text[2:]) {
-
- tool, nameargs, ok := strings.Cut(c.Text[2:], ":")
- if !ok {
- // Must be one of {line,extern,export}.
- tool, nameargs = "", tool
- }
- name, args, _ := strings.Cut(nameargs, " ") // tab??
- // Permit an additional line comment after the args, chiefly to support
- // [golang.org/x/tools/go/analysis/analysistest].
- args, _, _ = strings.Cut(args, "//")
- res = append(res, &directive{
- Pos: c.Slash,
- Tool: tool,
- Name: name,
- Args: strings.TrimSpace(args),
- })
- }
- }
- }
- return
-}
-
-// isDirective reports whether c is a comment directive.
-// This code is also in go/printer.
-func isDirective(c string) bool {
- // "//line " is a line directive.
- // "//extern " is for gccgo.
- // "//export " is for cgo.
- // (The // has been removed.)
- if strings.HasPrefix(c, "line ") || strings.HasPrefix(c, "extern ") || strings.HasPrefix(c, "export ") {
- return true
- }
-
- // "//[a-z0-9]+:[a-z0-9]"
- // (The // has been removed.)
- colon := strings.Index(c, ":")
- if colon <= 0 || colon+1 >= len(c) {
- return false
- }
- for i := 0; i <= colon+1; i++ {
- if i == colon {
- continue
- }
- b := c[i]
- if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') {
- return false
- }
- }
- return true
-}
diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go
deleted file mode 100644
index 101924366d6..00000000000
--- a/gopls/internal/analysis/gofix/gofix.go
+++ /dev/null
@@ -1,341 +0,0 @@
-// Copyright 2023 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 gofix
-
-import (
- "fmt"
- "go/ast"
- "go/token"
- "go/types"
-
- _ "embed"
-
- "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/gopls/internal/util/moreiters"
- "golang.org/x/tools/internal/analysisinternal"
- "golang.org/x/tools/internal/astutil/cursor"
- "golang.org/x/tools/internal/astutil/edge"
- "golang.org/x/tools/internal/diff"
- "golang.org/x/tools/internal/refactor/inline"
- "golang.org/x/tools/internal/typesinternal"
-)
-
-//go:embed doc.go
-var doc string
-
-var Analyzer = &analysis.Analyzer{
- Name: "gofix",
- Doc: analysisinternal.MustExtractDoc(doc, "gofix"),
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix",
- Run: run,
- FactTypes: []analysis.Fact{new(goFixInlineFuncFact), new(goFixInlineConstFact)},
- Requires: []*analysis.Analyzer{inspect.Analyzer},
-}
-
-func run(pass *analysis.Pass) (any, error) {
- // Memoize repeated calls for same file.
- fileContent := make(map[string][]byte)
- readFile := func(node ast.Node) ([]byte, error) {
- filename := pass.Fset.File(node.Pos()).Name()
- content, ok := fileContent[filename]
- if !ok {
- var err error
- content, err = pass.ReadFile(filename)
- if err != nil {
- return nil, err
- }
- fileContent[filename] = content
- }
- return content, nil
- }
-
- // Return the unique ast.File for a cursor.
- currentFile := func(c cursor.Cursor) *ast.File {
- cf, _ := moreiters.First(c.Ancestors((*ast.File)(nil)))
- return cf.Node().(*ast.File)
- }
-
- // Pass 1: find functions and constants annotated with an appropriate "//go:fix"
- // comment (the syntax proposed by #32816),
- // and export a fact for each one.
- inlinableFuncs := make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact)
- inlinableConsts := make(map[*types.Const]*goFixInlineConstFact)
-
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
- nodeFilter := []ast.Node{(*ast.FuncDecl)(nil), (*ast.GenDecl)(nil)}
- inspect.Preorder(nodeFilter, func(n ast.Node) {
- switch decl := n.(type) {
- case *ast.FuncDecl:
- if !hasFixInline(decl.Doc) {
- return
- }
- content, err := readFile(decl)
- if err != nil {
- pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err)
- return
- }
- callee, err := inline.AnalyzeCallee(discard, pass.Fset, pass.Pkg, pass.TypesInfo, decl, content)
- if err != nil {
- pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err)
- return
- }
- fn := pass.TypesInfo.Defs[decl.Name].(*types.Func)
- pass.ExportObjectFact(fn, &goFixInlineFuncFact{callee})
- inlinableFuncs[fn] = callee
-
- case *ast.GenDecl:
- if decl.Tok != token.CONST {
- return
- }
- declInline := hasFixInline(decl.Doc)
- // Accept inline directives on the entire decl as well as individual specs.
- for _, spec := range decl.Specs {
- spec := spec.(*ast.ValueSpec) // guaranteed by Tok == CONST
- specInline := hasFixInline(spec.Doc)
- if declInline || specInline {
- for i, name := range spec.Names {
- if i >= len(spec.Values) {
- // Possible following an iota.
- break
- }
- val := spec.Values[i]
- var rhsID *ast.Ident
- switch e := val.(type) {
- case *ast.Ident:
- // Constants defined with the predeclared iota cannot be inlined.
- if pass.TypesInfo.Uses[e] == builtinIota {
- pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota")
- continue
- }
- rhsID = e
- case *ast.SelectorExpr:
- rhsID = e.Sel
- default:
- pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant")
- continue
- }
- lhs := pass.TypesInfo.Defs[name].(*types.Const)
- rhs := pass.TypesInfo.Uses[rhsID].(*types.Const) // must be so in a well-typed program
- con := &goFixInlineConstFact{
- RHSName: rhs.Name(),
- RHSPkgName: rhs.Pkg().Name(),
- RHSPkgPath: rhs.Pkg().Path(),
- }
- if rhs.Pkg() == pass.Pkg {
- con.rhsObj = rhs
- }
- inlinableConsts[lhs] = con
- // Create a fact only if the LHS is exported and defined at top level.
- // We create a fact even if the RHS is non-exported,
- // so we can warn uses in other packages.
- if lhs.Exported() && typesinternal.IsPackageLevel(lhs) {
- pass.ExportObjectFact(lhs, con)
- }
- }
- }
- }
- }
- })
-
- // Pass 2. Inline each static call to an inlinable function
- // and each reference to an inlinable constant.
- //
- // TODO(adonovan): handle multiple diffs that each add the same import.
- for cur := range cursor.Root(inspect).Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) {
- n := cur.Node()
- switch n := n.(type) {
- case *ast.CallExpr:
- call := n
- if fn := typeutil.StaticCallee(pass.TypesInfo, call); fn != nil {
- // Inlinable?
- callee, ok := inlinableFuncs[fn]
- if !ok {
- var fact goFixInlineFuncFact
- if pass.ImportObjectFact(fn, &fact) {
- callee = fact.Callee
- inlinableFuncs[fn] = callee
- }
- }
- if callee == nil {
- continue // nope
- }
-
- // Inline the call.
- content, err := readFile(call)
- if err != nil {
- pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err)
- continue
- }
- curFile := currentFile(cur)
- caller := &inline.Caller{
- Fset: pass.Fset,
- Types: pass.Pkg,
- Info: pass.TypesInfo,
- File: curFile,
- Call: call,
- Content: content,
- }
- res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard})
- if err != nil {
- pass.Reportf(call.Lparen, "%v", err)
- continue
- }
- if res.Literalized {
- // Users are not fond of inlinings that literalize
- // f(x) to func() { ... }(), so avoid them.
- //
- // (Unfortunately the inliner is very timid,
- // and often literalizes when it cannot prove that
- // reducing the call is safe; the user of this tool
- // has no indication of what the problem is.)
- continue
- }
- got := res.Content
-
- // Suggest the "fix".
- var textEdits []analysis.TextEdit
- for _, edit := range diff.Bytes(content, got) {
- textEdits = append(textEdits, analysis.TextEdit{
- Pos: curFile.FileStart + token.Pos(edit.Start),
- End: curFile.FileStart + token.Pos(edit.End),
- NewText: []byte(edit.New),
- })
- }
- pass.Report(analysis.Diagnostic{
- Pos: call.Pos(),
- End: call.End(),
- Message: fmt.Sprintf("Call of %v should be inlined", callee),
- SuggestedFixes: []analysis.SuggestedFix{{
- Message: fmt.Sprintf("Inline call of %v", callee),
- TextEdits: textEdits,
- }},
- })
- }
-
- case *ast.Ident:
- // If the identifier is a use of an inlinable constant, suggest inlining it.
- if con, ok := pass.TypesInfo.Uses[n].(*types.Const); ok {
- fcon, ok := inlinableConsts[con]
- if !ok {
- var fact goFixInlineConstFact
- if pass.ImportObjectFact(con, &fact) {
- fcon = &fact
- inlinableConsts[con] = fcon
- }
- }
- if fcon == nil {
- continue // nope
- }
-
- // If n is qualified by a package identifier, we'll need the full selector expression.
- var sel *ast.SelectorExpr
- if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel {
- sel = cur.Parent().Node().(*ast.SelectorExpr)
- }
- curFile := currentFile(cur)
-
- // We have an identifier A here (n), possibly qualified by a package identifier (sel.X),
- // and an inlinable "const A = B" elsewhere (fcon).
- // Consider replacing A with B.
-
- // Check that the expression we are inlining (B) means the same thing
- // (refers to the same object) in n's scope as it does in A's scope.
- // If the RHS is not in the current package, AddImport will handle
- // shadowing, so we only need to worry about when both expressions
- // are in the current package.
- if pass.Pkg.Path() == fcon.RHSPkgPath {
- // fcon.rhsObj is the object referred to by B in the definition of A.
- scope := pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope
- _, obj := scope.LookupParent(fcon.RHSName, n.Pos()) // what "B" means in n's scope
- if obj == nil {
- // Should be impossible: if code at n can refer to the LHS,
- // it can refer to the RHS.
- panic(fmt.Sprintf("no object for inlinable const %s RHS %s", n.Name, fcon.RHSName))
- }
- if obj != fcon.rhsObj {
- // "B" means something different here than at the inlinable const's scope.
- continue
- }
- }
- var (
- importPrefix string
- edits []analysis.TextEdit
- )
- if fcon.RHSPkgPath != pass.Pkg.Path() {
- _, importPrefix, edits = analysisinternal.AddImport(
- pass.TypesInfo, curFile, fcon.RHSPkgName, fcon.RHSPkgPath, fcon.RHSName, n.Pos())
- }
- var (
- pos = n.Pos()
- end = n.End()
- name = n.Name
- )
- // Replace the entire SelectorExpr if there is one.
- if sel != nil {
- pos = sel.Pos()
- end = sel.End()
- name = sel.X.(*ast.Ident).Name + "." + n.Name
- }
- edits = append(edits, analysis.TextEdit{
- Pos: pos,
- End: end,
- NewText: []byte(importPrefix + fcon.RHSName),
- })
- pass.Report(analysis.Diagnostic{
- Pos: pos,
- End: end,
- Message: fmt.Sprintf("Constant %s should be inlined", name),
- SuggestedFixes: []analysis.SuggestedFix{{
- Message: fmt.Sprintf("Inline constant %s", name),
- TextEdits: edits,
- }},
- })
- }
- }
- }
-
- return nil, nil
-}
-
-// hasFixInline reports the presence of a "//go:fix inline" directive
-// in the comments.
-func hasFixInline(cg *ast.CommentGroup) bool {
- for _, d := range directives(cg) {
- if d.Tool == "go" && d.Name == "fix" && d.Args == "inline" {
- return true
- }
- }
- return false
-}
-
-// A goFixInlineFuncFact is exported for each function marked "//go:fix inline".
-// It holds information about the callee to support inlining.
-type goFixInlineFuncFact struct{ Callee *inline.Callee }
-
-func (f *goFixInlineFuncFact) String() string { return "goFixInline " + f.Callee.String() }
-func (*goFixInlineFuncFact) AFact() {}
-
-// A goFixInlineConstFact is exported for each constant marked "//go:fix inline".
-// It holds information about an inlinable constant. Gob-serializable.
-type goFixInlineConstFact struct {
- // Information about "const LHSName = RHSName".
- RHSName string
- RHSPkgPath string
- RHSPkgName string
- rhsObj types.Object // for current package
-}
-
-func (c *goFixInlineConstFact) String() string {
- return fmt.Sprintf("goFixInline const %q.%s", c.RHSPkgPath, c.RHSName)
-}
-
-func (*goFixInlineConstFact) AFact() {}
-
-func discard(string, ...any) {}
-
-var builtinIota = types.Universe.Lookup("iota")
diff --git a/gopls/internal/analysis/gofix/gofix_test.go b/gopls/internal/analysis/gofix/gofix_test.go
deleted file mode 100644
index 32bd87b6cd2..00000000000
--- a/gopls/internal/analysis/gofix/gofix_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2018 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 gofix_test
-
-import (
- "testing"
-
- "golang.org/x/tools/go/analysis/analysistest"
- "golang.org/x/tools/gopls/internal/analysis/gofix"
-)
-
-func TestAnalyzer(t *testing.T) {
- analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), gofix.Analyzer, "a", "b")
-}
diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go b/gopls/internal/analysis/gofix/testdata/src/a/a.go
deleted file mode 100644
index ae486746e5b..00000000000
--- a/gopls/internal/analysis/gofix/testdata/src/a/a.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package a
-
-// Functions.
-
-func f() {
- One() // want `Call of a.One should be inlined`
-
- new(T).Two() // want `Call of \(a.T\).Two should be inlined`
-}
-
-type T struct{}
-
-//go:fix inline
-func One() int { return one } // want One:`goFixInline a.One`
-
-const one = 1
-
-//go:fix inline
-func (T) Two() int { return 2 } // want Two:`goFixInline \(a.T\).Two`
-
-// Constants.
-
-const Uno = 1
-
-//go:fix inline
-const In1 = Uno // want In1: `goFixInline const "a".Uno`
-
-const (
- no1 = one
-
- //go:fix inline
- In2 = one // want In2: `goFixInline const "a".one`
-)
-
-//go:fix inline
-const (
- in3 = one
- in4 = one
- bad1 = 1 // want `invalid //go:fix inline directive: const value is not the name of another constant`
-)
-
-//go:fix inline
-const in5,
- in6,
- bad2 = one, one,
- one + 1 // want `invalid //go:fix inline directive: const value is not the name of another constant`
-
-// Make sure we don't crash on iota consts, but still process the whole decl.
-//
-//go:fix inline
-const (
- a = iota // want `invalid //go:fix inline directive: const value is iota`
- b
- in7 = one
-)
-
-func _() {
- x := In1 // want `Constant In1 should be inlined`
- x = In2 // want `Constant In2 should be inlined`
- x = in3 // want `Constant in3 should be inlined`
- x = in4 // want `Constant in4 should be inlined`
- x = in5 // want `Constant in5 should be inlined`
- x = in6 // want `Constant in6 should be inlined`
- x = in7 // want `Constant in7 should be inlined`
- x = no1
- _ = x
-
- in1 := 1 // don't inline lvalues
- _ = in1
-}
-
-const (
- x = 1
- //go:fix inline
- in8 = x
-)
-
-func shadow() {
- var x int // shadows x at package scope
-
- //go:fix inline
- const a = iota // want `invalid //go:fix inline directive: const value is iota`
-
- const iota = 2
- // Below this point, iota is an ordinary constant.
-
- //go:fix inline
- const b = iota
-
- x = a // a is defined with the predeclared iota, so it cannot be inlined
- x = b // want `Constant b should be inlined`
-
- // Don't offer to inline in8, because the result, "x", would mean something different
- // in this scope than it does in the scope where in8 is defined.
- x = in8
-
- _ = x
-}
diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden
deleted file mode 100644
index 7d75a598fb7..00000000000
--- a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden
+++ /dev/null
@@ -1,98 +0,0 @@
-package a
-
-// Functions.
-
-func f() {
- _ = one // want `Call of a.One should be inlined`
-
- _ = 2 // want `Call of \(a.T\).Two should be inlined`
-}
-
-type T struct{}
-
-//go:fix inline
-func One() int { return one } // want One:`goFixInline a.One`
-
-const one = 1
-
-//go:fix inline
-func (T) Two() int { return 2 } // want Two:`goFixInline \(a.T\).Two`
-
-// Constants.
-
-const Uno = 1
-
-//go:fix inline
-const In1 = Uno // want In1: `goFixInline const "a".Uno`
-
-const (
- no1 = one
-
- //go:fix inline
- In2 = one // want In2: `goFixInline const "a".one`
-)
-
-//go:fix inline
-const (
- in3 = one
- in4 = one
- bad1 = 1 // want `invalid //go:fix inline directive: const value is not the name of another constant`
-)
-
-//go:fix inline
-const in5,
- in6,
- bad2 = one, one,
- one + 1 // want `invalid //go:fix inline directive: const value is not the name of another constant`
-
-// Make sure we don't crash on iota consts, but still process the whole decl.
-//
-//go:fix inline
-const (
- a = iota // want `invalid //go:fix inline directive: const value is iota`
- b
- in7 = one
-)
-
-func _() {
- x := Uno // want `Constant In1 should be inlined`
- x = one // want `Constant In2 should be inlined`
- x = one // want `Constant in3 should be inlined`
- x = one // want `Constant in4 should be inlined`
- x = one // want `Constant in5 should be inlined`
- x = one // want `Constant in6 should be inlined`
- x = one // want `Constant in7 should be inlined`
- x = no1
- _ = x
-
- in1 := 1 // don't inline lvalues
- _ = in1
-}
-
-const (
- x = 1
- //go:fix inline
- in8 = x
-)
-
-func shadow() {
- var x int // shadows x at package scope
-
- //go:fix inline
- const a = iota // want `invalid //go:fix inline directive: const value is iota`
-
- const iota = 2
- // Below this point, iota is an ordinary constant.
-
- //go:fix inline
- const b = iota
-
- x = a // a is defined with the predeclared iota, so it cannot be inlined
- x = iota // want `Constant b should be inlined`
-
- // Don't offer to inline in8, because the result, "x", would mean something different
- // in this scope than it does in the scope where in8 is defined.
- x = in8
-
- _ = x
-}
diff --git a/gopls/internal/analysis/hostport/hostport.go b/gopls/internal/analysis/hostport/hostport.go
deleted file mode 100644
index a7030ae116f..00000000000
--- a/gopls/internal/analysis/hostport/hostport.go
+++ /dev/null
@@ -1,191 +0,0 @@
-// 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 hostport defines an analyzer for calls to net.Dial with
-// addresses of the form "%s:%d" or "%s:%s", which work only with IPv4.
-package hostport
-
-import (
- "fmt"
- "go/ast"
- "go/constant"
- "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/go/types/typeutil"
- "golang.org/x/tools/gopls/internal/util/safetoken"
- "golang.org/x/tools/internal/analysisinternal"
- "golang.org/x/tools/internal/astutil/cursor"
-)
-
-const Doc = `check format of addresses passed to net.Dial
-
-This analyzer flags code that produce network address strings using
-fmt.Sprintf, as in this example:
-
- addr := fmt.Sprintf("%s:%d", host, 12345) // "will not work with IPv6"
- ...
- conn, err := net.Dial("tcp", addr) // "when passed to dial here"
-
-The analyzer suggests a fix to use the correct approach, a call to
-net.JoinHostPort:
-
- addr := net.JoinHostPort(host, "12345")
- ...
- conn, err := net.Dial("tcp", addr)
-
-A similar diagnostic and fix are produced for a format string of "%s:%s".
-`
-
-var Analyzer = &analysis.Analyzer{
- Name: "hostport",
- Doc: Doc,
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/hostport",
- Requires: []*analysis.Analyzer{inspect.Analyzer},
- Run: run,
-}
-
-func run(pass *analysis.Pass) (any, error) {
- // Fast path: if the package doesn't import net and fmt, skip
- // the traversal.
- if !analysisinternal.Imports(pass.Pkg, "net") ||
- !analysisinternal.Imports(pass.Pkg, "fmt") {
- return nil, nil
- }
-
- info := pass.TypesInfo
-
- // checkAddr reports a diagnostic (and returns true) if e
- // is a call of the form fmt.Sprintf("%d:%d", ...).
- // The diagnostic includes a fix.
- //
- // dialCall is non-nil if the Dial call is non-local
- // but within the same file.
- checkAddr := func(e ast.Expr, dialCall *ast.CallExpr) {
- if call, ok := e.(*ast.CallExpr); ok {
- obj := typeutil.Callee(info, call)
- if analysisinternal.IsFunctionNamed(obj, "fmt", "Sprintf") {
- // Examine format string.
- formatArg := call.Args[0]
- if tv := info.Types[formatArg]; tv.Value != nil {
- numericPort := false
- format := constant.StringVal(tv.Value)
- switch format {
- case "%s:%d":
- // Have: fmt.Sprintf("%s:%d", host, port)
- numericPort = true
-
- case "%s:%s":
- // Have: fmt.Sprintf("%s:%s", host, portStr)
- // Keep port string as is.
-
- default:
- return
- }
-
- // Use granular edits to preserve original formatting.
- edits := []analysis.TextEdit{
- {
- // Replace fmt.Sprintf with net.JoinHostPort.
- Pos: call.Fun.Pos(),
- End: call.Fun.End(),
- NewText: []byte("net.JoinHostPort"),
- },
- {
- // Delete format string.
- Pos: formatArg.Pos(),
- End: call.Args[1].Pos(),
- },
- }
-
- // Turn numeric port into a string.
- if numericPort {
- // port => fmt.Sprintf("%d", port)
- // 123 => "123"
- port := call.Args[2]
- newPort := fmt.Sprintf(`fmt.Sprintf("%%d", %s)`, port)
- if port := info.Types[port].Value; port != nil {
- if i, ok := constant.Int64Val(port); ok {
- newPort = fmt.Sprintf(`"%d"`, i) // numeric constant
- }
- }
-
- edits = append(edits, analysis.TextEdit{
- Pos: port.Pos(),
- End: port.End(),
- NewText: []byte(newPort),
- })
- }
-
- // Refer to Dial call, if not adjacent.
- suffix := ""
- if dialCall != nil {
- suffix = fmt.Sprintf(" (passed to net.Dial at L%d)",
- safetoken.StartPosition(pass.Fset, dialCall.Pos()).Line)
- }
-
- pass.Report(analysis.Diagnostic{
- // Highlight the format string.
- Pos: formatArg.Pos(),
- End: formatArg.End(),
- Message: fmt.Sprintf("address format %q does not work with IPv6%s", format, suffix),
- SuggestedFixes: []analysis.SuggestedFix{{
- Message: "Replace fmt.Sprintf with net.JoinHostPort",
- TextEdits: edits,
- }},
- })
- }
- }
- }
- }
-
- // Check address argument of each call to net.Dial et al.
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
- for curCall := range cursor.Root(inspect).Preorder((*ast.CallExpr)(nil)) {
- call := curCall.Node().(*ast.CallExpr)
-
- obj := typeutil.Callee(info, call)
- if analysisinternal.IsFunctionNamed(obj, "net", "Dial", "DialTimeout") ||
- analysisinternal.IsMethodNamed(obj, "net", "Dialer", "Dial") {
-
- switch address := call.Args[1].(type) {
- case *ast.CallExpr:
- // net.Dial("tcp", fmt.Sprintf("%s:%d", ...))
- checkAddr(address, nil)
-
- case *ast.Ident:
- // addr := fmt.Sprintf("%s:%d", ...)
- // ...
- // net.Dial("tcp", addr)
-
- // Search for decl of addrVar within common ancestor of addrVar and Dial call.
- if addrVar, ok := info.Uses[address].(*types.Var); ok {
- pos := addrVar.Pos()
- for curAncestor := range curCall.Ancestors() {
- if curIdent, ok := curAncestor.FindPos(pos, pos); ok {
- // curIdent is the declaring ast.Ident of addr.
- switch parent := curIdent.Parent().Node().(type) {
- case *ast.AssignStmt:
- if len(parent.Rhs) == 1 {
- // Have: addr := fmt.Sprintf("%s:%d", ...)
- checkAddr(parent.Rhs[0], call)
- }
-
- case *ast.ValueSpec:
- if len(parent.Values) == 1 {
- // Have: var addr = fmt.Sprintf("%s:%d", ...)
- checkAddr(parent.Values[0], call)
- }
- }
- break
- }
- }
- }
- }
- }
- }
- return nil, nil
-}
diff --git a/gopls/internal/analysis/maprange/cmd/maprange/main.go b/gopls/internal/analysis/maprange/cmd/maprange/main.go
new file mode 100644
index 00000000000..ec1fd5ca93c
--- /dev/null
+++ b/gopls/internal/analysis/maprange/cmd/maprange/main.go
@@ -0,0 +1,14 @@
+// Copyright 2025 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.
+
+// The maprange command applies the golang.org/x/tools/gopls/internal/analysis/maprange
+// analysis to the specified packages of Go source code.
+package main
+
+import (
+ "golang.org/x/tools/go/analysis/singlechecker"
+ "golang.org/x/tools/gopls/internal/analysis/maprange"
+)
+
+func main() { singlechecker.Main(maprange.Analyzer) }
diff --git a/gopls/internal/analysis/maprange/doc.go b/gopls/internal/analysis/maprange/doc.go
new file mode 100644
index 00000000000..46f465059a9
--- /dev/null
+++ b/gopls/internal/analysis/maprange/doc.go
@@ -0,0 +1,37 @@
+// Copyright 2025 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 maprange defines an Analyzer that checks for redundant use
+// of the functions maps.Keys and maps.Values in "for" statements with
+// "range" clauses.
+//
+// # Analyzer maprange
+//
+// maprange: checks for unnecessary calls to maps.Keys and maps.Values in range statements
+//
+// Consider a loop written like this:
+//
+// for val := range maps.Values(m) {
+// fmt.Println(val)
+// }
+//
+// This should instead be written without the call to maps.Values:
+//
+// for _, val := range m {
+// fmt.Println(val)
+// }
+//
+// golang.org/x/exp/maps returns slices for Keys/Values instead of iterators,
+// but unnecessary calls should similarly be removed:
+//
+// for _, key := range maps.Keys(m) {
+// fmt.Println(key)
+// }
+//
+// should be rewritten as:
+//
+// for key := range m {
+// fmt.Println(key)
+// }
+package maprange
diff --git a/gopls/internal/analysis/maprange/maprange.go b/gopls/internal/analysis/maprange/maprange.go
new file mode 100644
index 00000000000..eed04b14e72
--- /dev/null
+++ b/gopls/internal/analysis/maprange/maprange.go
@@ -0,0 +1,159 @@
+// Copyright 2025 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 maprange
+
+import (
+ _ "embed"
+ "fmt"
+ "go/ast"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/gopls/internal/util/moreiters"
+ "golang.org/x/tools/internal/analysisinternal"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/astutil/edge"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
+ "golang.org/x/tools/internal/versions"
+)
+
+//go:embed doc.go
+var doc string
+
+var Analyzer = &analysis.Analyzer{
+ Name: "maprange",
+ Doc: analysisinternal.MustExtractDoc(doc, "maprange"),
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/maprange",
+ Requires: []*analysis.Analyzer{typeindexanalyzer.Analyzer},
+ Run: run,
+}
+
+// This is a variable because the package name is different in Google's code base.
+var xmaps = "golang.org/x/exp/maps"
+
+func run(pass *analysis.Pass) (any, error) {
+ switch pass.Pkg.Path() {
+ case "maps", xmaps:
+ // These packages know how to use their own APIs.
+ return nil, nil
+ }
+ var (
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ mapsKeys = index.Object("maps", "Keys")
+ mapsValues = index.Object("maps", "Values")
+ xmapsKeys = index.Object(xmaps, "Keys")
+ xmapsValues = index.Object(xmaps, "Values")
+ )
+ for _, callee := range []types.Object{mapsKeys, mapsValues, xmapsKeys, xmapsValues} {
+ for curCall := range index.Calls(callee) {
+ if ek, _ := curCall.ParentEdge(); ek != edge.RangeStmt_X {
+ continue
+ }
+ analyzeRangeStmt(pass, callee, curCall)
+ }
+ }
+ return nil, nil
+}
+
+// analyzeRangeStmt analyzes range statements iterating over calls to maps.Keys
+// or maps.Values (from the standard library "maps" or "golang.org/x/exp/maps").
+//
+// It reports a diagnostic with a suggested fix to simplify the loop by removing
+// the unnecessary function call and adjusting range variables, if possible.
+// For certain patterns involving x/exp/maps.Keys before Go 1.22, it reports
+// a diagnostic about potential incorrect usage without a suggested fix.
+// No diagnostic is reported if the range statement doesn't require changes.
+func analyzeRangeStmt(pass *analysis.Pass, callee types.Object, curCall cursor.Cursor) {
+ var (
+ call = curCall.Node().(*ast.CallExpr)
+ rangeStmt = curCall.Parent().Node().(*ast.RangeStmt)
+ pkg = callee.Pkg().Path()
+ fn = callee.Name()
+ )
+ var edits []analysis.TextEdit
+
+ // Check if the call to maps.Keys or maps.Values can be removed/replaced.
+ // Example:
+ // for range maps.Keys(m)
+ // ^^^^^^^^^ removeCall
+ // for i, _ := range maps.Keys(m)
+ // ^^^^^^^^^ replace with `len`
+ //
+ // If we have: for i, k := range maps.Keys(m) (only possible using x/exp/maps)
+ // or: for i, v = range maps.Values(m)
+ // do not remove the call.
+ removeCall := !isSet(rangeStmt.Key) || !isSet(rangeStmt.Value)
+ replace := ""
+ if pkg == xmaps && isSet(rangeStmt.Key) && rangeStmt.Value == nil {
+ // If we have: for i := range maps.Keys(m) (using x/exp/maps),
+ // Replace with: for i := range len(m)
+ replace = "len"
+ canRangeOverInt := fileUses(pass.TypesInfo, curCall, "go1.22")
+ if !canRangeOverInt {
+ pass.Report(analysis.Diagnostic{
+ Pos: call.Pos(),
+ End: call.End(),
+ Message: fmt.Sprintf("likely incorrect use of %s.%s (returns a slice)", pkg, fn),
+ })
+ return
+ }
+ }
+ if removeCall {
+ edits = append(edits, analysis.TextEdit{
+ Pos: call.Fun.Pos(),
+ End: call.Fun.End(),
+ NewText: []byte(replace)})
+ }
+ // Check if the key of the range statement should be removed.
+ // Example:
+ // for _, k := range maps.Keys(m)
+ // ^^^ removeKey ^^^^^^^^^ removeCall
+ removeKey := pkg == xmaps && fn == "Keys" && !isSet(rangeStmt.Key) && isSet(rangeStmt.Value)
+ if removeKey {
+ edits = append(edits, analysis.TextEdit{
+ Pos: rangeStmt.Key.Pos(),
+ End: rangeStmt.Value.Pos(),
+ })
+ }
+ // Check if a key should be inserted to the range statement.
+ // Example:
+ // for _, v := range maps.Values(m)
+ // ^^^ addKey ^^^^^^^^^^^ removeCall
+ addKey := pkg == "maps" && fn == "Values" && isSet(rangeStmt.Key)
+ if addKey {
+ edits = append(edits, analysis.TextEdit{
+ Pos: rangeStmt.Key.Pos(),
+ End: rangeStmt.Key.Pos(),
+ NewText: []byte("_, "),
+ })
+ }
+
+ if len(edits) > 0 {
+ pass.Report(analysis.Diagnostic{
+ Pos: call.Pos(),
+ End: call.End(),
+ Message: fmt.Sprintf("unnecessary and inefficient call of %s.%s", pkg, fn),
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: fmt.Sprintf("Remove unnecessary call to %s.%s", pkg, fn),
+ TextEdits: edits,
+ }},
+ })
+ }
+}
+
+// isSet reports whether an ast.Expr is a non-nil expression that is not the blank identifier.
+func isSet(expr ast.Expr) bool {
+ ident, ok := expr.(*ast.Ident)
+ return expr != nil && (!ok || ident.Name != "_")
+}
+
+// fileUses reports whether the file containing the specified cursor
+// uses at least the specified version of Go (e.g. "go1.24").
+func fileUses(info *types.Info, c cursor.Cursor, version string) bool {
+ c, _ = moreiters.First(c.Enclosing((*ast.File)(nil)))
+ file := c.Node().(*ast.File)
+ return !versions.Before(info.FileVersions[file], version)
+}
diff --git a/gopls/internal/analysis/maprange/maprange_test.go b/gopls/internal/analysis/maprange/maprange_test.go
new file mode 100644
index 00000000000..1759dc1db99
--- /dev/null
+++ b/gopls/internal/analysis/maprange/maprange_test.go
@@ -0,0 +1,23 @@
+// Copyright 2025 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 maprange_test
+
+import (
+ "golang.org/x/tools/go/analysis/analysistest"
+ "golang.org/x/tools/gopls/internal/analysis/maprange"
+ "golang.org/x/tools/internal/testfiles"
+ "path/filepath"
+ "testing"
+)
+
+func TestBasic(t *testing.T) {
+ dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "basic.txtar"))
+ analysistest.RunWithSuggestedFixes(t, dir, maprange.Analyzer, "maprange")
+}
+
+func TestOld(t *testing.T) {
+ dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "old.txtar"))
+ analysistest.RunWithSuggestedFixes(t, dir, maprange.Analyzer, "maprange")
+}
diff --git a/gopls/internal/analysis/maprange/testdata/basic.txtar b/gopls/internal/analysis/maprange/testdata/basic.txtar
new file mode 100644
index 00000000000..1950e958218
--- /dev/null
+++ b/gopls/internal/analysis/maprange/testdata/basic.txtar
@@ -0,0 +1,209 @@
+Test of fixing redundant calls to maps.Keys and maps.Values
+(both stdlib "maps" and "golang.org/x/exp/maps") for Go 1.24.
+
+-- go.mod --
+module maprange
+
+require golang.org/x/exp v0.0.0
+
+replace golang.org/x/exp => ./exp
+
+go 1.24
+
+-- basic.go --
+package basic
+
+import "maps"
+
+func _() {
+ m := make(map[int]int)
+
+ for range maps.Keys(m) { // want `unnecessary and inefficient call of maps.Keys`
+ }
+
+ for range maps.Values(m) { // want `unnecessary and inefficient call of maps.Values`
+ }
+
+ var x struct {
+ Map map[int]int
+ }
+ x.Map = make(map[int]int)
+ for x.Map[1] = range maps.Keys(m) { // want `unnecessary and inefficient call of maps.Keys`
+ }
+
+ for x.Map[2] = range maps.Values(m) { // want `unnecessary and inefficient call of maps.Values`
+ }
+
+ for k := range maps.Keys(m) { // want `unnecessary and inefficient call of maps.Keys`
+ _ = k
+ }
+
+ for v := range maps.Values(m) { // want `unnecessary and inefficient call of maps.Values`
+ _ = v
+ }
+
+ for range maps.Keys(x.Map) { // want `unnecessary and inefficient call of maps.Keys`
+ }
+
+ for /* comment */ k := range /* comment */ maps.Keys(/* comment */ m) { // want `unnecessary and inefficient call of maps.Keys`
+ _ = k
+ }
+}
+
+-- basic.go.golden --
+package basic
+
+import "maps"
+
+func _() {
+ m := make(map[int]int)
+
+ for range m { // want `unnecessary and inefficient call of maps.Keys`
+ }
+
+ for range m { // want `unnecessary and inefficient call of maps.Values`
+ }
+
+ var x struct {
+ Map map[int]int
+ }
+ x.Map = make(map[int]int)
+ for x.Map[1] = range m { // want `unnecessary and inefficient call of maps.Keys`
+ }
+
+ for _, x.Map[2] = range m { // want `unnecessary and inefficient call of maps.Values`
+ }
+
+ for k := range m { // want `unnecessary and inefficient call of maps.Keys`
+ _ = k
+ }
+
+ for _, v := range m { // want `unnecessary and inefficient call of maps.Values`
+ _ = v
+ }
+
+ for range x.Map { // want `unnecessary and inefficient call of maps.Keys`
+ }
+
+ for /* comment */ k := range /* comment */ /* comment */ m { // want `unnecessary and inefficient call of maps.Keys`
+ _ = k
+ }
+}
+
+-- xmaps.go --
+package basic
+
+import "golang.org/x/exp/maps"
+
+func _() {
+ m := make(map[int]int)
+
+ for range maps.Keys(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Keys`
+ }
+
+ for range maps.Values(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ }
+
+ for i := range maps.Values(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ _ = i
+ }
+
+ var x struct {
+ Map map[int]int
+ }
+ x.Map = make(map[int]int)
+ for _, x.Map[1] = range maps.Keys(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Keys`
+ }
+
+ for _, x.Map[2] = range maps.Values(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ }
+
+ for _, k := range maps.Keys(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Keys`
+ _ = k
+ }
+
+ for _, v := range maps.Values(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ _ = v
+ }
+
+ for range maps.Keys(x.Map) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Keys`
+ }
+
+ for i, k := range maps.Keys(m) { // ok: this can't be straightforwardly rewritten
+ _, _ = i, k
+ }
+
+ for _, _ = range maps.Values(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ }
+}
+
+-- xmaps.go.golden --
+package basic
+
+import "golang.org/x/exp/maps"
+
+func _() {
+ m := make(map[int]int)
+
+ for range m { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Keys`
+ }
+
+ for range m { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ }
+
+ for i := range len(m) { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ _ = i
+ }
+
+ var x struct {
+ Map map[int]int
+ }
+ x.Map = make(map[int]int)
+ for x.Map[1] = range m { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Keys`
+ }
+
+ for _, x.Map[2] = range m { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ }
+
+ for k := range m { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Keys`
+ _ = k
+ }
+
+ for _, v := range m { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ _ = v
+ }
+
+ for range x.Map { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Keys`
+ }
+
+ for i, k := range maps.Keys(m) { // ok: this can't be straightforwardly rewritten
+ _, _ = i, k
+ }
+
+ for _, _ = range m { // want `unnecessary and inefficient call of golang.org/x/exp/maps.Values`
+ }
+}
+
+-- exp/go.mod --
+module golang.org/x/exp
+
+go 1.24
+
+-- exp/maps/maps.go --
+package maps
+
+func Keys[M ~map[K]V, K comparable, V any](m M) []K {
+ r := make([]K, 0, len(m))
+ for k := range m {
+ r = append(r, k)
+ }
+ return r
+}
+
+func Values[M ~map[K]V, K comparable, V any](m M) []V {
+ r := make([]V, 0, len(m))
+ for _, v := range m {
+ r = append(r, v)
+ }
+ return r
+}
\ No newline at end of file
diff --git a/gopls/internal/analysis/maprange/testdata/old.txtar b/gopls/internal/analysis/maprange/testdata/old.txtar
new file mode 100644
index 00000000000..d27ff8c2a22
--- /dev/null
+++ b/gopls/internal/analysis/maprange/testdata/old.txtar
@@ -0,0 +1,62 @@
+Test of fixing redundant calls to maps.Keys and maps.Values
+(both stdlib "maps" and "golang.org/x/exp/maps") for Go 1.21,
+before range over int made suggesting a fix for a rare case easier.
+
+-- go.mod --
+module maprange
+
+require golang.org/x/exp v0.0.0
+
+replace golang.org/x/exp => ./exp
+
+go 1.21
+
+-- old.go --
+package old
+
+import "golang.org/x/exp/maps"
+
+func _() {
+ m := make(map[int]int)
+
+ for i := range maps.Keys(m) { // want `likely incorrect use of golang.org/x/exp/maps.Keys \(returns a slice\)`
+ _ = i
+ }
+}
+
+-- old.go.golden --
+package old
+
+import "golang.org/x/exp/maps"
+
+func _() {
+ m := make(map[int]int)
+
+ for i := range maps.Keys(m) { // want `likely incorrect use of golang.org/x/exp/maps.Keys \(returns a slice\)`
+ _ = i
+ }
+}
+
+-- exp/go.mod --
+module golang.org/x/exp
+
+go 1.21
+
+-- exp/maps/maps.go --
+package maps
+
+func Keys[M ~map[K]V, K comparable, V any](m M) []K {
+ r := make([]K, 0, len(m))
+ for k := range m {
+ r = append(r, k)
+ }
+ return r
+}
+
+func Values[M ~map[K]V, K comparable, V any](m M) []V {
+ r := make([]V, 0, len(m))
+ for _, v := range m {
+ r = append(r, v)
+ }
+ return r
+}
\ No newline at end of file
diff --git a/gopls/internal/analysis/modernize/bloop.go b/gopls/internal/analysis/modernize/bloop.go
index f851a6688e1..ea2359c7fb6 100644
--- a/gopls/internal/analysis/modernize/bloop.go
+++ b/gopls/internal/analysis/modernize/bloop.go
@@ -14,8 +14,11 @@ import (
"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/gopls/internal/util/moreiters"
"golang.org/x/tools/internal/analysisinternal"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
)
// bloop updates benchmarks that use "for range b.N", replacing it
@@ -31,7 +34,11 @@ func bloop(pass *analysis.Pass) {
return
}
- info := pass.TypesInfo
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
+ )
// edits computes the text edits for a matched for/range loop
// at the specified cursor. b is the *testing.B value, and
@@ -41,27 +48,25 @@ func bloop(pass *analysis.Pass) {
// Within the same function, delete all calls to
// b.{Start,Stop,Timer} that precede the loop.
filter := []ast.Node{(*ast.ExprStmt)(nil), (*ast.FuncLit)(nil)}
- curFn.Inspect(filter, func(cur cursor.Cursor, push bool) (descend bool) {
- if push {
- node := cur.Node()
- if is[*ast.FuncLit](node) {
- return false // don't descend into FuncLits (e.g. sub-benchmarks)
- }
- stmt := node.(*ast.ExprStmt)
- if stmt.Pos() > start {
- return false // not preceding: stop
- }
- if call, ok := stmt.X.(*ast.CallExpr); ok {
- obj := typeutil.Callee(info, call)
- if analysisinternal.IsMethodNamed(obj, "testing", "B", "StopTimer", "StartTimer", "ResetTimer") {
- // Delete call statement.
- // TODO(adonovan): delete following newline, or
- // up to start of next stmt? (May delete a comment.)
- edits = append(edits, analysis.TextEdit{
- Pos: stmt.Pos(),
- End: stmt.End(),
- })
- }
+ curFn.Inspect(filter, func(cur cursor.Cursor) (descend bool) {
+ node := cur.Node()
+ if is[*ast.FuncLit](node) {
+ return false // don't descend into FuncLits (e.g. sub-benchmarks)
+ }
+ stmt := node.(*ast.ExprStmt)
+ if stmt.Pos() > start {
+ return false // not preceding: stop
+ }
+ if call, ok := stmt.X.(*ast.CallExpr); ok {
+ obj := typeutil.Callee(info, call)
+ if analysisinternal.IsMethodNamed(obj, "testing", "B", "StopTimer", "StartTimer", "ResetTimer") {
+ // Delete call statement.
+ // TODO(adonovan): delete following newline, or
+ // up to start of next stmt? (May delete a comment.)
+ edits = append(edits, analysis.TextEdit{
+ Pos: stmt.Pos(),
+ End: stmt.End(),
+ })
}
}
return true
@@ -76,7 +81,6 @@ func bloop(pass *analysis.Pass) {
}
// Find all for/range statements.
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
loops := []ast.Node{
(*ast.ForStmt)(nil),
(*ast.RangeStmt)(nil),
@@ -101,11 +105,11 @@ func bloop(pass *analysis.Pass) {
if assign, ok := n.Init.(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE &&
len(assign.Rhs) == 1 &&
- isZeroLiteral(assign.Rhs[0]) &&
+ isZeroIntLiteral(info, assign.Rhs[0]) &&
is[*ast.IncDecStmt](n.Post) &&
n.Post.(*ast.IncDecStmt).Tok == token.INC &&
equalSyntax(n.Post.(*ast.IncDecStmt).X, assign.Lhs[0]) &&
- !uses(info, body, info.Defs[assign.Lhs[0].(*ast.Ident)]) {
+ !uses(index, body, info.Defs[assign.Lhs[0].(*ast.Ident)]) {
delStart, delEnd = n.Init.Pos(), n.Post.End()
}
@@ -152,9 +156,9 @@ func bloop(pass *analysis.Pass) {
}
// uses reports whether the subtree cur contains a use of obj.
-func uses(info *types.Info, cur cursor.Cursor, obj types.Object) bool {
- for curId := range cur.Preorder((*ast.Ident)(nil)) {
- if info.Uses[curId.Node().(*ast.Ident)] == obj {
+func uses(index *typeindex.Index, cur cursor.Cursor, obj types.Object) bool {
+ for use := range index.Uses(obj) {
+ if cur.Contains(use) {
return true
}
}
@@ -164,8 +168,5 @@ func uses(info *types.Info, cur cursor.Cursor, obj types.Object) bool {
// enclosingFunc returns the cursor for the innermost Func{Decl,Lit}
// that encloses c, if any.
func enclosingFunc(c cursor.Cursor) (cursor.Cursor, bool) {
- for curAncestor := range c.Ancestors((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) {
- return curAncestor, true
- }
- return cursor.Cursor{}, false
+ return moreiters.First(c.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)))
}
diff --git a/gopls/internal/analysis/modernize/doc.go b/gopls/internal/analysis/modernize/doc.go
index 15aeab64d8d..2c4b893f6d2 100644
--- a/gopls/internal/analysis/modernize/doc.go
+++ b/gopls/internal/analysis/modernize/doc.go
@@ -9,27 +9,90 @@
// modernize: simplify code by using modern constructs
//
// This analyzer reports opportunities for simplifying and clarifying
-// existing code by using more modern features of Go, such as:
-//
-// - replacing an if/else conditional assignment by a call to the
-// built-in min or max functions added in go1.21;
-// - replacing sort.Slice(x, func(i, j int) bool) { return s[i] < s[j] }
-// by a call to slices.Sort(s), added in go1.21;
-// - replacing interface{} by the 'any' type added in go1.18;
-// - replacing append([]T(nil), s...) by slices.Clone(s) or
-// slices.Concat(s), added in go1.21;
-// - replacing a loop around an m[k]=v map update by a call
-// to one of the Collect, Copy, Clone, or Insert functions
-// from the maps package, added in go1.21;
-// - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),
-// added in go1.19;
-// - replacing uses of context.WithCancel in tests with t.Context, added in
-// go1.24;
-// - replacing omitempty by omitzero on structs, added in go1.24;
-// - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),
-// added in go1.21
-// - replacing a 3-clause for i := 0; i < n; i++ {} loop by
-// for i := range n {}, added in go1.22;
-// - replacing Split in "for range strings.Split(...)" by go1.24's
-// more efficient SplitSeq;
+// existing code by using more modern features of Go and its standard
+// library.
+//
+// Each diagnostic provides a fix. Our intent is that these fixes may
+// be safely applied en masse without changing the behavior of your
+// program. In some cases the suggested fixes are imperfect and may
+// lead to (for example) unused imports or unused local variables,
+// causing build breakage. However, these problems are generally
+// trivial to fix. We regard any modernizer whose fix changes program
+// behavior to have a serious bug and will endeavor to fix it.
+//
+// To apply all modernization fixes en masse, you can use the
+// following command:
+//
+// $ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...
+//
+// (Do not use "go get -tool" to add gopls as a dependency of your
+// module; gopls commands must be built from their release branch.)
+//
+// If the tool warns of conflicting fixes, you may need to run it more
+// than once until it has applied all fixes cleanly. This command is
+// not an officially supported interface and may change in the future.
+//
+// Changes produced by this tool should be reviewed as usual before
+// being merged. In some cases, a loop may be replaced by a simple
+// function call, causing comments within the loop to be discarded.
+// Human judgment may be required to avoid losing comments of value.
+//
+// Each diagnostic reported by modernize has a specific category. (The
+// categories are listed below.) Diagnostics in some categories, such
+// as "efaceany" (which replaces "interface{}" with "any" where it is
+// safe to do so) are particularly numerous. It may ease the burden of
+// code review to apply fixes in two passes, the first change
+// consisting only of fixes of category "efaceany", the second
+// consisting of all others. This can be achieved using the -category flag:
+//
+// $ modernize -category=efaceany -fix -test ./...
+// $ modernize -category=-efaceany -fix -test ./...
+//
+// Categories of modernize diagnostic:
+//
+// - forvar: remove x := x variable declarations made unnecessary by the new semantics of loops in go1.22.
+//
+// - slicescontains: replace 'for i, elem := range s { if elem == needle { ...; break }'
+// by a call to slices.Contains, added in go1.21.
+//
+// - minmax: replace an if/else conditional assignment by a call to
+// the built-in min or max functions added in go1.21.
+//
+// - sortslice: replace sort.Slice(x, func(i, j int) bool) { return s[i] < s[j] }
+// by a call to slices.Sort(s), added in go1.21.
+//
+// - efaceany: replace interface{} by the 'any' type added in go1.18.
+//
+// - slicesclone: replace append([]T(nil), s...) by slices.Clone(s) or
+// slices.Concat(s), added in go1.21.
+//
+// - mapsloop: replace a loop around an m[k]=v map update by a call
+// to one of the Collect, Copy, Clone, or Insert functions from
+// the maps package, added in go1.21.
+//
+// - fmtappendf: replace []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),
+// added in go1.19.
+//
+// - testingcontext: replace uses of context.WithCancel in tests
+// with t.Context, added in go1.24.
+//
+// - omitzero: replace omitempty by omitzero on structs, added in go1.24.
+//
+// - bloop: replace "for i := range b.N" or "for range b.N" in a
+// benchmark with "for b.Loop()", and remove any preceding calls
+// to b.StopTimer, b.StartTimer, and b.ResetTimer.
+//
+// - slicesdelete: replace append(s[:i], s[i+1]...) by
+// slices.Delete(s, i, i+1), added in go1.21.
+//
+// - rangeint: replace a 3-clause "for i := 0; i < n; i++" loop by
+// "for i := range n", added in go1.22.
+//
+// - stringsseq: replace Split in "for range strings.Split(...)" by go1.24's
+// more efficient SplitSeq, or Fields with FieldSeq.
+//
+// - stringscutprefix: replace some uses of HasPrefix followed by TrimPrefix with CutPrefix,
+// added to the strings package in go1.20.
+//
+// - waitgroup: replace old complex usages of sync.WaitGroup by less complex WaitGroup.Go method in go1.25.
package modernize
diff --git a/gopls/internal/analysis/modernize/fmtappendf.go b/gopls/internal/analysis/modernize/fmtappendf.go
index 8575827aa3e..6b01d38050e 100644
--- a/gopls/internal/analysis/modernize/fmtappendf.go
+++ b/gopls/internal/analysis/modernize/fmtappendf.go
@@ -5,33 +5,35 @@
package modernize
import (
+ "fmt"
"go/ast"
"go/types"
"strings"
"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"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/astutil/edge"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
)
// The fmtappend function replaces []byte(fmt.Sprintf(...)) by
-// fmt.Appendf(nil, ...).
+// fmt.Appendf(nil, ...), and similarly for Sprint, Sprintln.
func fmtappendf(pass *analysis.Pass) {
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
- info := pass.TypesInfo
- for curFile := range filesUsing(inspect, info, "go1.19") {
- for curCallExpr := range curFile.Preorder((*ast.CallExpr)(nil)) {
- conv := curCallExpr.Node().(*ast.CallExpr)
- tv := info.Types[conv.Fun]
- if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
- call, ok := conv.Args[0].(*ast.CallExpr)
- if ok {
- obj := typeutil.Callee(info, call)
- if !analysisinternal.IsFunctionNamed(obj, "fmt", "Sprintf", "Sprintln", "Sprint") {
- continue
- }
+ index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ for _, fn := range []types.Object{
+ index.Object("fmt", "Sprintf"),
+ index.Object("fmt", "Sprintln"),
+ index.Object("fmt", "Sprint"),
+ } {
+ for curCall := range index.Calls(fn) {
+ call := curCall.Node().(*ast.CallExpr)
+ if ek, idx := curCall.ParentEdge(); ek == edge.CallExpr_Args && idx == 0 {
+ // Is parent a T(fmt.SprintX(...)) conversion?
+ conv := curCall.Parent().Node().(*ast.CallExpr)
+ tv := pass.TypesInfo.Types[conv.Fun]
+ if tv.IsType() && types.Identical(tv.Type, byteSliceType) &&
+ fileUses(pass.TypesInfo, enclosingFile(curCall), "go1.19") {
+ // Have: []byte(fmt.SprintX(...))
// Find "Sprint" identifier.
var id *ast.Ident
@@ -42,13 +44,14 @@ func fmtappendf(pass *analysis.Pass) {
id = e // "Sprint" after `import . "fmt"`
}
+ old, new := fn.Name(), strings.Replace(fn.Name(), "Sprint", "Append", 1)
pass.Report(analysis.Diagnostic{
Pos: conv.Pos(),
End: conv.End(),
Category: "fmtappendf",
- Message: "Replace []byte(fmt.Sprintf...) with fmt.Appendf",
+ Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new),
SuggestedFixes: []analysis.SuggestedFix{{
- Message: "Replace []byte(fmt.Sprintf...) with fmt.Appendf",
+ Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new),
TextEdits: []analysis.TextEdit{
{
// delete "[]byte("
@@ -63,7 +66,7 @@ func fmtappendf(pass *analysis.Pass) {
{
Pos: id.Pos(),
End: id.End(),
- NewText: []byte(strings.Replace(obj.Name(), "Sprint", "Append", 1)),
+ NewText: []byte(new),
},
{
Pos: call.Lparen + 1,
diff --git a/gopls/internal/analysis/modernize/forvar.go b/gopls/internal/analysis/modernize/forvar.go
new file mode 100644
index 00000000000..6f88ab77ed9
--- /dev/null
+++ b/gopls/internal/analysis/modernize/forvar.go
@@ -0,0 +1,95 @@
+// Copyright 2025 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/token"
+
+ "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/gopls/internal/util/bug"
+ "golang.org/x/tools/internal/analysisinternal"
+)
+
+// forvar offers to fix unnecessary copying of a for variable
+//
+// for _, x := range foo {
+// x := x // offer to remove this superfluous assignment
+// }
+//
+// Prerequisites:
+// First statement in a range loop has to be :=
+// where the two idents are the same,
+// and the ident is defined (:=) as a variable in the for statement.
+// (Note that this 'fix' does not work for three clause loops
+// because the Go specification says "The variable used by each subsequent iteration
+// is declared implicitly before executing the post statement and initialized to the
+// value of the previous iteration's variable at that moment.")
+func forvar(pass *analysis.Pass) {
+ info := pass.TypesInfo
+
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ for curFile := range filesUsing(inspect, info, "go1.22") {
+ for curLoop := range curFile.Preorder((*ast.RangeStmt)(nil)) {
+ // in a range loop. Is the first statement var := var?
+ // if so, is var one of the range vars, and is it defined
+ // in the for statement?
+ // If so, decide how much to delete.
+ loop := curLoop.Node().(*ast.RangeStmt)
+ if loop.Tok != token.DEFINE {
+ continue
+ }
+ v, stmt := loopVarRedecl(loop.Body)
+ if v == nil {
+ continue // index is not redeclared
+ }
+ if (loop.Key == nil || !equalSyntax(loop.Key, v)) &&
+ (loop.Value == nil || !equalSyntax(loop.Value, v)) {
+ continue
+ }
+ astFile := curFile.Node().(*ast.File)
+ edits := analysisinternal.DeleteStmt(pass.Fset, astFile, stmt, bug.Reportf)
+ if len(edits) == 0 {
+ bug.Reportf("forvar failed to delete statement")
+ continue
+ }
+ remove := edits[0]
+ diag := analysis.Diagnostic{
+ Pos: remove.Pos,
+ End: remove.End,
+ Category: "forvar",
+ Message: "copying variable is unneeded",
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: "Remove unneeded redeclaration",
+ TextEdits: []analysis.TextEdit{remove},
+ }},
+ }
+ pass.Report(diag)
+ }
+ }
+}
+
+// if the first statement is var := var, return var and the stmt
+func loopVarRedecl(body *ast.BlockStmt) (*ast.Ident, *ast.AssignStmt) {
+ if len(body.List) < 1 {
+ return nil, nil
+ }
+ stmt, ok := body.List[0].(*ast.AssignStmt)
+ if !ok || !isSimpleAssign(stmt) || stmt.Tok != token.DEFINE {
+ return nil, nil
+ }
+ if _, ok := stmt.Lhs[0].(*ast.Ident); !ok {
+ return nil, nil
+ }
+ if _, ok := stmt.Rhs[0].(*ast.Ident); !ok {
+ return nil, nil
+ }
+ if stmt.Lhs[0].(*ast.Ident).Name == stmt.Rhs[0].(*ast.Ident).Name {
+ return stmt.Lhs[0].(*ast.Ident), stmt
+ }
+ return nil, nil
+}
diff --git a/gopls/internal/analysis/modernize/maps.go b/gopls/internal/analysis/modernize/maps.go
index c93899621ef..1a5e2c3eeee 100644
--- a/gopls/internal/analysis/modernize/maps.go
+++ b/gopls/internal/analysis/modernize/maps.go
@@ -32,7 +32,7 @@ import (
//
// maps.Copy(m, x) (x is map)
// maps.Insert(m, x) (x is iter.Seq2)
-// m = maps.Clone(x) (x is map, m is a new map)
+// m = maps.Clone(x) (x is a non-nil map, m is a new map)
// m = maps.Collect(x) (x is iter.Seq2, m is a new map)
//
// A map is newly created if the preceding statement has one of these
@@ -41,7 +41,9 @@ import (
// m = make(M)
// m = M{}
func mapsloop(pass *analysis.Pass) {
- if pass.Pkg.Path() == "maps " {
+ // Skip the analyzer in packages where its
+ // fixes would create an import cycle.
+ if within(pass, "maps", "bytes", "runtime") {
return
}
@@ -77,6 +79,8 @@ func mapsloop(pass *analysis.Pass) {
// Is the preceding statement of the form
// m = make(M) or M{}
// and can we replace its RHS with slices.{Clone,Collect}?
+ //
+ // Beware: if x may be nil, we cannot use Clone as it preserves nilness.
var mrhs ast.Expr // make(M) or M{}, or nil
if curPrev, ok := curRange.PrevSibling(); ok {
if assign, ok := curPrev.Node().(*ast.AssignStmt); ok &&
@@ -87,9 +91,9 @@ func mapsloop(pass *analysis.Pass) {
// Have: m = rhs; for k, v := range x { m[k] = v }
var newMap bool
rhs := assign.Rhs[0]
- switch rhs := rhs.(type) {
+ switch rhs := ast.Unparen(rhs).(type) {
case *ast.CallExpr:
- if id, ok := rhs.Fun.(*ast.Ident); ok &&
+ if id, ok := ast.Unparen(rhs.Fun).(*ast.Ident); ok &&
info.Uses[id] == builtinMake {
// Have: m = make(...)
newMap = true
@@ -122,6 +126,16 @@ func mapsloop(pass *analysis.Pass) {
mrhs = rhs
}
}
+
+ // Temporarily disable the transformation to the
+ // (nil-preserving) maps.Clone until we can prove
+ // that x is non-nil. This is rarely possible,
+ // and may require control flow analysis
+ // (e.g. a dominating "if len(x)" check).
+ // See #71844.
+ if xmap {
+ mrhs = nil
+ }
}
}
}
@@ -142,16 +156,35 @@ func mapsloop(pass *analysis.Pass) {
start, end token.Pos
)
if mrhs != nil {
- // Replace RHS of preceding m=... assignment (and loop) with expression.
- start, end = mrhs.Pos(), rng.End()
- newText = fmt.Appendf(nil, "%s%s(%s)",
+ // Replace assignment and loop with expression.
+ //
+ // m = make(...)
+ // for k, v := range x { /* comments */ m[k] = v }
+ //
+ // ->
+ //
+ // /* comments */
+ // m = maps.Copy(x)
+ curPrev, _ := curRange.PrevSibling()
+ start, end = curPrev.Node().Pos(), rng.End()
+ newText = fmt.Appendf(nil, "%s%s = %s%s(%s)",
+ allComments(file, start, end),
+ analysisinternal.Format(pass.Fset, m),
prefix,
funcName,
analysisinternal.Format(pass.Fset, x))
} else {
// Replace loop with call statement.
+ //
+ // for k, v := range x { /* comments */ m[k] = v }
+ //
+ // ->
+ //
+ // /* comments */
+ // maps.Copy(m, x)
start, end = rng.Pos(), rng.End()
- newText = fmt.Appendf(nil, "%s%s(%s, %s)",
+ newText = fmt.Appendf(nil, "%s%s%s(%s, %s)",
+ allComments(file, start, end),
prefix,
funcName,
analysisinternal.Format(pass.Fset, m),
diff --git a/gopls/internal/analysis/modernize/minmax.go b/gopls/internal/analysis/modernize/minmax.go
index 26b12341cad..0e43ee11c3d 100644
--- a/gopls/internal/analysis/modernize/minmax.go
+++ b/gopls/internal/analysis/modernize/minmax.go
@@ -9,12 +9,14 @@ import (
"go/ast"
"go/token"
"go/types"
+ "strings"
"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"
"golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/typeparams"
)
// The minmax pass replaces if/else statements with calls to min or max.
@@ -24,6 +26,10 @@ import (
// 1. if a < b { x = a } else { x = b } => x = min(a, b)
// 2. x = a; if a < b { x = b } => x = max(a, b)
//
+// Pattern 1 requires that a is not NaN, and pattern 2 requires that b
+// is not Nan. Since this is hard to prove, we reject floating-point
+// numbers.
+//
// Variants:
// - all four ordered comparisons
// - "x := a" or "x = a" or "var x = a" in pattern 2
@@ -32,7 +38,7 @@ func minmax(pass *analysis.Pass) {
// check is called for all statements of this form:
// if a < b { lhs = rhs }
- check := func(curIfStmt cursor.Cursor, compare *ast.BinaryExpr) {
+ check := func(file *ast.File, curIfStmt cursor.Cursor, compare *ast.BinaryExpr) {
var (
ifStmt = curIfStmt.Node().(*ast.IfStmt)
tassign = ifStmt.Body.List[0].(*ast.AssignStmt)
@@ -57,7 +63,7 @@ func minmax(pass *analysis.Pass) {
if equalSyntax(lhs, lhs2) {
if equalSyntax(rhs, a) && equalSyntax(rhs2, b) {
sign = +sign
- } else if equalSyntax(rhs2, a) || equalSyntax(rhs, b) {
+ } else if equalSyntax(rhs2, a) && equalSyntax(rhs, b) {
sign = -sign
} else {
return
@@ -85,7 +91,8 @@ func minmax(pass *analysis.Pass) {
// Replace IfStmt with lhs = min(a, b).
Pos: ifStmt.Pos(),
End: ifStmt.End(),
- NewText: fmt.Appendf(nil, "%s = %s(%s, %s)",
+ NewText: fmt.Appendf(nil, "%s%s = %s(%s, %s)",
+ allComments(file, ifStmt.Pos(), ifStmt.End()),
analysisinternal.Format(pass.Fset, lhs),
sym,
analysisinternal.Format(pass.Fset, a),
@@ -95,7 +102,7 @@ func minmax(pass *analysis.Pass) {
})
}
- } else if prev, ok := curIfStmt.PrevSibling(); ok && isSimpleAssign(prev.Node()) {
+ } else if prev, ok := curIfStmt.PrevSibling(); ok && isSimpleAssign(prev.Node()) && ifStmt.Else == nil {
fassign := prev.Node().(*ast.AssignStmt)
// Have: lhs0 = rhs0; if a < b { lhs = rhs }
@@ -144,10 +151,13 @@ func minmax(pass *analysis.Pass) {
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace if/else with %s", sym),
TextEdits: []analysis.TextEdit{{
- // Replace rhs0 and IfStmt with min(a, b)
- Pos: rhs0.Pos(),
+ Pos: fassign.Pos(),
End: ifStmt.End(),
- NewText: fmt.Appendf(nil, "%s(%s, %s)",
+ // Replace "x := a; if ... {}" with "x = min(...)", preserving comments.
+ NewText: fmt.Appendf(nil, "%s %s %s %s(%s, %s)",
+ allComments(file, fassign.Pos(), ifStmt.End()),
+ analysisinternal.Format(pass.Fset, lhs),
+ fassign.Tok.String(),
sym,
analysisinternal.Format(pass.Fset, a),
analysisinternal.Format(pass.Fset, b)),
@@ -159,23 +169,35 @@ func minmax(pass *analysis.Pass) {
}
// Find all "if a < b { lhs = rhs }" statements.
+ info := pass.TypesInfo
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
- for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.21") {
+ for curFile := range filesUsing(inspect, info, "go1.21") {
+ astFile := curFile.Node().(*ast.File)
for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
ifStmt := curIfStmt.Node().(*ast.IfStmt)
-
if compare, ok := ifStmt.Cond.(*ast.BinaryExpr); ok &&
ifStmt.Init == nil &&
isInequality(compare.Op) != 0 &&
isAssignBlock(ifStmt.Body) {
-
- // Have: if a < b { lhs = rhs }
- check(curIfStmt, compare)
+ // a blank var has no type.
+ if tLHS := info.TypeOf(ifStmt.Body.List[0].(*ast.AssignStmt).Lhs[0]); tLHS != nil && !maybeNaN(tLHS) {
+ // Have: if a < b { lhs = rhs }
+ check(astFile, curIfStmt, compare)
+ }
}
}
}
}
+// allComments collects all the comments from start to end.
+func allComments(file *ast.File, start, end token.Pos) string {
+ var buf strings.Builder
+ for co := range analysisinternal.Comments(file, start, end) {
+ _, _ = fmt.Fprintf(&buf, "%s\n", co.Text)
+ }
+ return buf.String()
+}
+
// isInequality reports non-zero if tok is one of < <= => >:
// +1 for > and -1 for <.
func isInequality(tok token.Token) int {
@@ -206,6 +228,21 @@ func isSimpleAssign(n ast.Node) bool {
len(assign.Rhs) == 1
}
+// maybeNaN reports whether t is (or may be) a floating-point type.
+func maybeNaN(t types.Type) bool {
+ // For now, we rely on core types.
+ // TODO(adonovan): In the post-core-types future,
+ // follow the approach of types.Checker.applyTypeFunc.
+ t = typeparams.CoreType(t)
+ if t == nil {
+ return true // fail safe
+ }
+ if basic, ok := t.(*types.Basic); ok && basic.Info()&types.IsFloat != 0 {
+ return true
+ }
+ return false
+}
+
// -- utils --
func is[T any](x any) bool {
diff --git a/gopls/internal/analysis/modernize/modernize.go b/gopls/internal/analysis/modernize/modernize.go
index 0f7b58eed37..44992c3aa14 100644
--- a/gopls/internal/analysis/modernize/modernize.go
+++ b/gopls/internal/analysis/modernize/modernize.go
@@ -7,19 +7,24 @@ package modernize
import (
_ "embed"
"go/ast"
+ "go/constant"
"go/format"
"go/token"
"go/types"
"iter"
"regexp"
+ "slices"
"strings"
"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/gopls/internal/util/astutil"
+ "golang.org/x/tools/gopls/internal/util/moreiters"
"golang.org/x/tools/internal/analysisinternal"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/versions"
)
@@ -29,11 +34,20 @@ var doc string
var Analyzer = &analysis.Analyzer{
Name: "modernize",
Doc: analysisinternal.MustExtractDoc(doc, "modernize"),
- Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Requires: []*analysis.Analyzer{inspect.Analyzer, typeindexanalyzer.Analyzer},
Run: run,
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize",
}
+// Stopgap until general solution in CL 655555 lands. A change to the
+// cmd/vet CLI requires a proposal whereas a change to an analyzer's
+// flag set does not.
+var category string
+
+func init() {
+ Analyzer.Flags.StringVar(&category, "category", "", "comma-separated list of categories to apply; with a leading '-', a list of categories to ignore")
+}
+
func run(pass *analysis.Pass) (any, error) {
// Decorate pass.Report to suppress diagnostics in generated files.
//
@@ -53,6 +67,10 @@ func run(pass *analysis.Pass) (any, error) {
if diag.Category == "" {
panic("Diagnostic.Category is unset")
}
+ // TODO(adonovan): stopgap until CL 655555 lands.
+ if !enabledCategory(category, diag.Category) {
+ return
+ }
if _, ok := generated[pass.Fset.File(diag.Pos)]; ok {
return // skip checking if it's generated code
}
@@ -64,24 +82,20 @@ func run(pass *analysis.Pass) (any, error) {
bloop(pass)
efaceany(pass)
fmtappendf(pass)
+ forvar(pass)
mapsloop(pass)
minmax(pass)
omitzero(pass)
rangeint(pass)
slicescontains(pass)
slicesdelete(pass)
- splitseq(pass)
+ stringscutprefix(pass)
+ stringsseq(pass)
sortslice(pass)
testingContext(pass)
+ waitgroup(pass)
- // TODO(adonovan):
- // - more modernizers here; see #70815.
- // - opt: interleave these micro-passes within a single inspection.
- // - solve the "duplicate import" problem (#68765) when a number of
- // fixes in the same file are applied in parallel and all add
- // the same import. The tests exhibit the problem.
- // - should all diagnostics be of the form "x can be modernized by y"
- // or is that a foolish consistency?
+ // TODO(adonovan): opt: interleave these micro-passes within a single inspection.
return nil, nil
}
@@ -106,14 +120,22 @@ func formatExprs(fset *token.FileSet, exprs []ast.Expr) string {
return buf.String()
}
-// isZeroLiteral reports whether e is the literal 0.
-func isZeroLiteral(e ast.Expr) bool {
- lit, ok := e.(*ast.BasicLit)
- return ok && lit.Kind == token.INT && lit.Value == "0"
+// isZeroIntLiteral reports whether e is an integer whose value is 0.
+func isZeroIntLiteral(info *types.Info, e ast.Expr) bool {
+ return isIntLiteral(info, e, 0)
+}
+
+// isIntLiteral reports whether e is an integer with given value.
+func isIntLiteral(info *types.Info, e ast.Expr, n int64) bool {
+ return info.Types[e].Value == constant.MakeInt64(n)
}
// filesUsing returns a cursor for each *ast.File in the inspector
// that uses at least the specified version of Go (e.g. "go1.24").
+//
+// TODO(adonovan): opt: eliminate this function, instead following the
+// approach of [fmtappendf], which uses typeindex and [fileUses].
+// See "Tip" at [fileUses] for motivation.
func filesUsing(inspect *inspector.Inspector, info *types.Info, version string) iter.Seq[cursor.Cursor] {
return func(yield func(cursor.Cursor) bool) {
for curFile := range cursor.Root(inspect).Children() {
@@ -125,6 +147,33 @@ func filesUsing(inspect *inspector.Inspector, info *types.Info, version string)
}
}
+// fileUses reports whether the specified file uses at least the
+// specified version of Go (e.g. "go1.24").
+//
+// Tip: we recommend using this check "late", just before calling
+// pass.Report, rather than "early" (when entering each ast.File, or
+// each candidate node of interest, during the traversal), because the
+// operation is not free, yet is not a highly selective filter: the
+// fraction of files that pass most version checks is high and
+// increases over time.
+func fileUses(info *types.Info, file *ast.File, version string) bool {
+ return !versions.Before(info.FileVersions[file], version)
+}
+
+// enclosingFile returns the syntax tree for the file enclosing c.
+func enclosingFile(c cursor.Cursor) *ast.File {
+ c, _ = moreiters.First(c.Enclosing((*ast.File)(nil)))
+ return c.Node().(*ast.File)
+}
+
+// within reports whether the current pass is analyzing one of the
+// specified standard packages or their dependencies.
+func within(pass *analysis.Pass, pkgs ...string) bool {
+ path := pass.Pkg.Path()
+ return analysisinternal.IsStdPackage(path) &&
+ moreiters.Contains(stdlib.Dependencies(pkgs...), path)
+}
+
var (
builtinAny = types.Universe.Lookup("any")
builtinAppend = types.Universe.Lookup("append")
@@ -137,3 +186,60 @@ var (
byteSliceType = types.NewSlice(types.Typ[types.Byte])
omitemptyRegex = regexp.MustCompile(`(?:^json| json):"[^"]*(,omitempty)(?:"|,[^"]*")\s?`)
)
+
+// enabledCategory reports whether a given category is enabled by the specified
+// filter. filter is a comma-separated list of categories, optionally prefixed
+// with `-` to disable all provided categories. All categories are enabled with
+// an empty filter.
+//
+// (Will be superseded by https://go.dev/cl/655555.)
+func enabledCategory(filter, category string) bool {
+ if filter == "" {
+ return true
+ }
+ // negation must be specified at the start
+ filter, exclude := strings.CutPrefix(filter, "-")
+ filters := strings.Split(filter, ",")
+ if slices.Contains(filters, category) {
+ return !exclude
+ }
+ return exclude
+}
+
+// noEffects reports whether the expression has no side effects, i.e., it
+// does not modify the memory state. This function is conservative: it may
+// return false even when the expression has no effect.
+func noEffects(info *types.Info, expr ast.Expr) bool {
+ noEffects := true
+ ast.Inspect(expr, func(n ast.Node) bool {
+ switch v := n.(type) {
+ case nil, *ast.Ident, *ast.BasicLit, *ast.BinaryExpr, *ast.ParenExpr,
+ *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr, *ast.TypeAssertExpr,
+ *ast.StarExpr, *ast.CompositeLit, *ast.ArrayType, *ast.StructType,
+ *ast.MapType, *ast.InterfaceType, *ast.KeyValueExpr:
+ // No effect
+ case *ast.UnaryExpr:
+ // Channel send <-ch has effects
+ if v.Op == token.ARROW {
+ noEffects = false
+ }
+ case *ast.CallExpr:
+ // Type conversion has no effects
+ if !info.Types[v].IsType() {
+ // TODO(adonovan): Add a case for built-in functions without side
+ // effects (by using callsPureBuiltin from tools/internal/refactor/inline)
+
+ noEffects = false
+ }
+ case *ast.FuncLit:
+ // A FuncLit has no effects, but do not descend into it.
+ return false
+ default:
+ // All other expressions have effects
+ noEffects = false
+ }
+
+ return noEffects
+ })
+ return noEffects
+}
diff --git a/gopls/internal/analysis/modernize/modernize_test.go b/gopls/internal/analysis/modernize/modernize_test.go
index 6662914b28d..e823e983995 100644
--- a/gopls/internal/analysis/modernize/modernize_test.go
+++ b/gopls/internal/analysis/modernize/modernize_test.go
@@ -17,14 +17,19 @@ func Test(t *testing.T) {
"bloop",
"efaceany",
"fmtappendf",
+ "forvar",
"mapsloop",
"minmax",
"omitzero",
"rangeint",
"slicescontains",
"slicesdelete",
+ "stringscutprefix",
+ "stringscutprefix/bytescutprefix",
"splitseq",
+ "fieldsseq",
"sortslice",
"testingcontext",
+ "waitgroup",
)
}
diff --git a/gopls/internal/analysis/modernize/rangeint.go b/gopls/internal/analysis/modernize/rangeint.go
index c36203cef06..1d3f4b5db0c 100644
--- a/gopls/internal/analysis/modernize/rangeint.go
+++ b/gopls/internal/analysis/modernize/rangeint.go
@@ -8,13 +8,18 @@ import (
"fmt"
"go/ast"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/astutil/edge"
+ "golang.org/x/tools/internal/typesinternal"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
)
// rangeint offers a fix to replace a 3-clause 'for' loop:
@@ -36,13 +41,23 @@ import (
// - The limit must not be b.N, to avoid redundancy with bloop's fixes.
//
// Caveats:
-// - The fix will cause the limit expression to be evaluated exactly
-// once, instead of once per iteration. The limit may be a function call
-// (e.g. seq.Len()). The fix may change the cardinality of side effects.
+//
+// The fix causes the limit expression to be evaluated exactly once,
+// instead of once per iteration. So, to avoid changing the
+// cardinality of side effects, the limit expression must not involve
+// function calls (e.g. seq.Len()) or channel receives. Moreover, the
+// value of the limit expression must be loop invariant, which in
+// practice means it must take one of the following forms:
+//
+// - a local variable that is assigned only once and not address-taken;
+// - a constant; or
+// - len(s), where s has the above properties.
func rangeint(pass *analysis.Pass) {
info := pass.TypesInfo
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ typeindex := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+
for curFile := range filesUsing(inspect, info, "go1.22") {
nextLoop:
for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) {
@@ -50,7 +65,7 @@ func rangeint(pass *analysis.Pass) {
if init, ok := loop.Init.(*ast.AssignStmt); ok &&
isSimpleAssign(init) &&
is[*ast.Ident](init.Lhs[0]) &&
- isZeroLiteral(init.Rhs[0]) {
+ isZeroIntLiteral(info, init.Rhs[0]) {
// Have: for i = 0; ... (or i := 0)
index := init.Lhs[0].(*ast.Ident)
@@ -58,13 +73,41 @@ func rangeint(pass *analysis.Pass) {
compare.Op == token.LSS &&
equalSyntax(compare.X, init.Lhs[0]) {
// Have: for i = 0; i < limit; ... {}
+
limit := compare.Y
- // Skip loops up to b.N in benchmarks; see [bloop].
- if sel, ok := limit.(*ast.SelectorExpr); ok &&
- sel.Sel.Name == "N" &&
- analysisinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") {
- continue // skip b.N
+ // If limit is "len(slice)", simplify it to "slice".
+ //
+ // (Don't replace "for i := 0; i < len(map); i++"
+ // with "for range m" because it's too hard to prove
+ // that len(m) is loop-invariant).
+ if call, ok := limit.(*ast.CallExpr); ok &&
+ typeutil.Callee(info, call) == builtinLen &&
+ is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
+ limit = call.Args[0]
+ }
+
+ // Check the form of limit: must be a constant,
+ // or a local var that is not assigned or address-taken.
+ limitOK := false
+ if info.Types[limit].Value != nil {
+ limitOK = true // constant
+ } else if id, ok := limit.(*ast.Ident); ok {
+ if v, ok := info.Uses[id].(*types.Var); ok &&
+ !(v.Exported() && typesinternal.IsPackageLevel(v)) {
+ // limit is a local or unexported global var.
+ // (An exported global may have uses we can't see.)
+ for cur := range typeindex.Uses(v) {
+ if isScalarLvalue(info, cur) {
+ // Limit var is assigned or address-taken.
+ continue nextLoop
+ }
+ }
+ limitOK = true
+ }
+ }
+ if !limitOK {
+ continue nextLoop
}
if inc, ok := loop.Post.(*ast.IncDecStmt); ok &&
@@ -73,7 +116,7 @@ func rangeint(pass *analysis.Pass) {
// Have: for i = 0; i < limit; i++ {}
// Find references to i within the loop body.
- v := info.Defs[index]
+ v := info.ObjectOf(index)
used := false
for curId := range curLoop.Child(loop.Body).Preorder((*ast.Ident)(nil)) {
id := curId.Node().(*ast.Ident)
@@ -83,7 +126,7 @@ func rangeint(pass *analysis.Pass) {
// Reject if any is an l-value (assigned or address-taken):
// a "for range int" loop does not respect assignments to
// the loop variable.
- if isScalarLvalue(curId) {
+ if isScalarLvalue(info, curId) {
continue nextLoop
}
}
@@ -98,6 +141,59 @@ func rangeint(pass *analysis.Pass) {
})
}
+ // If i is used after the loop,
+ // don't offer a fix, as a range loop
+ // leaves i with a different final value (limit-1).
+ if init.Tok == token.ASSIGN {
+ for curId := range curLoop.Parent().Preorder((*ast.Ident)(nil)) {
+ id := curId.Node().(*ast.Ident)
+ if id.Pos() > loop.End() && info.Uses[id] == v {
+ continue nextLoop
+ }
+ }
+ }
+
+ // If limit is len(slice),
+ // simplify "range len(slice)" to "range slice".
+ if call, ok := limit.(*ast.CallExpr); ok &&
+ typeutil.Callee(info, call) == builtinLen &&
+ is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
+ limit = call.Args[0]
+ }
+
+ // If the limit is a untyped constant of non-integer type,
+ // such as "const limit = 1e3", its effective type may
+ // differ between the two forms.
+ // In a for loop, it must be comparable with int i,
+ // for i := 0; i < limit; i++
+ // but in a range loop it would become a float,
+ // for i := range limit {}
+ // which is a type error. We need to convert it to int
+ // in this case.
+ //
+ // Unfortunately go/types discards the untyped type
+ // (but see Untyped in golang/go#70638) so we must
+ // re-type check the expression to detect this case.
+ var beforeLimit, afterLimit string
+ if v := info.Types[limit].Value; v != nil {
+ tVar := info.TypeOf(init.Rhs[0])
+
+ // TODO(adonovan): use a types.Qualifier that respects the existing
+ // imports of this file that are visible (not shadowed) at the current position,
+ // and adds new imports as needed, similar to analysisinternal.AddImport.
+ // (Unfortunately types.Qualifier doesn't provide the name of the package
+ // member to be qualified, a qualifier cannot perform the necessary shadowing
+ // check for dot-imported names.)
+ beforeLimit, afterLimit = fmt.Sprintf("%s(", types.TypeString(tVar, types.RelativeTo(pass.Pkg))), ")"
+ info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
+ if types.CheckExpr(pass.Fset, pass.Pkg, limit.Pos(), limit, info2) == nil {
+ tLimit := types.Default(info2.TypeOf(limit))
+ if types.AssignableTo(tLimit, tVar) {
+ beforeLimit, afterLimit = "", ""
+ }
+ }
+ }
+
pass.Report(analysis.Diagnostic{
Pos: init.Pos(),
End: inc.End(),
@@ -111,15 +207,30 @@ func rangeint(pass *analysis.Pass) {
// ----- ---
// -------
// for i := range limit {}
+
+ // Delete init.
{
Pos: init.Rhs[0].Pos(),
End: limit.Pos(),
NewText: []byte("range "),
},
+ // Add "int(" before limit, if needed.
+ {
+ Pos: limit.Pos(),
+ End: limit.Pos(),
+ NewText: []byte(beforeLimit),
+ },
+ // Delete inc.
{
Pos: limit.End(),
End: inc.End(),
},
+ // Add ")" after limit, if needed.
+ {
+ Pos: limit.End(),
+ End: limit.End(),
+ NewText: []byte(afterLimit),
+ },
}...),
}},
})
@@ -135,7 +246,7 @@ func rangeint(pass *analysis.Pass) {
//
// This function is valid only for scalars (x = ...),
// not for aggregates (x.a[i] = ...)
-func isScalarLvalue(curId cursor.Cursor) bool {
+func isScalarLvalue(info *types.Info, curId cursor.Cursor) bool {
// Unfortunately we can't simply use info.Types[e].Assignable()
// as it is always true for a variable even when that variable is
// used only as an r-value. So we must inspect enclosing syntax.
@@ -143,15 +254,22 @@ func isScalarLvalue(curId cursor.Cursor) bool {
cur := curId
// Strip enclosing parens.
- ek, _ := cur.Edge()
+ ek, _ := cur.ParentEdge()
for ek == edge.ParenExpr_X {
cur = cur.Parent()
- ek, _ = cur.Edge()
+ ek, _ = cur.ParentEdge()
}
switch ek {
case edge.AssignStmt_Lhs:
- return true // i = j
+ assign := cur.Parent().Node().(*ast.AssignStmt)
+ if assign.Tok != token.DEFINE {
+ return true // i = j or i += j
+ }
+ id := curId.Node().(*ast.Ident)
+ if v, ok := info.Defs[id]; ok && v.Pos() != id.Pos() {
+ return true // reassignment of i (i, j := 1, 2)
+ }
case edge.IncDecStmt_X:
return true // i++, i--
case edge.UnaryExpr_X:
diff --git a/gopls/internal/analysis/modernize/slices.go b/gopls/internal/analysis/modernize/slices.go
index bdab9dea649..18e02d51ebf 100644
--- a/gopls/internal/analysis/modernize/slices.go
+++ b/gopls/internal/analysis/modernize/slices.go
@@ -12,6 +12,7 @@ import (
"go/ast"
"go/types"
"slices"
+ "strconv"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -27,6 +28,10 @@ import (
// with a call to go1.21's slices.Concat(base, a, b, c), or simpler
// replacements such as slices.Clone(a) in degenerate cases.
//
+// We offer bytes.Clone in preference to slices.Clone where
+// appropriate, if the package already imports "bytes";
+// their behaviors are identical.
+//
// 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.
@@ -41,7 +46,9 @@ import (
// 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" {
+ // Skip the analyzer in packages where its
+ // fixes would create an import cycle.
+ if within(pass, "slices", "bytes", "runtime") {
return
}
@@ -94,15 +101,32 @@ func appendclipped(pass *analysis.Pass) {
}
}
- // append(zerocap, s...) -> slices.Clone(s)
- _, prefix, importEdits := analysisinternal.AddImport(info, file, "slices", "slices", "Clone", call.Pos())
+ // If the slice type is []byte, and the file imports
+ // "bytes" but not "slices", prefer the (behaviorally
+ // identical) bytes.Clone for local consistency.
+ // https://go.dev/issue/70815#issuecomment-2671572984
+ fileImports := func(path string) bool {
+ return slices.ContainsFunc(file.Imports, func(spec *ast.ImportSpec) bool {
+ value, _ := strconv.Unquote(spec.Path.Value)
+ return value == path
+ })
+ }
+ clonepkg := cond(
+ types.Identical(info.TypeOf(call), byteSliceType) &&
+ !fileImports("slices") && fileImports("bytes"),
+ "bytes",
+ "slices")
+
+ // append(zerocap, s...) -> slices.Clone(s) or bytes.Clone(s)
+ _, prefix, importEdits := analysisinternal.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos())
+ message := fmt.Sprintf("Replace append with %s.Clone", clonepkg)
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
Category: "slicesclone",
- Message: "Replace append with slices.Clone",
+ Message: message,
SuggestedFixes: []analysis.SuggestedFix{{
- Message: "Replace append with slices.Clone",
+ Message: message,
TextEdits: append(importEdits, []analysis.TextEdit{{
Pos: call.Pos(),
End: call.End(),
@@ -186,13 +210,16 @@ func appendclipped(pass *analysis.Pass) {
// x[:len(x):len(x)] (nonempty) res=x
// x[:k:k] (nonempty)
// slices.Clip(x) (nonempty) res=x
+//
+// TODO(adonovan): Add a check that the expression x has no side effects in
+// case x[:len(x):len(x)] -> x. Now the program behavior may change.
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]
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]
+ empty = isZeroIntLiteral(info, e.High) // x[:0:0]
if call, ok := e.High.(*ast.CallExpr); ok &&
typeutil.Callee(info, call) == builtinLen &&
equalSyntax(call.Args[0], e.X) {
diff --git a/gopls/internal/analysis/modernize/slicescontains.go b/gopls/internal/analysis/modernize/slicescontains.go
index 09642448bb5..78a569eeca9 100644
--- a/gopls/internal/analysis/modernize/slicescontains.go
+++ b/gopls/internal/analysis/modernize/slicescontains.go
@@ -15,8 +15,10 @@ import (
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/typeparams"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
)
// The slicescontains pass identifies loops that can be replaced by a
@@ -46,13 +48,21 @@ import (
// It may change cardinality of effects of the "needle" expression.
// (Mostly this appears to be a desirable optimization, avoiding
// redundantly repeated evaluation.)
+//
+// TODO(adonovan): Add a check that needle/predicate expression from
+// if-statement has no effects. Now the program behavior may change.
func slicescontains(pass *analysis.Pass) {
- // Don't modify the slices package itself.
- if pass.Pkg.Path() == "slices" {
+ // Skip the analyzer in packages where its
+ // fixes would create an import cycle.
+ if within(pass, "slices", "runtime") {
return
}
- info := pass.TypesInfo
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
+ )
// check is called for each RangeStmt of this form:
// for i, elem := range s { if cond { ... } }
@@ -119,6 +129,11 @@ func slicescontains(pass *analysis.Pass) {
isSliceElem(cond.Args[0]) &&
typeutil.Callee(info, cond) != nil { // not a conversion
+ // skip variadic functions
+ if sig, ok := info.TypeOf(cond.Fun).(*types.Signature); ok && sig.Variadic() {
+ return
+ }
+
funcName = "ContainsFunc"
arg2 = cond.Fun // "if predicate(elem)"
}
@@ -140,8 +155,8 @@ func slicescontains(pass *analysis.Pass) {
if !ok {
panic(fmt.Sprintf("FindNode(%T) failed", n))
}
- return uses(info, cur, info.Defs[rng.Key.(*ast.Ident)]) ||
- rng.Value != nil && uses(info, cur, info.Defs[rng.Value.(*ast.Ident)])
+ return uses(index, cur, info.Defs[rng.Key.(*ast.Ident)]) ||
+ rng.Value != nil && uses(index, cur, info.Defs[rng.Value.(*ast.Ident)])
}
if usesRangeVar(body) {
// Body uses range var "i" or "elem".
@@ -345,7 +360,6 @@ func slicescontains(pass *analysis.Pass) {
}
}
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
for curFile := range filesUsing(inspect, info, "go1.21") {
file := curFile.Node().(*ast.File)
diff --git a/gopls/internal/analysis/modernize/slicesdelete.go b/gopls/internal/analysis/modernize/slicesdelete.go
index 24b2182ca6a..493009c35be 100644
--- a/gopls/internal/analysis/modernize/slicesdelete.go
+++ b/gopls/internal/analysis/modernize/slicesdelete.go
@@ -21,6 +21,12 @@ import (
// 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) {
+ // 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) {
@@ -88,7 +94,7 @@ func slicesdelete(pass *analysis.Pass) {
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) &&
+ 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)
diff --git a/gopls/internal/analysis/modernize/sortslice.go b/gopls/internal/analysis/modernize/sortslice.go
index 7f695d76495..bbd04e9293d 100644
--- a/gopls/internal/analysis/modernize/sortslice.go
+++ b/gopls/internal/analysis/modernize/sortslice.go
@@ -5,16 +5,14 @@
package modernize
import (
- "fmt"
"go/ast"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
)
// The sortslice pass replaces sort.Slice(slice, less) with
@@ -37,18 +35,19 @@ import (
// - sort.Sort(x) where x has a named slice type whose Less method is the natural order.
// -> sort.Slice(x)
func sortslice(pass *analysis.Pass) {
- if !analysisinternal.Imports(pass.Pkg, "sort") {
+ // Skip the analyzer in packages where its
+ // fixes would create an import cycle.
+ if within(pass, "slices", "sort", "runtime") {
return
}
- info := pass.TypesInfo
-
- check := func(file *ast.File, call *ast.CallExpr) {
- // call to sort.Slice?
- obj := typeutil.Callee(info, call)
- if !analysisinternal.IsFunctionNamed(obj, "sort", "Slice") {
- return
- }
+ var (
+ info = pass.TypesInfo
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ sortSlice = index.Object("sort", "Slice")
+ )
+ for curCall := range index.Calls(sortSlice) {
+ call := curCall.Node().(*ast.CallExpr)
if lit, ok := call.Args[1].(*ast.FuncLit); ok && len(lit.Body.List) == 1 {
sig := info.Types[lit.Type].Type.(*types.Signature)
@@ -57,57 +56,50 @@ func sortslice(pass *analysis.Pass) {
i := sig.Params().At(0)
j := sig.Params().At(1)
- ret := lit.Body.List[0].(*ast.ReturnStmt)
- if compare, ok := ret.Results[0].(*ast.BinaryExpr); ok && compare.Op == token.LSS {
- // isIndex reports whether e is s[v].
- isIndex := func(e ast.Expr, v *types.Var) bool {
- index, ok := e.(*ast.IndexExpr)
- return ok &&
- equalSyntax(index.X, s) &&
- is[*ast.Ident](index.Index) &&
- info.Uses[index.Index.(*ast.Ident)] == v
- }
- if isIndex(compare.X, i) && isIndex(compare.Y, j) {
- // Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
+ if ret, ok := lit.Body.List[0].(*ast.ReturnStmt); ok {
+ if compare, ok := ret.Results[0].(*ast.BinaryExpr); ok && compare.Op == token.LSS {
+ // isIndex reports whether e is s[v].
+ isIndex := func(e ast.Expr, v *types.Var) bool {
+ index, ok := e.(*ast.IndexExpr)
+ return ok &&
+ equalSyntax(index.X, s) &&
+ is[*ast.Ident](index.Index) &&
+ info.Uses[index.Index.(*ast.Ident)] == v
+ }
+ file := enclosingFile(curCall)
+ if isIndex(compare.X, i) && isIndex(compare.Y, j) &&
+ fileUses(info, file, "go1.21") {
+ // Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
- _, prefix, importEdits := analysisinternal.AddImport(
- info, file, "slices", "slices", "Sort", call.Pos())
+ _, prefix, importEdits := analysisinternal.AddImport(
+ info, file, "slices", "slices", "Sort", call.Pos())
- pass.Report(analysis.Diagnostic{
- // Highlight "sort.Slice".
- Pos: call.Fun.Pos(),
- End: call.Fun.End(),
- Category: "sortslice",
- Message: fmt.Sprintf("sort.Slice can be modernized using slices.Sort"),
- SuggestedFixes: []analysis.SuggestedFix{{
- Message: fmt.Sprintf("Replace sort.Slice call by slices.Sort"),
- TextEdits: append(importEdits, []analysis.TextEdit{
- {
- // Replace sort.Slice with slices.Sort.
- Pos: call.Fun.Pos(),
- End: call.Fun.End(),
- NewText: []byte(prefix + "Sort"),
- },
- {
- // Eliminate FuncLit.
- Pos: call.Args[0].End(),
- End: call.Rparen,
- },
- }...),
- }},
- })
+ pass.Report(analysis.Diagnostic{
+ // Highlight "sort.Slice".
+ Pos: call.Fun.Pos(),
+ End: call.Fun.End(),
+ Category: "sortslice",
+ Message: "sort.Slice can be modernized using slices.Sort",
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: "Replace sort.Slice call by slices.Sort",
+ TextEdits: append(importEdits, []analysis.TextEdit{
+ {
+ // Replace sort.Slice with slices.Sort.
+ Pos: call.Fun.Pos(),
+ End: call.Fun.End(),
+ NewText: []byte(prefix + "Sort"),
+ },
+ {
+ // Eliminate FuncLit.
+ Pos: call.Args[0].End(),
+ End: call.Rparen,
+ },
+ }...),
+ }},
+ })
+ }
}
}
}
}
-
- 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)
- check(file, call)
- }
- }
}
diff --git a/gopls/internal/analysis/modernize/stringscutprefix.go b/gopls/internal/analysis/modernize/stringscutprefix.go
new file mode 100644
index 00000000000..f8e9be63e3c
--- /dev/null
+++ b/gopls/internal/analysis/modernize/stringscutprefix.go
@@ -0,0 +1,205 @@
+// Copyright 2025 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/token"
+
+ "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"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
+)
+
+// stringscutprefix offers a fix to replace an if statement which
+// calls to the 2 patterns below with strings.CutPrefix.
+//
+// Patterns:
+//
+// 1. if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre) }
+// =>
+// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
+//
+// 2. if after := strings.TrimPrefix(s, pre); after != s { use(after) }
+// =>
+// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
+//
+// The use must occur within the first statement of the block, and the offered fix
+// only replaces the first occurrence of strings.TrimPrefix.
+//
+// Variants:
+// - bytes.HasPrefix usage as pattern 1.
+func stringscutprefix(pass *analysis.Pass) {
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
+
+ stringsTrimPrefix = index.Object("strings", "TrimPrefix")
+ bytesTrimPrefix = index.Object("bytes", "TrimPrefix")
+ )
+ if !index.Used(stringsTrimPrefix, bytesTrimPrefix) {
+ return
+ }
+
+ const (
+ category = "stringscutprefix"
+ fixedMessage = "Replace HasPrefix/TrimPrefix with CutPrefix"
+ )
+
+ for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.20") {
+ for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
+ ifStmt := curIfStmt.Node().(*ast.IfStmt)
+
+ // pattern1
+ if call, ok := ifStmt.Cond.(*ast.CallExpr); ok && len(ifStmt.Body.List) > 0 {
+ obj := typeutil.Callee(info, call)
+ if !analysisinternal.IsFunctionNamed(obj, "strings", "HasPrefix") &&
+ !analysisinternal.IsFunctionNamed(obj, "bytes", "HasPrefix") {
+ continue
+ }
+
+ // Replace the first occurrence of strings.TrimPrefix(s, pre) in the first statement only,
+ // but not later statements in case s or pre are modified by intervening logic.
+ firstStmt := curIfStmt.Child(ifStmt.Body).Child(ifStmt.Body.List[0])
+ for curCall := range firstStmt.Preorder((*ast.CallExpr)(nil)) {
+ call1 := curCall.Node().(*ast.CallExpr)
+ obj1 := typeutil.Callee(info, call1)
+ // bytesTrimPrefix or stringsTrimPrefix might be nil if the file doesn't import it,
+ // so we need to ensure the obj1 is not nil otherwise the call1 is not TrimPrefix and cause a panic.
+ if obj1 == nil ||
+ obj1 != stringsTrimPrefix && obj1 != bytesTrimPrefix {
+ continue
+ }
+ // Have: if strings.HasPrefix(s0, pre0) { ...strings.TrimPrefix(s, pre)... }
+ var (
+ s0 = call.Args[0]
+ pre0 = call.Args[1]
+ s = call1.Args[0]
+ pre = call1.Args[1]
+ )
+
+ // check whether the obj1 uses the exact the same argument with strings.HasPrefix
+ // shadow variables won't be valid because we only access the first statement.
+ if equalSyntax(s0, s) && equalSyntax(pre0, pre) {
+ after := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "after")
+ _, prefix, importEdits := analysisinternal.AddImport(
+ info,
+ curFile.Node().(*ast.File),
+ obj1.Pkg().Name(),
+ obj1.Pkg().Path(),
+ "CutPrefix",
+ call.Pos(),
+ )
+ okVarName := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
+ pass.Report(analysis.Diagnostic{
+ // highlight at HasPrefix call.
+ Pos: call.Pos(),
+ End: call.End(),
+ Category: category,
+ Message: "HasPrefix + TrimPrefix can be simplified to CutPrefix",
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: fixedMessage,
+ // if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre)) }
+ // ------------ ----------------- ----- --------------------------
+ // if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
+ TextEdits: append(importEdits, []analysis.TextEdit{
+ {
+ Pos: call.Fun.Pos(),
+ End: call.Fun.Pos(),
+ NewText: fmt.Appendf(nil, "%s, %s :=", after, okVarName),
+ },
+ {
+ Pos: call.Fun.Pos(),
+ End: call.Fun.End(),
+ NewText: fmt.Appendf(nil, "%sCutPrefix", prefix),
+ },
+ {
+ Pos: call.End(),
+ End: call.End(),
+ NewText: fmt.Appendf(nil, "; %s ", okVarName),
+ },
+ {
+ Pos: call1.Pos(),
+ End: call1.End(),
+ NewText: []byte(after),
+ },
+ }...),
+ }}},
+ )
+ break
+ }
+ }
+ }
+
+ // pattern2
+ if bin, ok := ifStmt.Cond.(*ast.BinaryExpr); ok &&
+ bin.Op == token.NEQ &&
+ ifStmt.Init != nil &&
+ isSimpleAssign(ifStmt.Init) {
+ assign := ifStmt.Init.(*ast.AssignStmt)
+ if call, ok := assign.Rhs[0].(*ast.CallExpr); ok && assign.Tok == token.DEFINE {
+ lhs := assign.Lhs[0]
+ obj := typeutil.Callee(info, call)
+ if obj == stringsTrimPrefix &&
+ (equalSyntax(lhs, bin.X) && equalSyntax(call.Args[0], bin.Y) ||
+ (equalSyntax(lhs, bin.Y) && equalSyntax(call.Args[0], bin.X))) {
+ okVarName := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
+ // Have one of:
+ // if rest := TrimPrefix(s, prefix); rest != s {
+ // if rest := TrimPrefix(s, prefix); s != rest {
+
+ // We use AddImport not to add an import (since it exists already)
+ // but to compute the correct prefix in the dot-import case.
+ _, prefix, importEdits := analysisinternal.AddImport(
+ info,
+ curFile.Node().(*ast.File),
+ obj.Pkg().Name(),
+ obj.Pkg().Path(),
+ "CutPrefix",
+ call.Pos(),
+ )
+
+ pass.Report(analysis.Diagnostic{
+ // highlight from the init and the condition end.
+ Pos: ifStmt.Init.Pos(),
+ End: ifStmt.Cond.End(),
+ Category: category,
+ Message: "TrimPrefix can be simplified to CutPrefix",
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: fixedMessage,
+ // if x := strings.TrimPrefix(s, pre); x != s ...
+ // ---- ---------- ------
+ // if x, ok := strings.CutPrefix (s, pre); ok ...
+ TextEdits: append(importEdits, []analysis.TextEdit{
+ {
+ Pos: assign.Lhs[0].End(),
+ End: assign.Lhs[0].End(),
+ NewText: fmt.Appendf(nil, ", %s", okVarName),
+ },
+ {
+ Pos: call.Fun.Pos(),
+ End: call.Fun.End(),
+ NewText: fmt.Appendf(nil, "%sCutPrefix", prefix),
+ },
+ {
+ Pos: ifStmt.Cond.Pos(),
+ End: ifStmt.Cond.End(),
+ NewText: []byte(okVarName),
+ },
+ }...),
+ }},
+ })
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/gopls/internal/analysis/modernize/splitseq.go b/gopls/internal/analysis/modernize/stringsseq.go
similarity index 63%
rename from gopls/internal/analysis/modernize/splitseq.go
rename to gopls/internal/analysis/modernize/stringsseq.go
index 1f3da859e9b..d32f8be754f 100644
--- a/gopls/internal/analysis/modernize/splitseq.go
+++ b/gopls/internal/analysis/modernize/stringsseq.go
@@ -5,6 +5,7 @@
package modernize
import (
+ "fmt"
"go/ast"
"go/token"
"go/types"
@@ -13,12 +14,14 @@ import (
"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"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil/edge"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
)
-// splitseq offers a fix to replace a call to strings.Split with
-// SplitSeq when it is the operand of a range loop, either directly:
+// stringsseq offers a fix to replace a call to strings.Split with
+// SplitSeq or strings.Fields with FieldsSeq
+// when it is the operand of a range loop, either directly:
//
// for _, line := range strings.Split() {...}
//
@@ -29,13 +32,22 @@ import (
//
// Variants:
// - bytes.SplitSeq
-func splitseq(pass *analysis.Pass) {
- if !analysisinternal.Imports(pass.Pkg, "strings") &&
- !analysisinternal.Imports(pass.Pkg, "bytes") {
+// - bytes.FieldsSeq
+func stringsseq(pass *analysis.Pass) {
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
+
+ stringsSplit = index.Object("strings", "Split")
+ stringsFields = index.Object("strings", "Fields")
+ bytesSplit = index.Object("bytes", "Split")
+ bytesFields = index.Object("bytes", "Fields")
+ )
+ if !index.Used(stringsSplit, stringsFields, bytesSplit, bytesFields) {
return
}
- info := pass.TypesInfo
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+
for curFile := range filesUsing(inspect, info, "go1.24") {
for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
rng := curRange.Node().(*ast.RangeStmt)
@@ -52,14 +64,14 @@ func splitseq(pass *analysis.Pass) {
if !ok {
if id, ok := rng.X.(*ast.Ident); ok {
if v, ok := info.Uses[id].(*types.Var); ok {
- if ek, idx := curRange.Edge(); ek == edge.BlockStmt_List && idx > 0 {
+ if ek, idx := curRange.ParentEdge(); ek == edge.BlockStmt_List && idx > 0 {
curPrev, _ := curRange.PrevSibling()
if assign, ok := curPrev.Node().(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE &&
len(assign.Lhs) == 1 &&
len(assign.Rhs) == 1 &&
info.Defs[assign.Lhs[0].(*ast.Ident)] == v &&
- soleUse(info, v) == id {
+ soleUseIs(index, v, id) {
// Have:
// lines := ...
// for _, line := range lines {...}
@@ -88,21 +100,26 @@ func splitseq(pass *analysis.Pass) {
})
}
- if sel, ok := call.Fun.(*ast.SelectorExpr); ok &&
- (analysisinternal.IsFunctionNamed(typeutil.Callee(info, call), "strings", "Split") ||
- analysisinternal.IsFunctionNamed(typeutil.Callee(info, call), "bytes", "Split")) {
+ sel, ok := call.Fun.(*ast.SelectorExpr)
+ if !ok {
+ continue
+ }
+
+ switch obj := typeutil.Callee(info, call); obj {
+ case stringsSplit, stringsFields, bytesSplit, bytesFields:
+ oldFnName := obj.Name()
+ seqFnName := fmt.Sprintf("%sSeq", oldFnName)
pass.Report(analysis.Diagnostic{
Pos: sel.Pos(),
End: sel.End(),
- Category: "splitseq",
- Message: "Ranging over SplitSeq is more efficient",
+ Category: "stringsseq",
+ Message: fmt.Sprintf("Ranging over %s is more efficient", seqFnName),
SuggestedFixes: []analysis.SuggestedFix{{
- Message: "Replace Split with SplitSeq",
+ Message: fmt.Sprintf("Replace %s with %s", oldFnName, seqFnName),
TextEdits: append(edits, analysis.TextEdit{
- // Split -> SplitSeq
Pos: sel.Sel.Pos(),
End: sel.Sel.End(),
- NewText: []byte("SplitSeq")}),
+ NewText: []byte(seqFnName)}),
}},
})
}
diff --git a/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go b/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go
new file mode 100644
index 00000000000..6425211b924
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go
@@ -0,0 +1,11 @@
+package appendclipped
+
+import (
+ "bytes"
+)
+
+var _ bytes.Buffer
+
+func _(b []byte) {
+ print(append([]byte{}, b...)) // want "Replace append with bytes.Clone"
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go.golden b/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go.golden
new file mode 100644
index 00000000000..f49be6156b2
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go.golden
@@ -0,0 +1,11 @@
+package appendclipped
+
+import (
+ "bytes"
+)
+
+var _ bytes.Buffer
+
+func _(b []byte) {
+ print(bytes.Clone(b)) // want "Replace append with bytes.Clone"
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go
new file mode 100644
index 00000000000..b86df1a8a94
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go
@@ -0,0 +1,42 @@
+//go:build go1.24
+
+package fieldsseq
+
+import (
+ "bytes"
+ "strings"
+)
+
+func _() {
+ for _, line := range strings.Fields("") { // want "Ranging over FieldsSeq is more efficient"
+ println(line)
+ }
+ for i, line := range strings.Fields("") { // nope: uses index var
+ println(i, line)
+ }
+ for i, _ := range strings.Fields("") { // nope: uses index var
+ println(i)
+ }
+ for i := range strings.Fields("") { // nope: uses index var
+ println(i)
+ }
+ for _ = range strings.Fields("") { // want "Ranging over FieldsSeq is more efficient"
+ }
+ for range strings.Fields("") { // want "Ranging over FieldsSeq is more efficient"
+ }
+ for range bytes.Fields(nil) { // want "Ranging over FieldsSeq is more efficient"
+ }
+ {
+ lines := strings.Fields("") // want "Ranging over FieldsSeq is more efficient"
+ for _, line := range lines {
+ println(line)
+ }
+ }
+ {
+ lines := strings.Fields("") // nope: lines is used not just by range
+ for _, line := range lines {
+ println(line)
+ }
+ println(lines)
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go.golden b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go.golden
new file mode 100644
index 00000000000..9fa1bfd1b62
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go.golden
@@ -0,0 +1,42 @@
+//go:build go1.24
+
+package fieldsseq
+
+import (
+ "bytes"
+ "strings"
+)
+
+func _() {
+ for line := range strings.FieldsSeq("") { // want "Ranging over FieldsSeq is more efficient"
+ println(line)
+ }
+ for i, line := range strings.Fields( "") { // nope: uses index var
+ println(i, line)
+ }
+ for i, _ := range strings.Fields( "") { // nope: uses index var
+ println(i)
+ }
+ for i := range strings.Fields( "") { // nope: uses index var
+ println(i)
+ }
+ for range strings.FieldsSeq("") { // want "Ranging over FieldsSeq is more efficient"
+ }
+ for range strings.FieldsSeq("") { // want "Ranging over FieldsSeq is more efficient"
+ }
+ for range bytes.FieldsSeq(nil) { // want "Ranging over FieldsSeq is more efficient"
+ }
+ {
+ lines := strings.FieldsSeq("") // want "Ranging over FieldsSeq is more efficient"
+ for line := range lines {
+ println(line)
+ }
+ }
+ {
+ lines := strings.Fields( "") // nope: lines is used not just by range
+ for _, line := range lines {
+ println(line)
+ }
+ println(lines)
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq_go123.go b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq_go123.go
new file mode 100644
index 00000000000..c2bd314db75
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq_go123.go
@@ -0,0 +1 @@
+package fieldsseq
diff --git a/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go b/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go
index a39a03ee786..a435b6a6461 100644
--- a/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go
+++ b/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go
@@ -29,8 +29,8 @@ func typealias() {
}
func otherprints() {
- sprint := []byte(fmt.Sprint("bye %d", 1)) // want "Replace .*Sprintf.* with fmt.Appendf"
+ sprint := []byte(fmt.Sprint("bye %d", 1)) // want "Replace .*Sprint.* with fmt.Append"
print(sprint)
- sprintln := []byte(fmt.Sprintln("bye %d", 1)) // want "Replace .*Sprintf.* with fmt.Appendf"
+ sprintln := []byte(fmt.Sprintln("bye %d", 1)) // want "Replace .*Sprintln.* with fmt.Appendln"
print(sprintln)
}
diff --git a/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go.golden b/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go.golden
index 7c8aa7b9a5e..4fd2b136b82 100644
--- a/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go.golden
@@ -29,8 +29,8 @@ func typealias() {
}
func otherprints() {
- sprint := fmt.Append(nil, "bye %d", 1) // want "Replace .*Sprintf.* with fmt.Appendf"
+ sprint := fmt.Append(nil, "bye %d", 1) // want "Replace .*Sprint.* with fmt.Append"
print(sprint)
- sprintln := fmt.Appendln(nil, "bye %d", 1) // want "Replace .*Sprintf.* with fmt.Appendf"
+ sprintln := fmt.Appendln(nil, "bye %d", 1) // want "Replace .*Sprintln.* with fmt.Appendln"
print(sprintln)
}
\ No newline at end of file
diff --git a/gopls/internal/analysis/modernize/testdata/src/forvar/forvar.go b/gopls/internal/analysis/modernize/testdata/src/forvar/forvar.go
new file mode 100644
index 00000000000..dd5ecd75e29
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/forvar/forvar.go
@@ -0,0 +1,62 @@
+package forvar
+
+func _(m map[int]int, s []int) {
+ // changed
+ for i := range s {
+ i := i // want "copying variable is unneeded"
+ go f(i)
+ }
+ for _, v := range s {
+ v := v // want "copying variable is unneeded"
+ go f(v)
+ }
+ for k, v := range m {
+ k := k // want "copying variable is unneeded"
+ v := v // nope: report only the first redeclaration
+ go f(k)
+ go f(v)
+ }
+ for _, v := range m {
+ v := v // want "copying variable is unneeded"
+ go f(v)
+ }
+ for i := range s {
+ /* hi */ i := i // want "copying variable is unneeded"
+ go f(i)
+ }
+ // nope
+ var i, k, v int
+
+ for i = range s { // nope, scope change
+ i := i
+ go f(i)
+ }
+ for _, v = range s { // nope, scope change
+ v := v
+ go f(v)
+ }
+ for k = range m { // nope, scope change
+ k := k
+ go f(k)
+ }
+ for k, v = range m { // nope, scope change
+ k := k
+ v := v
+ go f(k)
+ go f(v)
+ }
+ for _, v = range m { // nope, scope change
+ v := v
+ go f(v)
+ }
+ for _, v = range m { // nope, not x := x
+ v := i
+ go f(v)
+ }
+ for i := range s {
+ i := (i)
+ go f(i)
+ }
+}
+
+func f(n int) {}
diff --git a/gopls/internal/analysis/modernize/testdata/src/forvar/forvar.go.golden b/gopls/internal/analysis/modernize/testdata/src/forvar/forvar.go.golden
new file mode 100644
index 00000000000..35f71404c35
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/forvar/forvar.go.golden
@@ -0,0 +1,62 @@
+package forvar
+
+func _(m map[int]int, s []int) {
+ // changed
+ for i := range s {
+ // want "copying variable is unneeded"
+ go f(i)
+ }
+ for _, v := range s {
+ // want "copying variable is unneeded"
+ go f(v)
+ }
+ for k, v := range m {
+ // want "copying variable is unneeded"
+ v := v // nope: report only the first redeclaration
+ go f(k)
+ go f(v)
+ }
+ for _, v := range m {
+ // want "copying variable is unneeded"
+ go f(v)
+ }
+ for i := range s {
+ /* hi */ // want "copying variable is unneeded"
+ go f(i)
+ }
+ // nope
+ var i, k, v int
+
+ for i = range s { // nope, scope change
+ i := i
+ go f(i)
+ }
+ for _, v = range s { // nope, scope change
+ v := v
+ go f(v)
+ }
+ for k = range m { // nope, scope change
+ k := k
+ go f(k)
+ }
+ for k, v = range m { // nope, scope change
+ k := k
+ v := v
+ go f(k)
+ go f(v)
+ }
+ for _, v = range m { // nope, scope change
+ v := v
+ go f(v)
+ }
+ for _, v = range m { // nope, not x := x
+ v := i
+ go f(v)
+ }
+ for i := range s {
+ i := (i)
+ go f(i)
+ }
+}
+
+func f(n int) {}
diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go
index 769b4c84f60..7d0f7d17e91 100644
--- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go
+++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go
@@ -16,6 +16,7 @@ type M map[int]string
func useCopy(dst, src map[int]string) {
// Replace loop by maps.Copy.
for key, value := range src {
+ // A
dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
}
}
@@ -23,15 +24,45 @@ func useCopy(dst, src map[int]string) {
func useCopyGeneric[K comparable, V any, M ~map[K]V](dst, src M) {
// Replace loop by maps.Copy.
for key, value := range src {
+ // A
dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
}
}
-func useClone(src map[int]string) {
- // Replace make(...) by maps.Clone.
+func useCopyNotClone(src map[int]string) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
+ // Replace make(...) by maps.Copy.
dst := make(map[int]string, len(src))
+ // A
+ for key, value := range src {
+ // B
+ dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
+ // C
+ }
+
+ // A
+ dst = map[int]string{}
+ // B
for key, value := range src {
- dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone"
+ // C
+ dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
+ }
+ println(dst)
+}
+
+func useCopyParen(src map[int]string) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
+ // Replace (make)(...) by maps.Clone.
+ dst := (make)(map[int]string, len(src))
+ for key, value := range src {
+ dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
+ }
+
+ dst = (map[int]string{})
+ for key, value := range src {
+ dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
}
println(dst)
}
@@ -55,32 +86,38 @@ func useCopy_typesDiffer2(src map[int]string) {
}
func useClone_typesDiffer3(src map[int]string) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
// Replace loop and make(...) as maps.Clone(src) returns map[int]string
// which is assignable to M.
var dst M
dst = make(M, len(src))
for key, value := range src {
- dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone"
+ dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
}
println(dst)
}
func useClone_typesDiffer4(src map[int]string) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
// Replace loop and make(...) as maps.Clone(src) returns map[int]string
// which is assignable to M.
var dst M
dst = make(M, len(src))
for key, value := range src {
- dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone"
+ dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
}
println(dst)
}
func useClone_generic[Map ~map[K]V, K comparable, V any](src Map) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
// Replace loop and make(...) by maps.Clone
dst := make(Map, len(src))
for key, value := range src {
- dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone"
+ dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
}
println(dst)
}
@@ -97,8 +134,10 @@ func useInsert_assignableToSeq2(dst map[int]string, src func(yield func(int, str
func useCollect(src iter.Seq2[int, string]) {
// Replace loop and make(...) by maps.Collect.
var dst map[int]string
- dst = make(map[int]string)
+ dst = make(map[int]string) // A
+ // B
for key, value := range src {
+ // C
dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Collect"
}
}
@@ -108,7 +147,9 @@ func useInsert_typesDifferAssign(src iter.Seq2[int, string]) {
// that is assignable to M.
var dst M
dst = make(M)
+ // A
for key, value := range src {
+ // B
dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Collect"
}
}
diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden
index b9aa39021e8..9136105b908 100644
--- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden
@@ -15,23 +15,56 @@ type M map[int]string
func useCopy(dst, src map[int]string) {
// Replace loop by maps.Copy.
+ // A
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
maps.Copy(dst, src)
}
func useCopyGeneric[K comparable, V any, M ~map[K]V](dst, src M) {
// Replace loop by maps.Copy.
+ // A
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
maps.Copy(dst, src)
}
-func useClone(src map[int]string) {
- // Replace make(...) by maps.Clone.
- dst := maps.Clone(src)
+func useCopyNotClone(src map[int]string) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
+ // Replace make(...) by maps.Copy.
+ dst := make(map[int]string, len(src))
+ // A
+ // B
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
+ // C
+ maps.Copy(dst, src)
+
+ // A
+ dst = map[int]string{}
+ // B
+ // C
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
+ maps.Copy(dst, src)
+ println(dst)
+}
+
+func useCopyParen(src map[int]string) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
+ // Replace (make)(...) by maps.Clone.
+ dst := (make)(map[int]string, len(src))
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
+ maps.Copy(dst, src)
+
+ dst = (map[int]string{})
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
+ maps.Copy(dst, src)
println(dst)
}
func useCopy_typesDiffer(src M) {
// Replace loop but not make(...) as maps.Copy(src) would return wrong type M.
dst := make(map[int]string, len(src))
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
maps.Copy(dst, src)
println(dst)
}
@@ -39,29 +72,42 @@ func useCopy_typesDiffer(src M) {
func useCopy_typesDiffer2(src map[int]string) {
// Replace loop but not make(...) as maps.Copy(src) would return wrong type map[int]string.
dst := make(M, len(src))
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
maps.Copy(dst, src)
println(dst)
}
func useClone_typesDiffer3(src map[int]string) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
// Replace loop and make(...) as maps.Clone(src) returns map[int]string
// which is assignable to M.
var dst M
- dst = maps.Clone(src)
+ dst = make(M, len(src))
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
+ maps.Copy(dst, src)
println(dst)
}
func useClone_typesDiffer4(src map[int]string) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
// Replace loop and make(...) as maps.Clone(src) returns map[int]string
// which is assignable to M.
var dst M
- dst = maps.Clone(src)
+ dst = make(M, len(src))
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
+ maps.Copy(dst, src)
println(dst)
}
func useClone_generic[Map ~map[K]V, K comparable, V any](src Map) {
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
// Replace loop and make(...) by maps.Clone
- dst := maps.Clone(src)
+ dst := make(Map, len(src))
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
+ maps.Copy(dst, src)
println(dst)
}
@@ -69,12 +115,17 @@ func useClone_generic[Map ~map[K]V, K comparable, V any](src Map) {
func useInsert_assignableToSeq2(dst map[int]string, src func(yield func(int, string) bool)) {
// Replace loop by maps.Insert because src is assignable to iter.Seq2.
+ // want "Replace m\\[k\\]=v loop with maps.Insert"
maps.Insert(dst, src)
}
func useCollect(src iter.Seq2[int, string]) {
// Replace loop and make(...) by maps.Collect.
var dst map[int]string
+ // A
+ // B
+ // C
+ // want "Replace m\\[k\\]=v loop with maps.Collect"
dst = maps.Collect(src)
}
@@ -82,6 +133,9 @@ func useInsert_typesDifferAssign(src iter.Seq2[int, string]) {
// Replace loop and make(...): maps.Collect returns an unnamed map type
// that is assignable to M.
var dst M
+ // A
+ // B
+ // want "Replace m\\[k\\]=v loop with maps.Collect"
dst = maps.Collect(src)
}
@@ -89,6 +143,7 @@ func useInsert_typesDifferDeclare(src iter.Seq2[int, string]) {
// Replace loop but not make(...) as maps.Collect would return an
// unnamed map type that would change the type of dst.
dst := make(M)
+ // want "Replace m\\[k\\]=v loop with maps.Insert"
maps.Insert(dst, src)
}
diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go
index c33d43e23ad..ae28f11afda 100644
--- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go
+++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go
@@ -14,10 +14,12 @@ func useCopyDot(dst, src map[int]string) {
}
func useCloneDot(src map[int]string) {
- // Replace make(...) by maps.Clone.
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
+ // Replace make(...) by maps.Copy.
dst := make(map[int]string, len(src))
for key, value := range src {
- dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone"
+ dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy"
}
println(dst)
}
diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go.golden b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go.golden
index d6a30537645..6347d56360a 100644
--- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go.golden
@@ -8,12 +8,16 @@ var _ = Clone[M] // force "maps" import so that each diagnostic doesn't add one
func useCopyDot(dst, src map[int]string) {
// Replace loop by maps.Copy.
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
Copy(dst, src)
}
func useCloneDot(src map[int]string) {
- // Replace make(...) by maps.Clone.
- dst := Clone(src)
+ // Clone is tempting but wrong when src may be nil; see #71844.
+
+ // Replace make(...) by maps.Copy.
+ dst := make(map[int]string, len(src))
+ // want "Replace m\\[k\\]=v loop with maps.Copy"
+ Copy(dst, src)
println(dst)
}
-
diff --git a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go
index c73bd30139b..cdc767450d2 100644
--- a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go
+++ b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go
@@ -1,9 +1,12 @@
package minmax
func ifmin(a, b int) {
- x := a
+ x := a // A
+ // B
if a < b { // want "if statement can be modernized using max"
- x = b
+ // C
+ x = b // D
+ // E
}
print(x)
}
@@ -33,20 +36,30 @@ func ifmaxvariant(a, b int) {
}
func ifelsemin(a, b int) {
- var x int
+ var x int // A
+ // B
if a <= b { // want "if/else statement can be modernized using min"
- x = a
+ // C
+ x = a // D
+ // E
} else {
- x = b
+ // F
+ x = b // G
+ // H
}
print(x)
}
func ifelsemax(a, b int) {
- var x int
+ // A
+ var x int // B
+ // C
if a >= b { // want "if/else statement can be modernized using max"
- x = a
+ // D
+ x = a // E
+ // F
} else {
+ // G
x = b
}
print(x)
@@ -92,3 +105,54 @@ func nopeAssignHasIncrementOperator() {
}
print(y)
}
+
+// Regression test for https://github.com/golang/go/issues/71721.
+func nopeNotAMinimum(x, y int) int {
+ // A value of -1 or 0 will use a default value (30).
+ if x <= 0 {
+ y = 30
+ } else {
+ y = x
+ }
+ return y
+}
+
+// Regression test for https://github.com/golang/go/issues/71847#issuecomment-2673491596
+func nopeHasElseBlock(x int) int {
+ y := x
+ // Before, this was erroneously reduced to y = max(x, 0)
+ if y < 0 {
+ y = 0
+ } else {
+ y += 2
+ }
+ return y
+}
+
+func fix72727(a, b int) {
+ o := a - 42
+ // some important comment. DO NOT REMOVE.
+ if o < b { // want "if statement can be modernized using max"
+ o = b
+ }
+}
+
+type myfloat float64
+
+// The built-in min/max differ in their treatement of NaN,
+// so reject floating-point numbers (#72829).
+func nopeFloat(a, b myfloat) (res myfloat) {
+ if a < b {
+ res = a
+ } else {
+ res = b
+ }
+ return
+}
+
+// Regression test for golang/go#72928.
+func underscoreAssign(a, b int) {
+ if a > b {
+ _ = a
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden
index 11eac2c1418..b7be86bf416 100644
--- a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden
@@ -1,33 +1,57 @@
package minmax
func ifmin(a, b int) {
+ // A
+ // B
+ // want "if statement can be modernized using max"
+ // C
+ // D
+ // E
x := max(a, b)
print(x)
}
func ifmax(a, b int) {
+ // want "if statement can be modernized using min"
x := min(a, b)
print(x)
}
func ifminvariant(a, b int) {
+ // want "if statement can be modernized using min"
x := min(a, b)
print(x)
}
func ifmaxvariant(a, b int) {
+ // want "if statement can be modernized using min"
x := min(a, b)
print(x)
}
func ifelsemin(a, b int) {
- var x int
+ var x int // A
+ // B
+ // want "if/else statement can be modernized using min"
+ // C
+ // D
+ // E
+ // F
+ // G
+ // H
x = min(a, b)
print(x)
}
func ifelsemax(a, b int) {
- var x int
+ // A
+ var x int // B
+ // C
+ // want "if/else statement can be modernized using max"
+ // D
+ // E
+ // F
+ // G
x = max(a, b)
print(x)
}
@@ -55,6 +79,7 @@ func nopeIfStmtHasInitStmt() {
// Regression test for a bug: fix was "y := max(x, y)".
func oops() {
x := 1
+ // want "if statement can be modernized using max"
y := max(x, 2)
print(y)
}
@@ -69,3 +94,52 @@ func nopeAssignHasIncrementOperator() {
}
print(y)
}
+
+// Regression test for https://github.com/golang/go/issues/71721.
+func nopeNotAMinimum(x, y int) int {
+ // A value of -1 or 0 will use a default value (30).
+ if x <= 0 {
+ y = 30
+ } else {
+ y = x
+ }
+ return y
+}
+
+// Regression test for https://github.com/golang/go/issues/71847#issuecomment-2673491596
+func nopeHasElseBlock(x int) int {
+ y := x
+ // Before, this was erroneously reduced to y = max(x, 0)
+ if y < 0 {
+ y = 0
+ } else {
+ y += 2
+ }
+ return y
+}
+
+func fix72727(a, b int) {
+ // some important comment. DO NOT REMOVE.
+ // want "if statement can be modernized using max"
+ o := max(a-42, b)
+}
+
+type myfloat float64
+
+// The built-in min/max differ in their treatement of NaN,
+// so reject floating-point numbers (#72829).
+func nopeFloat(a, b myfloat) (res myfloat) {
+ if a < b {
+ res = a
+ } else {
+ res = b
+ }
+ return
+}
+
+// Regression test for golang/go#72928.
+func underscoreAssign(a, b int) {
+ if a > b {
+ _ = a
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go
index e17dccac9d0..74f3488546c 100644
--- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go
+++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go
@@ -1,16 +1,74 @@
package rangeint
-func _(i int, s struct{ i int }) {
+import (
+ "os"
+ os1 "os"
+)
+
+func _(i int, s struct{ i int }, slice []int) {
for i := 0; i < 10; i++ { // want "for loop can be modernized using range over int"
println(i)
}
- for i = 0; i < f(); i++ { // want "for loop can be modernized using range over int"
+ for j := int(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := int8(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := int16(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := int32(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := int64(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := uint8(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := uint16(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := uint32(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := uint64(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := int8(0.); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := int8(.0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := os.FileMode(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+
+ {
+ var i int
+ for i = 0; i < 10; i++ { // want "for loop can be modernized using range over int"
+ }
+ // NB: no uses of i after loop.
}
for i := 0; i < 10; i++ { // want "for loop can be modernized using range over int"
// i unused within loop
}
+ for i := 0; i < len(slice); i++ { // want "for loop can be modernized using range over int"
+ println(slice[i])
+ }
+ for i := 0; i < len(""); i++ { // want "for loop can be modernized using range over int"
+ // NB: not simplified to range ""
+ }
// nope
+ for j := .0; j < 10; j++ { // nope: j is a float type
+ println(j)
+ }
+ for j := float64(0); j < 10; j++ { // nope: j is a float type
+ println(j)
+ }
for i := 0; i < 10; { // nope: missing increment
}
for i := 0; i < 10; i-- { // nope: negative increment
@@ -32,6 +90,144 @@ func _(i int, s struct{ i int }) {
for i := 0; i < 10; i++ { // nope: assigns i
i = 8
}
+
+ // The limit expression must be loop invariant;
+ // see https://github.com/golang/go/issues/72917
+ for i := 0; i < f(); i++ { // nope
+ }
+ {
+ var s struct{ limit int }
+ for i := 0; i < s.limit; i++ { // nope: limit is not a const or local var
+ }
+ }
+ {
+ const k = 10
+ for i := 0; i < k; i++ { // want "for loop can be modernized using range over int"
+ }
+ }
+ {
+ var limit = 10
+ for i := 0; i < limit; i++ { // want "for loop can be modernized using range over int"
+ }
+ }
+ {
+ var limit = 10
+ for i := 0; i < limit; i++ { // nope: limit is address-taken
+ }
+ print(&limit)
+ }
+ {
+ limit := 10
+ limit++
+ for i := 0; i < limit; i++ { // nope: limit is assigned other than by its declaration
+ }
+ }
+ for i := 0; i < Global; i++ { // nope: limit is an exported global var; may be updated elsewhere
+ }
+ for i := 0; i < len(table); i++ { // want "for loop can be modernized using range over int"
+ }
+ {
+ s := []string{}
+ for i := 0; i < len(s); i++ { // nope: limit is not loop-invariant
+ s = s[1:]
+ }
+ }
+ for i := 0; i < len(slice); i++ { // nope: i is incremented within loop
+ i += 1
+ }
}
+var Global int
+
+var table = []string{"hello", "world"}
+
func f() int { return 0 }
+
+// Repro for part of #71847: ("for range n is invalid if the loop body contains i++"):
+func _(s string) {
+ var i int // (this is necessary)
+ for i = 0; i < len(s); i++ { // nope: loop body increments i
+ if true {
+ i++ // nope
+ }
+ }
+}
+
+// Repro for #71952: for and range loops have different final values
+// on i (n and n-1, respectively) so we can't offer the fix if i is
+// used after the loop.
+func nopePostconditionDiffers() {
+ i := 0
+ for i = 0; i < 5; i++ {
+ println(i)
+ }
+ println(i) // must print 5, not 4
+}
+
+// Non-integer untyped constants need to be explicitly converted to int.
+func issue71847d() {
+ const limit = 1e3 // float
+ for i := 0; i < limit; i++ { // want "for loop can be modernized using range over int"
+ }
+ for i := int(0); i < limit; i++ { // want "for loop can be modernized using range over int"
+ }
+ for i := uint(0); i < limit; i++ { // want "for loop can be modernized using range over int"
+ }
+
+ const limit2 = 1 + 0i // complex
+ for i := 0; i < limit2; i++ { // want "for loop can be modernized using range over int"
+ }
+}
+
+func issue72726() {
+ var n, kd int
+ for i := 0; i < n; i++ { // want "for loop can be modernized using range over int"
+ // nope: j will be invisible once it's refactored to 'for j := range min(n-j, kd+1)'
+ for j := 0; j < min(n-j, kd+1); j++ { // nope
+ _, _ = i, j
+ }
+ }
+
+ for i := 0; i < i; i++ { // nope
+ }
+
+ var i int
+ for i = 0; i < i/2; i++ { // nope
+ }
+
+ var arr []int
+ for i = 0; i < arr[i]; i++ { // nope
+ }
+}
+
+func todo() {
+ for j := os1.FileMode(0); j < 10; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+}
+
+type T uint
+type TAlias = uint
+
+func Fn(a int) T {
+ return T(a)
+}
+
+func issue73037() {
+ var q T
+ for a := T(0); a < q; a++ { // want "for loop can be modernized using range over int"
+ println(a)
+ }
+ for a := Fn(0); a < q; a++ {
+ println(a)
+ }
+ var qa TAlias
+ for a := TAlias(0); a < qa; a++ { // want "for loop can be modernized using range over int"
+ println(a)
+ }
+ for a := T(0); a < 10; a++ { // want "for loop can be modernized using range over int"
+ for b := T(0); b < 10; b++ { // want "for loop can be modernized using range over int"
+ println(a, b)
+ }
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden
index 5a76229c858..cdd2f118997 100644
--- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden
@@ -1,16 +1,74 @@
package rangeint
-func _(i int, s struct{ i int }) {
+import (
+ "os"
+ os1 "os"
+)
+
+func _(i int, s struct{ i int }, slice []int) {
for i := range 10 { // want "for loop can be modernized using range over int"
println(i)
}
- for i = range f() { // want "for loop can be modernized using range over int"
+ for j := range 10 { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range int8(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range int16(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range int32(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range int64(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range uint8(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range uint16(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range uint32(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range uint64(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range int8(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range int8(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+ for j := range os.FileMode(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+
+ {
+ var i int
+ for i = range 10 { // want "for loop can be modernized using range over int"
+ }
+ // NB: no uses of i after loop.
}
for range 10 { // want "for loop can be modernized using range over int"
// i unused within loop
}
+ for i := range slice { // want "for loop can be modernized using range over int"
+ println(slice[i])
+ }
+ for range len("") { // want "for loop can be modernized using range over int"
+ // NB: not simplified to range ""
+ }
// nope
+ for j := .0; j < 10; j++ { // nope: j is a float type
+ println(j)
+ }
+ for j := float64(0); j < 10; j++ { // nope: j is a float type
+ println(j)
+ }
for i := 0; i < 10; { // nope: missing increment
}
for i := 0; i < 10; i-- { // nope: negative increment
@@ -32,6 +90,144 @@ func _(i int, s struct{ i int }) {
for i := 0; i < 10; i++ { // nope: assigns i
i = 8
}
+
+ // The limit expression must be loop invariant;
+ // see https://github.com/golang/go/issues/72917
+ for i := 0; i < f(); i++ { // nope
+ }
+ {
+ var s struct{ limit int }
+ for i := 0; i < s.limit; i++ { // nope: limit is not a const or local var
+ }
+ }
+ {
+ const k = 10
+ for range k { // want "for loop can be modernized using range over int"
+ }
+ }
+ {
+ var limit = 10
+ for range limit { // want "for loop can be modernized using range over int"
+ }
+ }
+ {
+ var limit = 10
+ for i := 0; i < limit; i++ { // nope: limit is address-taken
+ }
+ print(&limit)
+ }
+ {
+ limit := 10
+ limit++
+ for i := 0; i < limit; i++ { // nope: limit is assigned other than by its declaration
+ }
+ }
+ for i := 0; i < Global; i++ { // nope: limit is an exported global var; may be updated elsewhere
+ }
+ for range table { // want "for loop can be modernized using range over int"
+ }
+ {
+ s := []string{}
+ for i := 0; i < len(s); i++ { // nope: limit is not loop-invariant
+ s = s[1:]
+ }
+ }
+ for i := 0; i < len(slice); i++ { // nope: i is incremented within loop
+ i += 1
+ }
}
+var Global int
+
+var table = []string{"hello", "world"}
+
func f() int { return 0 }
+
+// Repro for part of #71847: ("for range n is invalid if the loop body contains i++"):
+func _(s string) {
+ var i int // (this is necessary)
+ for i = 0; i < len(s); i++ { // nope: loop body increments i
+ if true {
+ i++ // nope
+ }
+ }
+}
+
+// Repro for #71952: for and range loops have different final values
+// on i (n and n-1, respectively) so we can't offer the fix if i is
+// used after the loop.
+func nopePostconditionDiffers() {
+ i := 0
+ for i = 0; i < 5; i++ {
+ println(i)
+ }
+ println(i) // must print 5, not 4
+}
+
+// Non-integer untyped constants need to be explicitly converted to int.
+func issue71847d() {
+ const limit = 1e3 // float
+ for range int(limit) { // want "for loop can be modernized using range over int"
+ }
+ for range int(limit) { // want "for loop can be modernized using range over int"
+ }
+ for range uint(limit) { // want "for loop can be modernized using range over int"
+ }
+
+ const limit2 = 1 + 0i // complex
+ for range int(limit2) { // want "for loop can be modernized using range over int"
+ }
+}
+
+func issue72726() {
+ var n, kd int
+ for i := range n { // want "for loop can be modernized using range over int"
+ // nope: j will be invisible once it's refactored to 'for j := range min(n-j, kd+1)'
+ for j := 0; j < min(n-j, kd+1); j++ { // nope
+ _, _ = i, j
+ }
+ }
+
+ for i := 0; i < i; i++ { // nope
+ }
+
+ var i int
+ for i = 0; i < i/2; i++ { // nope
+ }
+
+ var arr []int
+ for i = 0; i < arr[i]; i++ { // nope
+ }
+}
+
+func todo() {
+ for j := range os.FileMode(10) { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+}
+
+type T uint
+type TAlias = uint
+
+func Fn(a int) T {
+ return T(a)
+}
+
+func issue73037() {
+ var q T
+ for a := range q { // want "for loop can be modernized using range over int"
+ println(a)
+ }
+ for a := Fn(0); a < q; a++ {
+ println(a)
+ }
+ var qa TAlias
+ for a := range qa { // want "for loop can be modernized using range over int"
+ println(a)
+ }
+ for a := range T(10) { // want "for loop can be modernized using range over int"
+ for b := range T(10) { // want "for loop can be modernized using range over int"
+ println(a, b)
+ }
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/slicescontains/slicescontains.go b/gopls/internal/analysis/modernize/testdata/src/slicescontains/slicescontains.go
index 6116ce14838..03bcfc69904 100644
--- a/gopls/internal/analysis/modernize/testdata/src/slicescontains/slicescontains.go
+++ b/gopls/internal/analysis/modernize/testdata/src/slicescontains/slicescontains.go
@@ -146,3 +146,26 @@ func nopeNeedleHaystackDifferentTypes2(x error, args []any) {
}
}
}
+
+func nopeVariadicNamedContainsFunc(slice []int) bool {
+ for _, elem := range slice {
+ if variadicPredicate(elem) {
+ return true
+ }
+ }
+ return false
+}
+
+func variadicPredicate(int, ...any) bool
+
+func nopeVariadicContainsFunc(slice []int) bool {
+ f := func(int, ...any) bool {
+ return true
+ }
+ for _, elem := range slice {
+ if f(elem) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/slicescontains/slicescontains.go.golden b/gopls/internal/analysis/modernize/testdata/src/slicescontains/slicescontains.go.golden
index 2d67395f203..67e5b544960 100644
--- a/gopls/internal/analysis/modernize/testdata/src/slicescontains/slicescontains.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/slicescontains/slicescontains.go.golden
@@ -102,3 +102,26 @@ func nopeNeedleHaystackDifferentTypes2(x error, args []any) {
}
}
}
+
+func nopeVariadicNamedContainsFunc(slice []int) bool {
+ for _, elem := range slice {
+ if variadicPredicate(elem) {
+ return true
+ }
+ }
+ return false
+}
+
+func variadicPredicate(int, ...any) bool
+
+func nopeVariadicContainsFunc(slice []int) bool {
+ f := func(int, ...any) bool {
+ return true
+ }
+ for _, elem := range slice {
+ if f(elem) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go b/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go
index a710d06f2fe..0ee608d8f9f 100644
--- a/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go
+++ b/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go
@@ -2,6 +2,10 @@ package slicesdelete
var g struct{ f []int }
+func h() []int { return []int{} }
+
+var ch chan []int
+
func slicesdelete(test, other []byte, i int) {
const k = 1
_ = append(test[:i], test[i+1:]...) // want "Replace append with slices.Delete"
@@ -26,6 +30,10 @@ func slicesdelete(test, other []byte, i int) {
_ = append(g.f[:i], g.f[i+k:]...) // want "Replace append with slices.Delete"
+ _ = append(h()[:i], h()[i+1:]...) // potentially has side effects
+
+ _ = append((<-ch)[:i], (<-ch)[i+1:]...) // has side effects
+
_ = append(test[:3], test[i+1:]...) // cannot verify a < b
_ = append(test[:i-4], test[i-1:]...) // want "Replace append with slices.Delete"
diff --git a/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go.golden b/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go.golden
index 9b2ba9a0b80..a15eb07dee9 100644
--- a/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go.golden
@@ -2,51 +2,45 @@ package slicesdelete
import "slices"
-import "slices"
-
-import "slices"
-
-import "slices"
-
-import "slices"
+var g struct{ f []int }
-import "slices"
+func h() []int { return []int{} }
-import "slices"
+var ch chan []int
-import "slices"
+func slicesdelete(test, other []byte, i int) {
+ const k = 1
+ _ = slices.Delete(test, i, i+1) // want "Replace append with slices.Delete"
-var g struct{ f []int }
+ _ = slices.Delete(test, i+1, i+2) // want "Replace append with slices.Delete"
-func slicesdelete(test, other []byte, i int) {
- const k = 1
- _ = slices.Delete(test, i, i+1) // want "Replace append with slices.Delete"
+ _ = append(test[:i+1], test[i+1:]...) // not deleting any slice elements
- _ = slices.Delete(test, i+1, i+2) // want "Replace append with slices.Delete"
+ _ = append(test[:i], test[i-1:]...) // not deleting any slice elements
- _ = append(test[:i+1], test[i+1:]...) // not deleting any slice elements
+ _ = slices.Delete(test, i-1, i) // want "Replace append with slices.Delete"
- _ = append(test[:i], test[i-1:]...) // not deleting any slice elements
+ _ = slices.Delete(test, i-2, i+1) // want "Replace append with slices.Delete"
- _ = slices.Delete(test, i-1, i) // want "Replace append with slices.Delete"
+ _ = append(test[:i-2], other[i+1:]...) // different slices "test" and "other"
- _ = slices.Delete(test, i-2, i+1) // want "Replace append with slices.Delete"
+ _ = append(test[:i-2], other[i+1+k:]...) // cannot verify a < b
- _ = append(test[:i-2], other[i+1:]...) // different slices "test" and "other"
+ _ = append(test[:i-2], test[11:]...) // cannot verify a < b
- _ = append(test[:i-2], other[i+1+k:]...) // cannot verify a < b
+ _ = slices.Delete(test, 1, 3) // want "Replace append with slices.Delete"
- _ = append(test[:i-2], test[11:]...) // cannot verify a < b
+ _ = slices.Delete(g.f, i, i+k) // want "Replace append with slices.Delete"
- _ = slices.Delete(test, 1, 3) // want "Replace append with slices.Delete"
+ _ = append(h()[:i], h()[i+1:]...) // potentially has side effects
- _ = slices.Delete(g.f, i, i+k) // want "Replace append with slices.Delete"
+ _ = append((<-ch)[:i], (<-ch)[i+1:]...) // has side effects
- _ = append(test[:3], test[i+1:]...) // cannot verify a < b
+ _ = append(test[:3], test[i+1:]...) // cannot verify a < b
- _ = slices.Delete(test, i-4, i-1) // want "Replace append with slices.Delete"
+ _ = slices.Delete(test, i-4, i-1) // want "Replace append with slices.Delete"
- _ = slices.Delete(test, 1+2, 3+4) // want "Replace append with slices.Delete"
+ _ = slices.Delete(test, 1+2, 3+4) // want "Replace append with slices.Delete"
- _ = append(test[:1+2], test[i-1:]...) // cannot verify a < b
-}
\ No newline at end of file
+ _ = append(test[:1+2], test[i-1:]...) // cannot verify a < b
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go
index 53d15746839..19242065b24 100644
--- a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go
+++ b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go
@@ -20,6 +20,16 @@ func _(s []int) {
sort.Slice(s, func(i, j int) bool { return s[j] < s[i] }) // nope: wrong index var
}
-func _(s2 []struct{ x int }) {
+func _(sense bool, s2 []struct{ x int }) {
sort.Slice(s2, func(i, j int) bool { return s2[i].x < s2[j].x }) // nope: not a simple index operation
+
+ // Regression test for a crash: the sole statement of a
+ // comparison func body is not necessarily a return!
+ sort.Slice(s2, func(i, j int) bool {
+ if sense {
+ return s2[i].x < s2[j].x
+ } else {
+ return s2[i].x > s2[j].x
+ }
+ })
}
diff --git a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden
index d97636fd311..19149b4480a 100644
--- a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden
+++ b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden
@@ -2,8 +2,6 @@ package sortslice
import "slices"
-import "slices"
-
import "sort"
type myint int
@@ -24,6 +22,16 @@ func _(s []int) {
sort.Slice(s, func(i, j int) bool { return s[j] < s[i] }) // nope: wrong index var
}
-func _(s2 []struct{ x int }) {
+func _(sense bool, s2 []struct{ x int }) {
sort.Slice(s2, func(i, j int) bool { return s2[i].x < s2[j].x }) // nope: not a simple index operation
+
+ // Regression test for a crash: the sole statement of a
+ // comparison func body is not necessarily a return!
+ sort.Slice(s2, func(i, j int) bool {
+ if sense {
+ return s2[i].x < s2[j].x
+ } else {
+ return s2[i].x > s2[j].x
+ }
+ })
}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix.go b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix.go
new file mode 100644
index 00000000000..7c5363e6c8d
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix.go
@@ -0,0 +1,16 @@
+package bytescutprefix
+
+import (
+ "bytes"
+)
+
+func _() {
+ if bytes.HasPrefix(bss, bspre) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := bytes.TrimPrefix(bss, bspre)
+ _ = a
+ }
+ if bytes.HasPrefix([]byte(""), []byte("")) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := bytes.TrimPrefix([]byte(""), []byte(""))
+ _ = a
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix.go.golden b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix.go.golden
new file mode 100644
index 00000000000..8d41a8bf343
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix.go.golden
@@ -0,0 +1,16 @@
+package bytescutprefix
+
+import (
+ "bytes"
+)
+
+func _() {
+ if after, ok := bytes.CutPrefix(bss, bspre); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := after
+ _ = a
+ }
+ if after, ok := bytes.CutPrefix([]byte(""), []byte("")); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := after
+ _ = a
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix_dot.go b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix_dot.go
new file mode 100644
index 00000000000..bfde6b7a461
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix_dot.go
@@ -0,0 +1,15 @@
+package bytescutprefix
+
+import (
+ . "bytes"
+)
+
+var bss, bspre []byte
+
+// test supported cases of pattern 1
+func _() {
+ if HasPrefix(bss, bspre) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := TrimPrefix(bss, bspre)
+ _ = a
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix_dot.go.golden b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix_dot.go.golden
new file mode 100644
index 00000000000..8eb562e7940
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix_dot.go.golden
@@ -0,0 +1,15 @@
+package bytescutprefix
+
+import (
+ . "bytes"
+)
+
+var bss, bspre []byte
+
+// test supported cases of pattern 1
+func _() {
+ if after, ok := CutPrefix(bss, bspre); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := after
+ _ = a
+ }
+}
\ No newline at end of file
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix.go b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix.go
new file mode 100644
index 00000000000..7679bdb6e67
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix.go
@@ -0,0 +1,124 @@
+package stringscutprefix
+
+import (
+ "strings"
+)
+
+var (
+ s, pre string
+)
+
+// test supported cases of pattern 1
+func _() {
+ if strings.HasPrefix(s, pre) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := strings.TrimPrefix(s, pre)
+ _ = a
+ }
+ if strings.HasPrefix("", "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := strings.TrimPrefix("", "")
+ _ = a
+ }
+ if strings.HasPrefix(s, "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ println([]byte(strings.TrimPrefix(s, "")))
+ }
+ if strings.HasPrefix(s, "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a, b := "", strings.TrimPrefix(s, "")
+ _, _ = a, b
+ }
+ if strings.HasPrefix(s, "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a, b := strings.TrimPrefix(s, ""), strings.TrimPrefix(s, "") // only replace the first occurrence
+ s = "123"
+ b = strings.TrimPrefix(s, "") // only replace the first occurrence
+ _, _ = a, b
+ }
+
+ var a, b string
+ if strings.HasPrefix(s, "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a, b = "", strings.TrimPrefix(s, "")
+ _, _ = a, b
+ }
+}
+
+// test cases that are not supported by pattern1
+func _() {
+ ok := strings.HasPrefix("", "")
+ if ok { // noop, currently it doesn't track the result usage of HasPrefix
+ a := strings.TrimPrefix("", "")
+ _ = a
+ }
+ if strings.HasPrefix(s, pre) {
+ a := strings.TrimPrefix("", "") // noop, as the argument isn't the same
+ _ = a
+ }
+ if strings.HasPrefix(s, pre) {
+ var result string
+ result = strings.TrimPrefix("", "") // noop, as we believe define is more popular.
+ _ = result
+ }
+ if strings.HasPrefix("", "") {
+ a := strings.TrimPrefix(s, pre) // noop, as the argument isn't the same
+ _ = a
+ }
+}
+
+var value0 string
+
+// test supported cases of pattern2
+func _() {
+ if after := strings.TrimPrefix(s, pre); after != s { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ if after := strings.TrimPrefix(s, pre); s != after { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ if after := strings.TrimPrefix(s, pre); s != after { // want "TrimPrefix can be simplified to CutPrefix"
+ println(strings.TrimPrefix(s, pre)) // noop here
+ }
+ if after := strings.TrimPrefix(s, ""); s != after { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ var ok bool // define an ok variable to test the fix won't shadow it for its if stmt body
+ _ = ok
+ if after := strings.TrimPrefix(s, pre); after != s { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ var predefined string
+ if predefined = strings.TrimPrefix(s, pre); s != predefined { // noop
+ println(predefined)
+ }
+ if predefined = strings.TrimPrefix(s, pre); s != predefined { // noop
+ println(&predefined)
+ }
+ var value string
+ if value = strings.TrimPrefix(s, pre); s != value { // noop
+ println(value)
+ }
+ lhsMap := make(map[string]string)
+ if lhsMap[""] = strings.TrimPrefix(s, pre); s != lhsMap[""] { // noop
+ println(lhsMap[""])
+ }
+ arr := make([]string, 0)
+ if arr[0] = strings.TrimPrefix(s, pre); s != arr[0] { // noop
+ println(arr[0])
+ }
+ type example struct {
+ field string
+ }
+ var e example
+ if e.field = strings.TrimPrefix(s, pre); s != e.field { // noop
+ println(e.field)
+ }
+}
+
+// test cases that not supported by pattern2
+func _() {
+ if after := strings.TrimPrefix(s, pre); s != pre { // noop
+ println(after)
+ }
+ if after := strings.TrimPrefix(s, pre); after != pre { // noop
+ println(after)
+ }
+ if strings.TrimPrefix(s, pre) != s {
+ println(strings.TrimPrefix(s, pre))
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix.go.golden b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix.go.golden
new file mode 100644
index 00000000000..a6c52b08802
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix.go.golden
@@ -0,0 +1,124 @@
+package stringscutprefix
+
+import (
+ "strings"
+)
+
+var (
+ s, pre string
+)
+
+// test supported cases of pattern 1
+func _() {
+ if after, ok := strings.CutPrefix(s, pre); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := after
+ _ = a
+ }
+ if after, ok := strings.CutPrefix("", ""); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := after
+ _ = a
+ }
+ if after, ok := strings.CutPrefix(s, ""); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ println([]byte(after))
+ }
+ if after, ok := strings.CutPrefix(s, ""); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a, b := "", after
+ _, _ = a, b
+ }
+ if after, ok := strings.CutPrefix(s, ""); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a, b := after, strings.TrimPrefix(s, "") // only replace the first occurrence
+ s = "123"
+ b = strings.TrimPrefix(s, "") // only replace the first occurrence
+ _, _ = a, b
+ }
+
+ var a, b string
+ if after, ok := strings.CutPrefix(s, ""); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a, b = "", after
+ _, _ = a, b
+ }
+}
+
+// test cases that are not supported by pattern1
+func _() {
+ ok := strings.HasPrefix("", "")
+ if ok { // noop, currently it doesn't track the result usage of HasPrefix
+ a := strings.TrimPrefix("", "")
+ _ = a
+ }
+ if strings.HasPrefix(s, pre) {
+ a := strings.TrimPrefix("", "") // noop, as the argument isn't the same
+ _ = a
+ }
+ if strings.HasPrefix(s, pre) {
+ var result string
+ result = strings.TrimPrefix("", "") // noop, as we believe define is more popular.
+ _ = result
+ }
+ if strings.HasPrefix("", "") {
+ a := strings.TrimPrefix(s, pre) // noop, as the argument isn't the same
+ _ = a
+ }
+}
+
+var value0 string
+
+// test supported cases of pattern2
+func _() {
+ if after, ok := strings.CutPrefix(s, pre); ok { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ if after, ok := strings.CutPrefix(s, pre); ok { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ if after, ok := strings.CutPrefix(s, pre); ok { // want "TrimPrefix can be simplified to CutPrefix"
+ println(strings.TrimPrefix(s, pre)) // noop here
+ }
+ if after, ok := strings.CutPrefix(s, ""); ok { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ var ok bool // define an ok variable to test the fix won't shadow it for its if stmt body
+ _ = ok
+ if after, ok0 := strings.CutPrefix(s, pre); ok0 { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ var predefined string
+ if predefined = strings.TrimPrefix(s, pre); s != predefined { // noop
+ println(predefined)
+ }
+ if predefined = strings.TrimPrefix(s, pre); s != predefined { // noop
+ println(&predefined)
+ }
+ var value string
+ if value = strings.TrimPrefix(s, pre); s != value { // noop
+ println(value)
+ }
+ lhsMap := make(map[string]string)
+ if lhsMap[""] = strings.TrimPrefix(s, pre); s != lhsMap[""] { // noop
+ println(lhsMap[""])
+ }
+ arr := make([]string, 0)
+ if arr[0] = strings.TrimPrefix(s, pre); s != arr[0] { // noop
+ println(arr[0])
+ }
+ type example struct {
+ field string
+ }
+ var e example
+ if e.field = strings.TrimPrefix(s, pre); s != e.field { // noop
+ println(e.field)
+ }
+}
+
+// test cases that not supported by pattern2
+func _() {
+ if after := strings.TrimPrefix(s, pre); s != pre { // noop
+ println(after)
+ }
+ if after := strings.TrimPrefix(s, pre); after != pre { // noop
+ println(after)
+ }
+ if strings.TrimPrefix(s, pre) != s {
+ println(strings.TrimPrefix(s, pre))
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix_dot.go b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix_dot.go
new file mode 100644
index 00000000000..75ce5bbe39b
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix_dot.go
@@ -0,0 +1,23 @@
+package stringscutprefix
+
+import (
+ . "strings"
+)
+
+// test supported cases of pattern 1
+func _() {
+ if HasPrefix(s, pre) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := TrimPrefix(s, pre)
+ _ = a
+ }
+}
+
+// test supported cases of pattern2
+func _() {
+ if after := TrimPrefix(s, pre); after != s { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ if after := TrimPrefix(s, pre); s != after { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix_dot.go.golden b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix_dot.go.golden
new file mode 100644
index 00000000000..50e3b6ff0ca
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscutprefix/stringscutprefix_dot.go.golden
@@ -0,0 +1,23 @@
+package stringscutprefix
+
+import (
+ . "strings"
+)
+
+// test supported cases of pattern 1
+func _() {
+ if after, ok := CutPrefix(s, pre); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
+ a := after
+ _ = a
+ }
+}
+
+// test supported cases of pattern2
+func _() {
+ if after, ok := CutPrefix(s, pre); ok { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+ if after, ok := CutPrefix(s, pre); ok { // want "TrimPrefix can be simplified to CutPrefix"
+ println(after)
+ }
+}
\ No newline at end of file
diff --git a/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup.go b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup.go
new file mode 100644
index 00000000000..8269235bda7
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup.go
@@ -0,0 +1,152 @@
+package waitgroup
+
+import (
+ "fmt"
+ "sync"
+)
+
+// supported case for pattern 1.
+func _() {
+ var wg sync.WaitGroup
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ defer wg.Done()
+ fmt.Println()
+ }()
+
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ defer wg.Done()
+ }()
+
+ for range 10 {
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ defer wg.Done()
+ fmt.Println()
+ }()
+ }
+}
+
+// supported case for pattern 2.
+func _() {
+ var wg sync.WaitGroup
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ fmt.Println()
+ wg.Done()
+ }()
+
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ wg.Done()
+ }()
+
+ for range 10 {
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ fmt.Println()
+ wg.Done()
+ }()
+ }
+}
+
+// this function puts some wrong usages but waitgroup modernizer will still offer fixes.
+func _() {
+ var wg sync.WaitGroup
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ defer wg.Done()
+ defer wg.Done()
+ fmt.Println()
+ }()
+
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ defer wg.Done()
+ fmt.Println()
+ wg.Done()
+ }()
+
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ fmt.Println()
+ wg.Done()
+ wg.Done()
+ }()
+}
+
+// this function puts the unsupported cases of pattern 1.
+func _() {
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {}()
+
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ fmt.Println(i)
+ }(1)
+
+ wg.Add(1)
+ go func() {
+ fmt.Println()
+ defer wg.Done()
+ }()
+
+ wg.Add(1)
+ go func() { // noop: no wg.Done call inside function body.
+ fmt.Println()
+ }()
+
+ go func() { // noop: no Add call before this go stmt.
+ defer wg.Done()
+ fmt.Println()
+ }()
+
+ wg.Add(2) // noop: only support Add(1).
+ go func() {
+ defer wg.Done()
+ }()
+
+ var wg1 sync.WaitGroup
+ wg1.Add(1) // noop: Add and Done should be the same object.
+ go func() {
+ defer wg.Done()
+ fmt.Println()
+ }()
+
+ wg.Add(1) // noop: Add and Done should be the same object.
+ go func() {
+ defer wg1.Done()
+ fmt.Println()
+ }()
+}
+
+// this function puts the unsupported cases of pattern 2.
+func _() {
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ wg.Done()
+ fmt.Println()
+ }()
+
+ go func() { // noop: no Add call before this go stmt.
+ fmt.Println()
+ wg.Done()
+ }()
+
+ var wg1 sync.WaitGroup
+ wg1.Add(1) // noop: Add and Done should be the same object.
+ go func() {
+ fmt.Println()
+ wg.Done()
+ }()
+
+ wg.Add(1) // noop: Add and Done should be the same object.
+ go func() {
+ fmt.Println()
+ wg1.Done()
+ }()
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup.go.golden b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup.go.golden
new file mode 100644
index 00000000000..dd98429da0d
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup.go.golden
@@ -0,0 +1,143 @@
+package waitgroup
+
+import (
+ "fmt"
+ "sync"
+)
+
+// supported case for pattern 1.
+func _() {
+ var wg sync.WaitGroup
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ })
+
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ })
+
+ for range 10 {
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ })
+ }
+}
+
+// supported case for pattern 2.
+func _() {
+ var wg sync.WaitGroup
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ })
+
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ })
+
+ for range 10 {
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ })
+ }
+}
+
+// this function puts some wrong usages but waitgroup modernizer will still offer fixes.
+func _() {
+ var wg sync.WaitGroup
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ defer wg.Done()
+ fmt.Println()
+ })
+
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ wg.Done()
+ })
+
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ wg.Done()
+ })
+}
+
+// this function puts the unsupported cases of pattern 1.
+func _() {
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {}()
+
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ fmt.Println(i)
+ }(1)
+
+ wg.Add(1)
+ go func() {
+ fmt.Println()
+ defer wg.Done()
+ }()
+
+ wg.Add(1)
+ go func() { // noop: no wg.Done call inside function body.
+ fmt.Println()
+ }()
+
+ go func() { // noop: no Add call before this go stmt.
+ defer wg.Done()
+ fmt.Println()
+ }()
+
+ wg.Add(2) // noop: only support Add(1).
+ go func() {
+ defer wg.Done()
+ }()
+
+ var wg1 sync.WaitGroup
+ wg1.Add(1) // noop: Add and Done should be the same object.
+ go func() {
+ defer wg.Done()
+ fmt.Println()
+ }()
+
+ wg.Add(1) // noop: Add and Done should be the same object.
+ go func() {
+ defer wg1.Done()
+ fmt.Println()
+ }()
+}
+
+// this function puts the unsupported cases of pattern 2.
+func _() {
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ wg.Done()
+ fmt.Println()
+ }()
+
+ go func() { // noop: no Add call before this go stmt.
+ fmt.Println()
+ wg.Done()
+ }()
+
+ var wg1 sync.WaitGroup
+ wg1.Add(1) // noop: Add and Done should be the same object.
+ go func() {
+ fmt.Println()
+ wg.Done()
+ }()
+
+ wg.Add(1) // noop: Add and Done should be the same object.
+ go func() {
+ fmt.Println()
+ wg1.Done()
+ }()
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_alias.go b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_alias.go
new file mode 100644
index 00000000000..087edba27be
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_alias.go
@@ -0,0 +1,21 @@
+package waitgroup
+
+import (
+ "fmt"
+ sync1 "sync"
+)
+
+func _() {
+ var wg sync1.WaitGroup
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ defer wg.Done()
+ fmt.Println()
+ }()
+
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ fmt.Println()
+ wg.Done()
+ }()
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_alias.go.golden b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_alias.go.golden
new file mode 100644
index 00000000000..377973bc689
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_alias.go.golden
@@ -0,0 +1,19 @@
+package waitgroup
+
+import (
+ "fmt"
+ sync1 "sync"
+)
+
+func _() {
+ var wg sync1.WaitGroup
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ })
+
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ })
+}
\ No newline at end of file
diff --git a/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_dot.go b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_dot.go
new file mode 100644
index 00000000000..b4d1e150dbc
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_dot.go
@@ -0,0 +1,22 @@
+package waitgroup
+
+import (
+ "fmt"
+ . "sync"
+)
+
+// supported case for pattern 1.
+func _() {
+ var wg WaitGroup
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ defer wg.Done()
+ fmt.Println()
+ }()
+
+ wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go"
+ go func() {
+ fmt.Println()
+ wg.Done()
+ }()
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_dot.go.golden b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_dot.go.golden
new file mode 100644
index 00000000000..37584be72f8
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/waitgroup/waitgroup_dot.go.golden
@@ -0,0 +1,20 @@
+package waitgroup
+
+import (
+ "fmt"
+ . "sync"
+)
+
+// supported case for pattern 1.
+func _() {
+ var wg WaitGroup
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ })
+
+ // want "Goroutine creation can be simplified using WaitGroup.Go"
+ wg.Go(func() {
+ fmt.Println()
+ })
+}
\ No newline at end of file
diff --git a/gopls/internal/analysis/modernize/testingcontext.go b/gopls/internal/analysis/modernize/testingcontext.go
index 9bdc11ccfca..de52f756ab8 100644
--- a/gopls/internal/analysis/modernize/testingcontext.go
+++ b/gopls/internal/analysis/modernize/testingcontext.go
@@ -14,12 +14,11 @@ import (
"unicode/utf8"
"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"
- "golang.org/x/tools/internal/astutil/cursor"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil/edge"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
)
// The testingContext pass replaces calls to context.WithCancel from within
@@ -41,38 +40,32 @@ import (
// - the call is within a test or subtest function
// - the relevant testing.{T,B,F} is named and not shadowed at the call
func testingContext(pass *analysis.Pass) {
- if !analysisinternal.Imports(pass.Pkg, "testing") {
- return
- }
+ var (
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
- info := pass.TypesInfo
+ contextWithCancel = index.Object("context", "WithCancel")
+ )
- // checkCall finds eligible calls to context.WithCancel to replace.
- checkCall := func(cur cursor.Cursor) {
+calls:
+ for cur := range index.Calls(contextWithCancel) {
call := cur.Node().(*ast.CallExpr)
- obj := typeutil.Callee(info, call)
- if !analysisinternal.IsFunctionNamed(obj, "context", "WithCancel") {
- return
- }
-
- // Have: context.WithCancel(arg)
+ // Have: context.WithCancel(...)
arg, ok := call.Args[0].(*ast.CallExpr)
if !ok {
- return
+ continue
}
- if obj := typeutil.Callee(info, arg); !analysisinternal.IsFunctionNamed(obj, "context", "Background", "TODO") {
- return
+ if !analysisinternal.IsFunctionNamed(typeutil.Callee(info, arg), "context", "Background", "TODO") {
+ continue
}
-
// Have: context.WithCancel(context.{Background,TODO}())
parent := cur.Parent()
assign, ok := parent.Node().(*ast.AssignStmt)
if !ok || assign.Tok != token.DEFINE {
- return
+ continue
}
-
// Have: a, b := context.WithCancel(context.{Background,TODO}())
// Check that both a and b are declared, not redeclarations.
@@ -80,27 +73,27 @@ func testingContext(pass *analysis.Pass) {
for _, expr := range assign.Lhs {
id, ok := expr.(*ast.Ident)
if !ok {
- return
+ continue calls
}
obj, ok := info.Defs[id]
if !ok {
- return
+ continue calls
}
lhs = append(lhs, obj)
}
next, ok := parent.NextSibling()
if !ok {
- return
+ continue
}
defr, ok := next.Node().(*ast.DeferStmt)
if !ok {
- return
+ continue
}
- if soleUse(info, lhs[1]) != defr.Call.Fun {
- return
+ deferId, ok := defr.Call.Fun.(*ast.Ident)
+ if !ok || !soleUseIs(index, lhs[1], deferId) {
+ continue // b is used elsewhere
}
-
// Have:
// a, b := context.WithCancel(context.{Background,TODO}())
// defer b()
@@ -110,7 +103,7 @@ func testingContext(pass *analysis.Pass) {
if curFunc, ok := enclosingFunc(cur); ok {
switch n := curFunc.Node().(type) {
case *ast.FuncLit:
- if e, idx := curFunc.Edge(); e == edge.CallExpr_Args && idx == 1 {
+ if ek, idx := curFunc.ParentEdge(); ek == edge.CallExpr_Args && idx == 1 {
// Have: call(..., func(...) { ...context.WithCancel(...)... })
obj := typeutil.Callee(info, curFunc.Parent().Node().(*ast.CallExpr))
if (analysisinternal.IsMethodNamed(obj, "testing", "T", "Run") ||
@@ -126,8 +119,7 @@ func testingContext(pass *analysis.Pass) {
testObj = isTestFn(info, n)
}
}
-
- if testObj != nil {
+ if testObj != nil && fileUses(info, enclosingFile(cur), "go1.24") {
// Have a test function. Check that we can resolve the relevant
// testing.{T,B,F} at the current position.
if _, obj := lhs[0].Parent().LookupParent(testObj.Name(), lhs[0].Pos()); obj == testObj {
@@ -148,29 +140,19 @@ func testingContext(pass *analysis.Pass) {
}
}
}
-
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
- for curFile := range filesUsing(inspect, info, "go1.24") {
- for cur := range curFile.Preorder((*ast.CallExpr)(nil)) {
- checkCall(cur)
- }
- }
}
-// soleUse returns the ident that refers to obj, if there is exactly one.
-//
-// TODO(rfindley): consider factoring to share with gopls/internal/refactor/inline.
-func soleUse(info *types.Info, obj types.Object) (sole *ast.Ident) {
- // This is not efficient, but it is called infrequently.
- for id, obj2 := range info.Uses {
- if obj2 == obj {
- if sole != nil {
- return nil // not unique
- }
- sole = id
+// soleUseIs reports whether id is the sole Ident that uses obj.
+// (It returns false if there were no uses of obj.)
+func soleUseIs(index *typeindex.Index, obj types.Object, id *ast.Ident) bool {
+ empty := true
+ for use := range index.Uses(obj) {
+ empty = false
+ if use.Node() != id {
+ return false
}
}
- return sole
+ return !empty
}
// isTestFn checks whether fn is a test function (TestX, BenchmarkX, FuzzX),
diff --git a/gopls/internal/analysis/modernize/waitgroup.go b/gopls/internal/analysis/modernize/waitgroup.go
new file mode 100644
index 00000000000..080bd4d362a
--- /dev/null
+++ b/gopls/internal/analysis/modernize/waitgroup.go
@@ -0,0 +1,144 @@
+// Copyright 2025 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"
+ "slices"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/analysisinternal"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
+)
+
+// The waitgroup pass replaces old more complex code with
+// go1.25 added API WaitGroup.Go.
+//
+// Patterns:
+//
+// 1. wg.Add(1); go func() { defer wg.Done(); ... }()
+// =>
+// wg.Go(go func() { ... })
+//
+// 2. wg.Add(1); go func() { ...; wg.Done() }()
+// =>
+// wg.Go(go func() { ... })
+//
+// The wg.Done must occur within the first statement of the block in a
+// defer format or last statement of the block, and the offered fix
+// only removes the first/last wg.Done call. It doesn't fix existing
+// wrong usage of sync.WaitGroup.
+//
+// The use of WaitGroup.Go in pattern 1 implicitly introduces a
+// 'defer', which may change the behavior in the case of panic from
+// the "..." logic. In this instance, the change is safe: before and
+// after the transformation, an unhandled panic inevitably results in
+// a fatal crash. The fact that the transformed code calls wg.Done()
+// before the crash doesn't materially change anything. (If Done had
+// other effects, or blocked, or if WaitGroup.Go propagated panics
+// from child to parent goroutine, the argument would be different.)
+func waitgroup(pass *analysis.Pass) {
+ var (
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
+ syncWaitGroupAdd = index.Selection("sync", "WaitGroup", "Add")
+ syncWaitGroupDone = index.Selection("sync", "WaitGroup", "Done")
+ )
+ if !index.Used(syncWaitGroupDone) {
+ return
+ }
+
+ for curAddCall := range index.Calls(syncWaitGroupAdd) {
+ // Extract receiver from wg.Add call.
+ addCall := curAddCall.Node().(*ast.CallExpr)
+ if !isIntLiteral(info, addCall.Args[0], 1) {
+ continue // not a call to wg.Add(1)
+ }
+ // Inv: the Args[0] check ensures addCall is not of
+ // the form sync.WaitGroup.Add(&wg, 1).
+ addCallRecv := ast.Unparen(addCall.Fun).(*ast.SelectorExpr).X
+
+ // Following statement must be go func() { ... } ().
+ addStmt, ok := curAddCall.Parent().Node().(*ast.ExprStmt)
+ if !ok {
+ continue // unnecessary parens?
+ }
+ curNext, ok := curAddCall.Parent().NextSibling()
+ if !ok {
+ continue // no successor
+ }
+ goStmt, ok := curNext.Node().(*ast.GoStmt)
+ if !ok {
+ continue // not a go stmt
+ }
+ lit, ok := goStmt.Call.Fun.(*ast.FuncLit)
+ if !ok || len(goStmt.Call.Args) != 0 {
+ continue // go argument is not func(){...}()
+ }
+ list := lit.Body.List
+ if len(list) == 0 {
+ continue
+ }
+
+ // Body must start with "defer wg.Done()" or end with "wg.Done()".
+ var doneStmt ast.Stmt
+ if deferStmt, ok := list[0].(*ast.DeferStmt); ok &&
+ typeutil.Callee(info, deferStmt.Call) == syncWaitGroupDone &&
+ equalSyntax(ast.Unparen(deferStmt.Call.Fun).(*ast.SelectorExpr).X, addCallRecv) {
+ doneStmt = deferStmt // "defer wg.Done()"
+
+ } else if lastStmt, ok := list[len(list)-1].(*ast.ExprStmt); ok {
+ if doneCall, ok := lastStmt.X.(*ast.CallExpr); ok &&
+ typeutil.Callee(info, doneCall) == syncWaitGroupDone &&
+ equalSyntax(ast.Unparen(doneCall.Fun).(*ast.SelectorExpr).X, addCallRecv) {
+ doneStmt = lastStmt // "wg.Done()"
+ }
+ }
+ if doneStmt == nil {
+ continue
+ }
+
+ file := enclosingFile(curAddCall)
+ if !fileUses(info, file, "go1.25") {
+ continue
+ }
+
+ pass.Report(analysis.Diagnostic{
+ Pos: addCall.Pos(),
+ End: goStmt.End(),
+ Category: "waitgroup",
+ Message: "Goroutine creation can be simplified using WaitGroup.Go",
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: "Simplify by using WaitGroup.Go",
+ TextEdits: slices.Concat(
+ // delete "wg.Add(1)"
+ analysisinternal.DeleteStmt(pass.Fset, file, addStmt, nil),
+ // delete "wg.Done()" or "defer wg.Done()"
+ analysisinternal.DeleteStmt(pass.Fset, file, doneStmt, nil),
+ []analysis.TextEdit{
+ // go func()
+ // ------
+ // wg.Go(func()
+ {
+ Pos: goStmt.Pos(),
+ End: goStmt.Call.Pos(),
+ NewText: fmt.Appendf(nil, "%s.Go(", addCallRecv),
+ },
+ // ... }()
+ // -
+ // ... } )
+ {
+ Pos: goStmt.Call.Lparen,
+ End: goStmt.Call.Rparen,
+ },
+ },
+ ),
+ }},
+ })
+ }
+}
diff --git a/gopls/internal/analysis/nonewvars/nonewvars.go b/gopls/internal/analysis/nonewvars/nonewvars.go
index 9e5d79df02c..62383dc2309 100644
--- a/gopls/internal/analysis/nonewvars/nonewvars.go
+++ b/gopls/internal/analysis/nonewvars/nonewvars.go
@@ -7,16 +7,17 @@
package nonewvars
import (
- "bytes"
_ "embed"
"go/ast"
- "go/format"
"go/token"
"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/gopls/internal/util/moreiters"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
@@ -31,59 +32,44 @@ var Analyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/nonewvars",
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
- if len(pass.TypeErrors) == 0 {
- return nil, nil
- }
- nodeFilter := []ast.Node{(*ast.AssignStmt)(nil)}
- inspect.Preorder(nodeFilter, func(n ast.Node) {
- assignStmt, _ := n.(*ast.AssignStmt)
- // We only care about ":=".
- if assignStmt.Tok != token.DEFINE {
- return
+ for _, typeErr := range pass.TypeErrors {
+ if typeErr.Msg != "no new variables on left side of :=" {
+ continue // irrelevant error
+ }
+ _, start, end, ok := typesinternal.ErrorCodeStartEnd(typeErr)
+ if !ok {
+ continue // can't get position info
+ }
+ curErr, ok := cursor.Root(inspect).FindByPos(start, end)
+ if !ok {
+ continue // can't find errant node
}
- var file *ast.File
- for _, f := range pass.Files {
- if f.FileStart <= assignStmt.Pos() && assignStmt.Pos() < f.FileEnd {
- file = f
- break
- }
+ // Find enclosing assignment (which may be curErr itself).
+ curAssign, ok := moreiters.First(curErr.Enclosing((*ast.AssignStmt)(nil)))
+ if !ok {
+ continue // no enclosing assignment
}
- if file == nil {
- return
+ assign := curAssign.Node().(*ast.AssignStmt)
+ if assign.Tok != token.DEFINE {
+ continue // not a := statement
}
- for _, err := range pass.TypeErrors {
- if !FixesError(err.Msg) {
- continue
- }
- if assignStmt.Pos() > err.Pos || err.Pos >= assignStmt.End() {
- continue
- }
- var buf bytes.Buffer
- if err := format.Node(&buf, pass.Fset, file); err != nil {
- continue
- }
- pass.Report(analysis.Diagnostic{
- Pos: err.Pos,
- End: analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos),
- Message: err.Msg,
- SuggestedFixes: []analysis.SuggestedFix{{
- Message: "Change ':=' to '='",
- TextEdits: []analysis.TextEdit{{
- Pos: err.Pos,
- End: err.Pos + 1,
- }},
+ pass.Report(analysis.Diagnostic{
+ Pos: assign.TokPos,
+ End: assign.TokPos + token.Pos(len(":=")),
+ Message: typeErr.Msg,
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: "Change ':=' to '='",
+ TextEdits: []analysis.TextEdit{{
+ Pos: assign.TokPos,
+ End: assign.TokPos + token.Pos(len(":")),
}},
- })
- }
- })
+ }},
+ })
+ }
return nil, nil
}
-
-func FixesError(msg string) bool {
- return msg == "no new variables on left side of :="
-}
diff --git a/gopls/internal/analysis/noresultvalues/noresultvalues.go b/gopls/internal/analysis/noresultvalues/noresultvalues.go
index 118beb4568b..4f095c941c4 100644
--- a/gopls/internal/analysis/noresultvalues/noresultvalues.go
+++ b/gopls/internal/analysis/noresultvalues/noresultvalues.go
@@ -5,9 +5,8 @@
package noresultvalues
import (
- "bytes"
"go/ast"
- "go/format"
+ "go/token"
"strings"
_ "embed"
@@ -15,7 +14,10 @@ import (
"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/gopls/internal/util/moreiters"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
@@ -30,57 +32,42 @@ var Analyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/noresultvalues",
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
- if len(pass.TypeErrors) == 0 {
- return nil, nil
- }
-
- nodeFilter := []ast.Node{(*ast.ReturnStmt)(nil)}
- inspect.Preorder(nodeFilter, func(n ast.Node) {
- retStmt, _ := n.(*ast.ReturnStmt)
- var file *ast.File
- for _, f := range pass.Files {
- if f.FileStart <= retStmt.Pos() && retStmt.Pos() < f.FileEnd {
- file = f
- break
- }
+ for _, typErr := range pass.TypeErrors {
+ if !fixesError(typErr.Msg) {
+ continue // irrelevant error
}
- if file == nil {
- return
+ _, start, end, ok := typesinternal.ErrorCodeStartEnd(typErr)
+ if !ok {
+ continue // can't get position info
}
-
- for _, err := range pass.TypeErrors {
- if !FixesError(err.Msg) {
- continue
- }
- if retStmt.Pos() >= err.Pos || err.Pos >= retStmt.End() {
- continue
- }
- var buf bytes.Buffer
- if err := format.Node(&buf, pass.Fset, file); err != nil {
- continue
- }
+ curErr, ok := cursor.Root(inspect).FindByPos(start, end)
+ if !ok {
+ continue // can't find errant node
+ }
+ // Find first enclosing return statement, if any.
+ if curRet, ok := moreiters.First(curErr.Enclosing((*ast.ReturnStmt)(nil))); ok {
+ ret := curRet.Node()
pass.Report(analysis.Diagnostic{
- Pos: err.Pos,
- End: analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos),
- Message: err.Msg,
+ Pos: start,
+ End: end,
+ Message: typErr.Msg,
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Delete return values",
TextEdits: []analysis.TextEdit{{
- Pos: retStmt.Pos(),
- End: retStmt.End(),
- NewText: []byte("return"),
+ Pos: ret.Pos() + token.Pos(len("return")),
+ End: ret.End(),
}},
}},
})
}
- })
+ }
return nil, nil
}
-func FixesError(msg string) bool {
+func fixesError(msg string) bool {
return msg == "no result values expected" ||
strings.HasPrefix(msg, "too many return values") && strings.Contains(msg, "want ()")
}
diff --git a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go
index 15176cef1c8..b38ccf4d5ed 100644
--- a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go
+++ b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go
@@ -33,7 +33,7 @@ var Analyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifycompositelit",
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
// Gather information whether file is generated or not
generated := make(map[*token.File]bool)
for _, file := range pass.Files {
diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange.go b/gopls/internal/analysis/simplifyrange/simplifyrange.go
index 6d079059eb1..594ebd1f55a 100644
--- a/gopls/internal/analysis/simplifyrange/simplifyrange.go
+++ b/gopls/internal/analysis/simplifyrange/simplifyrange.go
@@ -5,10 +5,8 @@
package simplifyrange
import (
- "bytes"
_ "embed"
"go/ast"
- "go/printer"
"go/token"
"golang.org/x/tools/go/analysis"
@@ -28,7 +26,7 @@ var Analyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifyrange",
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
// Gather information whether file is generated or not
generated := make(map[*token.File]bool)
for _, file := range pass.Files {
@@ -42,73 +40,48 @@ func run(pass *analysis.Pass) (interface{}, error) {
(*ast.RangeStmt)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
- if _, ok := generated[pass.Fset.File(n.Pos())]; ok {
- return // skip checking if it's generated code
- }
+ rng := n.(*ast.RangeStmt)
- var copy *ast.RangeStmt // shallow-copy the AST before modifying
- {
- x := *n.(*ast.RangeStmt)
- copy = &x
- }
- end := newlineIndex(pass.Fset, copy)
+ kblank := isBlank(rng.Key)
+ vblank := isBlank(rng.Value)
+ var start, end token.Pos
+ switch {
+ case kblank && (rng.Value == nil || vblank):
+ // for _ = range x {}
+ // for _, _ = range x {}
+ // ^^^^^^^
+ start, end = rng.Key.Pos(), rng.Range
- // Range statements of the form: for i, _ := range x {}
- var old ast.Expr
- if isBlank(copy.Value) {
- old = copy.Value
- copy.Value = nil
- }
- // Range statements of the form: for _ := range x {}
- if isBlank(copy.Key) && copy.Value == nil {
- old = copy.Key
- copy.Key = nil
+ case vblank:
+ // for k, _ := range x {}
+ // ^^^
+ start, end = rng.Key.End(), rng.Value.End()
+
+ default:
+ return
}
- // Return early if neither if condition is met.
- if old == nil {
+
+ if generated[pass.Fset.File(n.Pos())] {
return
}
+
pass.Report(analysis.Diagnostic{
- Pos: old.Pos(),
- End: old.End(),
- Message: "simplify range expression",
- SuggestedFixes: suggestedFixes(pass.Fset, copy, end),
+ Pos: start,
+ End: end,
+ Message: "simplify range expression",
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: "Remove empty value",
+ TextEdits: []analysis.TextEdit{{
+ Pos: start,
+ End: end,
+ }},
+ }},
})
})
return nil, nil
}
-func suggestedFixes(fset *token.FileSet, rng *ast.RangeStmt, end token.Pos) []analysis.SuggestedFix {
- var b bytes.Buffer
- printer.Fprint(&b, fset, rng)
- stmt := b.Bytes()
- index := bytes.Index(stmt, []byte("\n"))
- // If there is a new line character, then don't replace the body.
- if index != -1 {
- stmt = stmt[:index]
- }
- return []analysis.SuggestedFix{{
- Message: "Remove empty value",
- TextEdits: []analysis.TextEdit{{
- Pos: rng.Pos(),
- End: end,
- NewText: stmt[:index],
- }},
- }}
-}
-
-func newlineIndex(fset *token.FileSet, rng *ast.RangeStmt) token.Pos {
- var b bytes.Buffer
- printer.Fprint(&b, fset, rng)
- contents := b.Bytes()
- index := bytes.Index(contents, []byte("\n"))
- if index == -1 {
- return rng.End()
- }
- return rng.Pos() + token.Pos(index)
-}
-
-func isBlank(x ast.Expr) bool {
- ident, ok := x.(*ast.Ident)
- return ok && ident.Name == "_"
+func isBlank(e ast.Expr) bool {
+ id, ok := e.(*ast.Ident)
+ return ok && id.Name == "_"
}
diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go
index 50a600e03bf..089f65df870 100644
--- a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go
+++ b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go
@@ -5,8 +5,6 @@
package simplifyrange_test
import (
- "go/build"
- "slices"
"testing"
"golang.org/x/tools/go/analysis/analysistest"
@@ -14,9 +12,8 @@ import (
)
func Test(t *testing.T) {
- testdata := analysistest.TestData()
- analysistest.RunWithSuggestedFixes(t, testdata, simplifyrange.Analyzer, "a", "generatedcode")
- if slices.Contains(build.Default.ReleaseTags, "go1.23") { // uses iter.Seq
- analysistest.RunWithSuggestedFixes(t, testdata, simplifyrange.Analyzer, "rangeoverfunc")
- }
+ analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), simplifyrange.Analyzer,
+ "a",
+ "generatedcode",
+ "rangeoverfunc")
}
diff --git a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go
index 49face1e968..1d7b1bd58f2 100644
--- a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go
+++ b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go
@@ -13,4 +13,11 @@ func m() {
}
for _ = range maps { // want "simplify range expression"
}
+ for _, _ = range maps { // want "simplify range expression"
+ }
+ for _, v := range maps { // nope
+ println(v)
+ }
+ for range maps { // nope
+ }
}
diff --git a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden
index ec8490ab337..25139bd93f2 100644
--- a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden
+++ b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden
@@ -13,4 +13,11 @@ func m() {
}
for range maps { // want "simplify range expression"
}
+ for range maps { // want "simplify range expression"
+ }
+ for _, v := range maps { // nope
+ println(v)
+ }
+ for range maps { // nope
+ }
}
diff --git a/gopls/internal/analysis/simplifyslice/simplifyslice.go b/gopls/internal/analysis/simplifyslice/simplifyslice.go
index 6755187afe5..28cc266d713 100644
--- a/gopls/internal/analysis/simplifyslice/simplifyslice.go
+++ b/gopls/internal/analysis/simplifyslice/simplifyslice.go
@@ -37,7 +37,7 @@ var Analyzer = &analysis.Analyzer{
// An example where it does not:
// x, y := b[:n], b[n:]
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
// Gather information whether file is generated or not
generated := make(map[*token.File]bool)
for _, file := range pass.Files {
diff --git a/gopls/internal/analysis/unusedfunc/doc.go b/gopls/internal/analysis/unusedfunc/doc.go
index 5946ed897bb..9e2fc8145c8 100644
--- a/gopls/internal/analysis/unusedfunc/doc.go
+++ b/gopls/internal/analysis/unusedfunc/doc.go
@@ -20,12 +20,29 @@
// that of any method of an interface type declared within the same
// package.
//
-// The tool may report a false positive for a declaration of an
-// unexported function that is referenced from another package using
-// the go:linkname mechanism, if the declaration's doc comment does
-// not also have a go:linkname comment. (Such code is in any case
-// strongly discouraged: linkname annotations, if they must be used at
-// all, should be used on both the declaration and the alias.)
+// The tool may report false positives in some situations, for
+// example:
+//
+// - For a declaration of an unexported function that is referenced
+// from another package using the go:linkname mechanism, if the
+// declaration's doc comment does not also have a go:linkname
+// comment.
+//
+// (Such code is in any case strongly discouraged: linkname
+// annotations, if they must be used at all, should be used on both
+// the declaration and the alias.)
+//
+// - For compiler intrinsics in the "runtime" package that, though
+// never referenced, are known to the compiler and are called
+// indirectly by compiled object code.
+//
+// - For functions called only from assembly.
+//
+// - For functions called only from files whose build tags are not
+// selected in the current build configuration.
+//
+// See https://github.com/golang/go/issues/71686 for discussion of
+// these limitations.
//
// The unusedfunc algorithm is not as precise as the
// golang.org/x/tools/cmd/deadcode tool, but it has the advantage that
diff --git a/gopls/internal/analysis/unusedparams/unusedparams.go b/gopls/internal/analysis/unusedparams/unusedparams.go
index 2986dfd6e41..824711242da 100644
--- a/gopls/internal/analysis/unusedparams/unusedparams.go
+++ b/gopls/internal/analysis/unusedparams/unusedparams.go
@@ -80,24 +80,9 @@ func run(pass *analysis.Pass) (any, error) {
inspect.Preorder(filter, func(n ast.Node) {
switch n := n.(type) {
case *ast.CallExpr:
- // Strip off any generic instantiation.
- fun := n.Fun
- switch fun_ := fun.(type) {
- case *ast.IndexExpr:
- fun = fun_.X // f[T]() (funcs[i]() is rejected below)
- case *ast.IndexListExpr:
- fun = fun_.X // f[K, V]()
- }
-
+ id := typesinternal.UsedIdent(pass.TypesInfo, n.Fun)
// Find object:
// record non-exported function, method, or func-typed var.
- var id *ast.Ident
- switch fun := fun.(type) {
- case *ast.Ident:
- id = fun
- case *ast.SelectorExpr:
- id = fun.Sel
- }
if id != nil && !id.IsExported() {
switch pass.TypesInfo.Uses[id].(type) {
case *types.Func, *types.Var:
@@ -139,194 +124,171 @@ func run(pass *analysis.Pass) (any, error) {
}
}
- // Inspect each file to see if it is generated.
- //
- // We do not want to report unused parameters in generated code itself,
- // however we need to include generated code in the overall analysis as
- // it may be calling functions in non-generated code.
- files := []ast.Node{(*ast.File)(nil)}
- cursor.Root(inspect).Inspect(files, func(c cursor.Cursor, push bool) bool {
- if !push {
- return true
- }
-
- isGenerated := ast.IsGenerated(c.Node().(*ast.File))
-
- // Descend into the file, check each non-address-taken function's parameters
- // are all used.
- funcs := []ast.Node{
- (*ast.FuncDecl)(nil),
- (*ast.FuncLit)(nil),
- }
- c.Inspect(funcs, func(c cursor.Cursor, push bool) bool {
- // (We always return true so that we visit nested FuncLits.)
- if !push {
- return true
+ // Check each non-address-taken function's parameters are all used.
+funcloop:
+ for c := range cursor.Root(inspect).Preorder((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) {
+ var (
+ fn types.Object // function symbol (*Func, possibly *Var for a FuncLit)
+ ftype *ast.FuncType
+ body *ast.BlockStmt
+ )
+ switch n := c.Node().(type) {
+ case *ast.FuncDecl:
+ // We can't analyze non-Go functions.
+ if n.Body == nil {
+ continue
}
- var (
- fn types.Object // function symbol (*Func, possibly *Var for a FuncLit)
- ftype *ast.FuncType
- body *ast.BlockStmt
- )
- switch n := c.Node().(type) {
- case *ast.FuncDecl:
- // We can't analyze non-Go functions.
- if n.Body == nil {
- return true
- }
-
- // Ignore exported functions and methods: we
- // must assume they may be address-taken in
- // another package.
- if n.Name.IsExported() {
- return true
- }
-
- // Ignore methods that match the name of any
- // interface method declared in this package,
- // as the method's signature may need to conform
- // to the interface.
- if n.Recv != nil && unexportedIMethodNames[n.Name.Name] {
- return true
- }
-
- fn = pass.TypesInfo.Defs[n.Name].(*types.Func)
- ftype, body = n.Type, n.Body
+ // Ignore exported functions and methods: we
+ // must assume they may be address-taken in
+ // another package.
+ if n.Name.IsExported() {
+ continue
+ }
- case *ast.FuncLit:
- // Find the symbol for the variable (if any)
- // to which the FuncLit is bound.
- // (We don't bother to allow ParenExprs.)
- switch parent := c.Parent().Node().(type) {
- case *ast.AssignStmt:
- // f = func() {...}
- // f := func() {...}
- if e, idx := c.Edge(); e == edge.AssignStmt_Rhs {
- // Inv: n == AssignStmt.Rhs[idx]
- if id, ok := parent.Lhs[idx].(*ast.Ident); ok {
- fn = pass.TypesInfo.ObjectOf(id)
+ // Ignore methods that match the name of any
+ // interface method declared in this package,
+ // as the method's signature may need to conform
+ // to the interface.
+ if n.Recv != nil && unexportedIMethodNames[n.Name.Name] {
+ continue
+ }
- // Edge case: f = func() {...}
- // should not count as a use.
- if pass.TypesInfo.Uses[id] != nil {
- usesOutsideCall[fn] = moreslices.Remove(usesOutsideCall[fn], id)
- }
+ fn = pass.TypesInfo.Defs[n.Name].(*types.Func)
+ ftype, body = n.Type, n.Body
+
+ case *ast.FuncLit:
+ // Find the symbol for the variable (if any)
+ // to which the FuncLit is bound.
+ // (We don't bother to allow ParenExprs.)
+ switch parent := c.Parent().Node().(type) {
+ case *ast.AssignStmt:
+ // f = func() {...}
+ // f := func() {...}
+ if ek, idx := c.ParentEdge(); ek == edge.AssignStmt_Rhs {
+ // Inv: n == AssignStmt.Rhs[idx]
+ if id, ok := parent.Lhs[idx].(*ast.Ident); ok {
+ fn = pass.TypesInfo.ObjectOf(id)
+
+ // Edge case: f = func() {...}
+ // should not count as a use.
+ if pass.TypesInfo.Uses[id] != nil {
+ usesOutsideCall[fn] = moreslices.Remove(usesOutsideCall[fn], id)
+ }
- if fn == nil && id.Name == "_" {
- // Edge case: _ = func() {...}
- // has no local var. Fake one.
- v := types.NewVar(id.Pos(), pass.Pkg, id.Name, pass.TypesInfo.TypeOf(n))
- typesinternal.SetVarKind(v, typesinternal.LocalVar)
- fn = v
- }
+ if fn == nil && id.Name == "_" {
+ // Edge case: _ = func() {...}
+ // has no local var. Fake one.
+ v := types.NewVar(id.Pos(), pass.Pkg, id.Name, pass.TypesInfo.TypeOf(n))
+ typesinternal.SetVarKind(v, typesinternal.LocalVar)
+ fn = v
}
}
+ }
- case *ast.ValueSpec:
- // var f = func() { ... }
- // (unless f is an exported package-level var)
- for i, val := range parent.Values {
- if val == n {
- v := pass.TypesInfo.Defs[parent.Names[i]]
- if !(v.Parent() == pass.Pkg.Scope() && v.Exported()) {
- fn = v
- }
- break
+ case *ast.ValueSpec:
+ // var f = func() { ... }
+ // (unless f is an exported package-level var)
+ for i, val := range parent.Values {
+ if val == n {
+ v := pass.TypesInfo.Defs[parent.Names[i]]
+ if !(v.Parent() == pass.Pkg.Scope() && v.Exported()) {
+ fn = v
}
+ break
}
}
-
- ftype, body = n.Type, n.Body
}
- // Ignore address-taken functions and methods: unused
- // parameters may be needed to conform to a func type.
- if fn == nil || len(usesOutsideCall[fn]) > 0 {
- return true
- }
+ ftype, body = n.Type, n.Body
+ }
- // If there are no parameters, there are no unused parameters.
- if ftype.Params.NumFields() == 0 {
- return true
- }
+ // Ignore address-taken functions and methods: unused
+ // parameters may be needed to conform to a func type.
+ if fn == nil || len(usesOutsideCall[fn]) > 0 {
+ continue
+ }
- // To reduce false positives, ignore functions with an
- // empty or panic body.
- //
- // We choose not to ignore functions whose body is a
- // single return statement (as earlier versions did)
- // func f() { return }
- // func f() { return g(...) }
- // as we suspect that was just heuristic to reduce
- // false positives in the earlier unsound algorithm.
- switch len(body.List) {
- case 0:
- // Empty body. Although the parameter is
- // unnecessary, it's pretty obvious to the
- // reader that that's the case, so we allow it.
- return true // func f() {}
- case 1:
- if stmt, ok := body.List[0].(*ast.ExprStmt); ok {
- // We allow a panic body, as it is often a
- // placeholder for a future implementation:
- // func f() { panic(...) }
- if call, ok := stmt.X.(*ast.CallExpr); ok {
- if fun, ok := call.Fun.(*ast.Ident); ok && fun.Name == "panic" {
- return true
- }
+ // If there are no parameters, there are no unused parameters.
+ if ftype.Params.NumFields() == 0 {
+ continue
+ }
+
+ // To reduce false positives, ignore functions with an
+ // empty or panic body.
+ //
+ // We choose not to ignore functions whose body is a
+ // single return statement (as earlier versions did)
+ // func f() { return }
+ // func f() { return g(...) }
+ // as we suspect that was just heuristic to reduce
+ // false positives in the earlier unsound algorithm.
+ switch len(body.List) {
+ case 0:
+ // Empty body. Although the parameter is
+ // unnecessary, it's pretty obvious to the
+ // reader that that's the case, so we allow it.
+ continue // func f() {}
+ case 1:
+ if stmt, ok := body.List[0].(*ast.ExprStmt); ok {
+ // We allow a panic body, as it is often a
+ // placeholder for a future implementation:
+ // func f() { panic(...) }
+ if call, ok := stmt.X.(*ast.CallExpr); ok {
+ if fun, ok := call.Fun.(*ast.Ident); ok && fun.Name == "panic" {
+ continue
}
}
}
+ }
- // Don't report diagnostics on generated files.
- if isGenerated {
- return true
+ // Don't report diagnostics on generated files.
+ // (We can't skip analysis of generated files, though.)
+ for curFile := range c.Enclosing((*ast.File)(nil)) {
+ if ast.IsGenerated(curFile.Node().(*ast.File)) {
+ continue funcloop
}
+ }
- // Report each unused parameter.
- for _, field := range ftype.Params.List {
- for _, id := range field.Names {
- if id.Name == "_" {
- continue
+ // Report each unused parameter.
+ for _, field := range ftype.Params.List {
+ for _, id := range field.Names {
+ if id.Name == "_" {
+ continue
+ }
+ param := pass.TypesInfo.Defs[id].(*types.Var)
+ if !usedVars[param] {
+ start, end := field.Pos(), field.End()
+ if len(field.Names) > 1 {
+ start, end = id.Pos(), id.End()
}
- param := pass.TypesInfo.Defs[id].(*types.Var)
- if !usedVars[param] {
- start, end := field.Pos(), field.End()
- if len(field.Names) > 1 {
- start, end = id.Pos(), id.End()
- }
- // This diagnostic carries both an edit-based fix to
- // rename the unused parameter, and a command-based fix
- // to remove it (see golang.RemoveUnusedParameter).
- pass.Report(analysis.Diagnostic{
- Pos: start,
- End: end,
- Message: fmt.Sprintf("unused parameter: %s", id.Name),
- Category: FixCategory,
- SuggestedFixes: []analysis.SuggestedFix{
- {
- Message: `Rename parameter to "_"`,
- TextEdits: []analysis.TextEdit{{
- Pos: id.Pos(),
- End: id.End(),
- NewText: []byte("_"),
- }},
- },
- {
- Message: fmt.Sprintf("Remove unused parameter %q", id.Name),
- // No TextEdits => computed by gopls command
- },
+ // This diagnostic carries both an edit-based fix to
+ // rename the unused parameter, and a command-based fix
+ // to remove it (see golang.RemoveUnusedParameter).
+ pass.Report(analysis.Diagnostic{
+ Pos: start,
+ End: end,
+ Message: fmt.Sprintf("unused parameter: %s", id.Name),
+ Category: FixCategory,
+ SuggestedFixes: []analysis.SuggestedFix{
+ {
+ Message: `Rename parameter to "_"`,
+ TextEdits: []analysis.TextEdit{{
+ Pos: id.Pos(),
+ End: id.End(),
+ NewText: []byte("_"),
+ }},
},
- })
- }
+ {
+ Message: fmt.Sprintf("Remove unused parameter %q", id.Name),
+ // No TextEdits => computed by gopls command
+ },
+ },
+ })
}
}
-
- return true
- })
- return true
- })
+ }
+ }
return nil, nil
}
diff --git a/gopls/internal/analysis/unusedvariable/unusedvariable.go b/gopls/internal/analysis/unusedvariable/unusedvariable.go
index 15bcd43d873..3ea1dbe6953 100644
--- a/gopls/internal/analysis/unusedvariable/unusedvariable.go
+++ b/gopls/internal/analysis/unusedvariable/unusedvariable.go
@@ -13,10 +13,12 @@ import (
"go/token"
"go/types"
"regexp"
+ "slices"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
)
@@ -45,7 +47,7 @@ func run(pass *analysis.Pass) (any, error) {
if len(match) > 0 {
varName := match[1]
// Beginning in Go 1.23, go/types began quoting vars as `v'.
- varName = strings.Trim(varName, "'`'")
+ varName = strings.Trim(varName, "`'")
err := runForError(pass, typeErr, varName)
if err != nil {
@@ -165,16 +167,13 @@ func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.Valu
// Find parent DeclStmt and delete it
for _, node := range path {
if declStmt, ok := node.(*ast.DeclStmt); ok {
- edits := deleteStmtFromBlock(pass.Fset, path, declStmt)
- if len(edits) == 0 {
- return nil // can this happen?
- }
- return []analysis.SuggestedFix{
- {
+ if edits := deleteStmtFromBlock(pass.Fset, path, declStmt); len(edits) > 0 {
+ return []analysis.SuggestedFix{{
Message: suggestedFixMessage(ident.Name),
TextEdits: edits,
- },
+ }}
}
+ return nil
}
}
}
@@ -222,16 +221,13 @@ func removeVariableFromAssignment(fset *token.FileSet, path []ast.Node, stmt *as
}
// RHS does not have any side effects, delete the whole statement
- edits := deleteStmtFromBlock(fset, path, stmt)
- if len(edits) == 0 {
- return nil // can this happen?
- }
- return []analysis.SuggestedFix{
- {
+ if edits := deleteStmtFromBlock(fset, path, stmt); len(edits) > 0 {
+ return []analysis.SuggestedFix{{
Message: suggestedFixMessage(ident.Name),
TextEdits: edits,
- },
+ }}
}
+ return nil
}
// Otherwise replace ident with `_`
@@ -253,34 +249,48 @@ func suggestedFixMessage(name string) string {
return fmt.Sprintf("Remove variable %s", name)
}
+// deleteStmtFromBlock returns the edits to remove stmt if its parent is a BlockStmt.
+// (stmt is not necessarily the leaf, path[0].)
+//
+// It returns nil if the parent is not a block, as in these examples:
+//
+// switch STMT; {}
+// switch { default: STMT }
+// select { default: STMT }
+//
+// TODO(adonovan): handle these cases too.
func deleteStmtFromBlock(fset *token.FileSet, path []ast.Node, stmt ast.Stmt) []analysis.TextEdit {
- // Find innermost enclosing BlockStmt.
- var block *ast.BlockStmt
- for i := range path {
- if blockStmt, ok := path[i].(*ast.BlockStmt); ok {
- block = blockStmt
- break
- }
+ // TODO(adonovan): simplify using Cursor API.
+ i := slices.Index(path, ast.Node(stmt)) // must be present
+ block, ok := path[i+1].(*ast.BlockStmt)
+ if !ok {
+ return nil // parent is not a BlockStmt
}
- nodeIndex := -1
- for i, blockStmt := range block.List {
- if blockStmt == stmt {
- nodeIndex = i
- break
- }
+ nodeIndex := slices.Index(block.List, stmt)
+ if nodeIndex == -1 {
+ bug.Reportf("%s: Stmt not found in BlockStmt.List", safetoken.StartPosition(fset, stmt.Pos())) // refine #71812
+ return nil
}
- // The statement we need to delete was not found in BlockStmt
- if nodeIndex == -1 {
+ if !stmt.Pos().IsValid() {
+ bug.Reportf("%s: invalid Stmt.Pos", safetoken.StartPosition(fset, stmt.Pos())) // refine #71812
return nil
}
// Delete until the end of the block unless there is another statement after
// the one we are trying to delete
end := block.Rbrace
+ if !end.IsValid() {
+ bug.Reportf("%s: BlockStmt has no Rbrace", safetoken.StartPosition(fset, block.Pos())) // refine #71812
+ return nil
+ }
if nodeIndex < len(block.List)-1 {
end = block.List[nodeIndex+1].Pos()
+ if end < stmt.Pos() {
+ bug.Reportf("%s: BlockStmt.List[last].Pos > BlockStmt.Rbrace", safetoken.StartPosition(fset, block.Pos())) // refine #71812
+ return nil
+ }
}
// Account for comments within the block containing the statement
@@ -298,7 +308,7 @@ outer:
// If a comment exists within the current block, after the unused variable statement,
// and before the next statement, we shouldn't delete it.
if coLine > stmtEndLine {
- end = co.Pos()
+ end = co.Pos() // preserves invariant stmt.Pos <= end (#71812)
break outer
}
if co.Pos() > end {
@@ -308,12 +318,11 @@ outer:
}
}
- return []analysis.TextEdit{
- {
- Pos: stmt.Pos(),
- End: end,
- },
- }
+ // Delete statement and optional following comment.
+ return []analysis.TextEdit{{
+ Pos: stmt.Pos(),
+ End: end,
+ }}
}
// exprMayHaveSideEffects reports whether the expression may have side effects
diff --git a/gopls/internal/analysis/yield/yield.go b/gopls/internal/analysis/yield/yield.go
index ccd30045f97..354cf372186 100644
--- a/gopls/internal/analysis/yield/yield.go
+++ b/gopls/internal/analysis/yield/yield.go
@@ -44,7 +44,7 @@ var Analyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/yield",
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass) (any, error) {
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// Find all calls to yield of the right type.
diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go
index d570c0a46ae..f63bcab2374 100644
--- a/gopls/internal/cache/analysis.go
+++ b/gopls/internal/cache/analysis.go
@@ -18,7 +18,6 @@ import (
"go/token"
"go/types"
"log"
- "maps"
urlpkg "net/url"
"path/filepath"
"reflect"
@@ -45,6 +44,7 @@ import (
"golang.org/x/tools/gopls/internal/util/frob"
"golang.org/x/tools/gopls/internal/util/moremaps"
"golang.org/x/tools/gopls/internal/util/persistent"
+ "golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/facts"
@@ -127,11 +127,12 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
// Filter and sort enabled root analyzers.
// A disabled analyzer may still be run if required by another.
- analyzers := analyzers(s.Options().Staticcheck)
- toSrc := make(map[*analysis.Analyzer]*settings.Analyzer)
- var enabledAnalyzers []*analysis.Analyzer // enabled subset + transitive requirements
- for _, a := range analyzers {
- if enabled, ok := s.Options().Analyses[a.Analyzer().Name]; enabled || !ok && a.EnabledByDefault() {
+ var (
+ toSrc = make(map[*analysis.Analyzer]*settings.Analyzer)
+ enabledAnalyzers []*analysis.Analyzer // enabled subset + transitive requirements
+ )
+ for _, a := range settings.AllAnalyzers {
+ if a.Enabled(s.Options()) {
toSrc[a.Analyzer()] = a
enabledAnalyzers = append(enabledAnalyzers, a.Analyzer())
}
@@ -139,7 +140,6 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
sort.Slice(enabledAnalyzers, func(i, j int) bool {
return enabledAnalyzers[i].Name < enabledAnalyzers[j].Name
})
- analyzers = nil // prevent accidental use
enabledAnalyzers = requiredAnalyzers(enabledAnalyzers)
@@ -431,14 +431,6 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
return results, nil
}
-func analyzers(staticcheck bool) []*settings.Analyzer {
- analyzers := slices.Collect(maps.Values(settings.DefaultAnalyzers))
- if staticcheck {
- analyzers = slices.AppendSeq(analyzers, maps.Values(settings.StaticcheckAnalyzers))
- }
- return analyzers
-}
-
func (an *analysisNode) decrefPreds() {
if an.unfinishedPreds.Add(-1) == 0 {
an.summary.Actions = nil
@@ -637,7 +629,7 @@ func (an *analysisNode) runCached(ctx context.Context, key file.Hash) (*analyzeS
return summary, nil
}
-// analysisCacheKey returns a cache key that is a cryptographic digest
+// cacheKey returns a cache key that is a cryptographic digest
// of the all the values that might affect type checking and analysis:
// the analyzer names, package metadata, names and contents of
// compiled Go files, and vdeps (successor) information
@@ -822,8 +814,7 @@ func typesLookup(pkg *types.Package) func(string) *types.Package {
)
// search scans children the next package in pending, looking for pkgPath.
- var search func(pkgPath string) (*types.Package, int)
- search = func(pkgPath string) (sought *types.Package, numPending int) {
+ search := func(pkgPath string) (sought *types.Package, numPending int) {
mu.Lock()
defer mu.Unlock()
@@ -886,7 +877,7 @@ type action struct {
vdeps map[PackageID]*analysisNode // vertical dependencies
// results of action.exec():
- result interface{} // result of Run function, of type a.ResultType
+ result any // result of Run function, of type a.ResultType
summary *actionSummary
err error
}
@@ -900,7 +891,6 @@ func (act *action) String() string {
func execActions(ctx context.Context, actions []*action) {
var wg sync.WaitGroup
for _, act := range actions {
- act := act
wg.Add(1)
go func() {
defer wg.Done()
@@ -965,7 +955,7 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) {
}
// Gather analysis Result values from horizontal dependencies.
- inputs := make(map[*analysis.Analyzer]interface{})
+ inputs := make(map[*analysis.Analyzer]any)
for _, dep := range act.hdeps {
inputs[dep.a] = dep.result
}
@@ -1029,93 +1019,6 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) {
factFilter[reflect.TypeOf(f)] = true
}
- // posToLocation converts from token.Pos to protocol form.
- posToLocation := func(start, end token.Pos) (protocol.Location, error) {
- tokFile := apkg.pkg.FileSet().File(start)
-
- // Find existing mapper by file name.
- // (Don't require an exact token.File match
- // as the analyzer may have re-parsed the file.)
- var (
- mapper *protocol.Mapper
- fixed bool
- )
- for _, p := range apkg.pkg.CompiledGoFiles() {
- if p.Tok.Name() == tokFile.Name() {
- mapper = p.Mapper
- fixed = p.Fixed() // suppress some assertions after parser recovery
- break
- }
- }
- if mapper == nil {
- // The start position was not among the package's parsed
- // Go files, indicating that the analyzer added new files
- // to the FileSet.
- //
- // For example, the cgocall analyzer re-parses and
- // type-checks some of the files in a special environment;
- // and asmdecl and other low-level runtime analyzers call
- // ReadFile to parse non-Go files.
- // (This is a supported feature, documented at go/analysis.)
- //
- // In principle these files could be:
- //
- // - OtherFiles (non-Go files such as asm).
- // However, we set Pass.OtherFiles=[] because
- // gopls won't service "diagnose" requests
- // for non-Go files, so there's no point
- // reporting diagnostics in them.
- //
- // - IgnoredFiles (files tagged for other configs).
- // However, we set Pass.IgnoredFiles=[] because,
- // in most cases, zero-config gopls should create
- // another view that covers these files.
- //
- // - Referents of //line directives, as in cgo packages.
- // The file names in this case are not known a priori.
- // gopls generally tries to avoid honoring line directives,
- // but analyzers such as cgocall may honor them.
- //
- // In short, it's unclear how this can be reached
- // other than due to an analyzer bug.
- return protocol.Location{}, bug.Errorf("diagnostic location is not among files of package: %s", tokFile.Name())
- }
- // Inv: mapper != nil
-
- if end == token.NoPos {
- end = start
- }
-
- // debugging #64547
- fileStart := token.Pos(tokFile.Base())
- fileEnd := fileStart + token.Pos(tokFile.Size())
- if start < fileStart {
- if !fixed {
- bug.Reportf("start < start of file")
- }
- start = fileStart
- }
- if end < start {
- // This can happen if End is zero (#66683)
- // or a small positive displacement from zero
- // due to recursive Node.End() computation.
- // This usually arises from poor parser recovery
- // of an incomplete term at EOF.
- if !fixed {
- bug.Reportf("end < start of file")
- }
- end = fileEnd
- }
- if end > fileEnd+1 {
- if !fixed {
- bug.Reportf("end > end of file + 1")
- }
- end = fileEnd
- }
-
- return mapper.PosLocation(tokFile, start, end)
- }
-
// Now run the (pkg, analyzer) action.
var diagnostics []gobDiagnostic
@@ -1132,11 +1035,15 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) {
ResultOf: inputs,
Report: func(d analysis.Diagnostic) {
// Assert that SuggestedFixes are well formed.
+ //
+ // ValidateFixes allows a fix.End to be slightly beyond
+ // EOF to avoid spurious assertions when reporting
+ // fixes as the end of truncated files; see #71659.
if err := analysisinternal.ValidateFixes(apkg.pkg.FileSet(), analyzer, d.SuggestedFixes); err != nil {
bug.Reportf("invalid SuggestedFixes: %v", err)
d.SuggestedFixes = nil
}
- diagnostic, err := toGobDiagnostic(posToLocation, analyzer, d)
+ diagnostic, err := toGobDiagnostic(apkg.pkg, analyzer, d)
if err != nil {
// Don't bug.Report here: these errors all originate in
// posToLocation, and we can more accurately discriminate
@@ -1179,7 +1086,7 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) {
// Recover from panics (only) within the analyzer logic.
// (Use an anonymous function to limit the recover scope.)
- var result interface{}
+ var result any
func() {
start := time.Now()
defer func() {
@@ -1328,12 +1235,12 @@ type gobTextEdit struct {
// toGobDiagnostic converts an analysis.Diagnosic to a serializable gobDiagnostic,
// which requires expanding token.Pos positions into protocol.Location form.
-func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location, error), a *analysis.Analyzer, diag analysis.Diagnostic) (gobDiagnostic, error) {
+func toGobDiagnostic(pkg *Package, a *analysis.Analyzer, diag analysis.Diagnostic) (gobDiagnostic, error) {
var fixes []gobSuggestedFix
for _, fix := range diag.SuggestedFixes {
var gobEdits []gobTextEdit
for _, textEdit := range fix.TextEdits {
- loc, err := posToLocation(textEdit.Pos, textEdit.End)
+ loc, err := diagnosticPosToLocation(pkg, false, textEdit.Pos, textEdit.End)
if err != nil {
return gobDiagnostic{}, fmt.Errorf("in SuggestedFixes: %w", err)
}
@@ -1350,7 +1257,10 @@ func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location
var related []gobRelatedInformation
for _, r := range diag.Related {
- loc, err := posToLocation(r.Pos, r.End)
+ // The position of RelatedInformation may be
+ // within another (dependency) package.
+ const allowDeps = true
+ loc, err := diagnosticPosToLocation(pkg, allowDeps, r.Pos, r.End)
if err != nil {
return gobDiagnostic{}, fmt.Errorf("in Related: %w", err)
}
@@ -1360,7 +1270,7 @@ func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location
})
}
- loc, err := posToLocation(diag.Pos, diag.End)
+ loc, err := diagnosticPosToLocation(pkg, false, diag.Pos, diag.End)
if err != nil {
return gobDiagnostic{}, err
}
@@ -1388,6 +1298,126 @@ func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location
}, nil
}
+// diagnosticPosToLocation converts from token.Pos to protocol form, in the
+// context of the specified package and, optionally, its dependencies.
+func diagnosticPosToLocation(pkg *Package, allowDeps bool, start, end token.Pos) (protocol.Location, error) {
+ if end == token.NoPos {
+ end = start
+ }
+
+ fset := pkg.FileSet()
+ tokFile := fset.File(start)
+
+ // Find existing mapper by file name.
+ // (Don't require an exact token.File match
+ // as the analyzer may have re-parsed the file.)
+ var (
+ mapper *protocol.Mapper
+ fixed bool
+ )
+ for _, p := range pkg.CompiledGoFiles() {
+ if p.Tok.Name() == tokFile.Name() {
+ mapper = p.Mapper
+ fixed = p.Fixed() // suppress some assertions after parser recovery
+ break
+ }
+ }
+ // TODO(adonovan): search pkg.AsmFiles too; see #71754.
+ if mapper != nil {
+ // debugging #64547
+ fileStart := token.Pos(tokFile.Base())
+ fileEnd := fileStart + token.Pos(tokFile.Size())
+ if start < fileStart {
+ if !fixed {
+ bug.Reportf("start < start of file")
+ }
+ start = fileStart
+ }
+ if end < start {
+ // This can happen if End is zero (#66683)
+ // or a small positive displacement from zero
+ // due to recursive Node.End() computation.
+ // This usually arises from poor parser recovery
+ // of an incomplete term at EOF.
+ if !fixed {
+ bug.Reportf("end < start of file")
+ }
+ end = fileEnd
+ }
+ if end > fileEnd+1 {
+ if !fixed {
+ bug.Reportf("end > end of file + 1")
+ }
+ end = fileEnd
+ }
+
+ return mapper.PosLocation(tokFile, start, end)
+ }
+
+ // Inv: the positions are not within this package.
+
+ if allowDeps {
+ // Positions in Diagnostic.RelatedInformation may belong to a
+ // dependency package. We cannot accurately map them to
+ // protocol.Location coordinates without a Mapper for the
+ // relevant file, but none exists if the file was loaded from
+ // export data, and we have no means (Snapshot) of loading it.
+ //
+ // So, fall back to approximate conversion to UTF-16:
+ // for non-ASCII text, the column numbers may be wrong.
+ var (
+ startPosn = safetoken.StartPosition(fset, start)
+ endPosn = safetoken.EndPosition(fset, end)
+ )
+ return protocol.Location{
+ URI: protocol.URIFromPath(startPosn.Filename),
+ Range: protocol.Range{
+ Start: protocol.Position{
+ Line: uint32(startPosn.Line - 1),
+ Character: uint32(startPosn.Column - 1),
+ },
+ End: protocol.Position{
+ Line: uint32(endPosn.Line - 1),
+ Character: uint32(endPosn.Column - 1),
+ },
+ },
+ }, nil
+ }
+
+ // The start position was not among the package's parsed
+ // Go files, indicating that the analyzer added new files
+ // to the FileSet.
+ //
+ // For example, the cgocall analyzer re-parses and
+ // type-checks some of the files in a special environment;
+ // and asmdecl and other low-level runtime analyzers call
+ // ReadFile to parse non-Go files.
+ // (This is a supported feature, documented at go/analysis.)
+ //
+ // In principle these files could be:
+ //
+ // - OtherFiles (non-Go files such as asm).
+ // However, we set Pass.OtherFiles=[] because
+ // gopls won't service "diagnose" requests
+ // for non-Go files, so there's no point
+ // reporting diagnostics in them.
+ //
+ // - IgnoredFiles (files tagged for other configs).
+ // However, we set Pass.IgnoredFiles=[] because,
+ // in most cases, zero-config gopls should create
+ // another view that covers these files.
+ //
+ // - Referents of //line directives, as in cgo packages.
+ // The file names in this case are not known a priori.
+ // gopls generally tries to avoid honoring line directives,
+ // but analyzers such as cgocall may honor them.
+ //
+ // In short, it's unclear how this can be reached
+ // other than due to an analyzer bug.
+
+ return protocol.Location{}, bug.Errorf("diagnostic location is not among files of package: %s", tokFile.Name())
+}
+
// effectiveURL computes the effective URL of diag,
// using the algorithm specified at Diagnostic.URL.
func effectiveURL(a *analysis.Analyzer, diag analysis.Diagnostic) string {
diff --git a/gopls/internal/cache/cache.go b/gopls/internal/cache/cache.go
index 9f85846165f..9d6d64c9e71 100644
--- a/gopls/internal/cache/cache.go
+++ b/gopls/internal/cache/cache.go
@@ -105,7 +105,7 @@ type Cache struct {
// our best knowledge of the current file system state.
*memoizedFS
- // modCache holds the
+ // modCache holds the shared goimports state for GOMODCACHE directories
modCache *sharedModCache
}
diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go
index d094c535d7a..909003288bc 100644
--- a/gopls/internal/cache/check.go
+++ b/gopls/internal/cache/check.go
@@ -44,11 +44,6 @@ import (
"golang.org/x/tools/internal/versions"
)
-// Various optimizations that should not affect correctness.
-const (
- preserveImportGraph = true // hold on to the import graph for open packages
-)
-
type unit = struct{}
// A typeCheckBatch holds data for a logical type-checking operation, which may
@@ -97,21 +92,6 @@ func (b *typeCheckBatch) getHandle(id PackageID) *packageHandle {
return b._handles[id]
}
-// A futurePackage is a future result of type checking or importing a package,
-// to be cached in a map.
-//
-// The goroutine that creates the futurePackage is responsible for evaluating
-// its value, and closing the done channel.
-type futurePackage struct {
- done chan unit
- v pkgOrErr
-}
-
-type pkgOrErr struct {
- pkg *types.Package
- err error
-}
-
// TypeCheck parses and type-checks the specified packages,
// and returns them in the same order as the ids.
// The resulting packages' types may belong to different importers,
@@ -657,7 +637,10 @@ func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageH
go func() {
exportData, err := gcimporter.IExportShallow(b.fset, pkg, bug.Reportf)
if err != nil {
- bug.Reportf("exporting package %v: %v", ph.mp.ID, err)
+ // Internal error; the stack will have been reported via
+ // bug.Reportf within IExportShallow, so there's not much
+ // to do here (issue #71067).
+ event.Error(ctx, "IExportShallow failed", err, label.Package.Of(string(ph.mp.ID)))
return
}
if err := filecache.Set(exportDataKind, ph.key, exportData); err != nil {
@@ -701,8 +684,7 @@ func importLookup(mp *metadata.Package, source metadata.Source) func(PackagePath
// search scans children the next package in pending, looking for pkgPath.
// Invariant: whenever search is called, pkgPath is not yet mapped.
- var search func(pkgPath PackagePath) (PackageID, bool)
- search = func(pkgPath PackagePath) (id PackageID, found bool) {
+ search := func(pkgPath PackagePath) (id PackageID, found bool) {
pkg := pending[0]
pending = pending[1:]
for depPath, depID := range pkg.DepsByPkgPath {
@@ -2001,7 +1983,7 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs [
batch := func(related []types.Error) {
var diags []*Diagnostic
for i, e := range related {
- code, start, end, ok := typesinternal.ReadGo116ErrorData(e)
+ code, start, end, ok := typesinternal.ErrorCodeStartEnd(e)
if !ok || !start.IsValid() || !end.IsValid() {
start, end = e.Pos, e.Pos
code = 0
@@ -2026,15 +2008,14 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs [
posn := safetoken.StartPosition(e.Fset, start)
if !posn.IsValid() {
// All valid positions produced by the type checker should described by
- // its fileset.
+ // its fileset, yet since type checker errors are associated with
+ // positions in the AST, and AST nodes can overflow the file
+ // (golang/go#48300), we can't rely on this.
//
- // Note: in golang/go#64488, we observed an error that was positioned
- // over fixed syntax, which overflowed its file. So it's definitely
- // possible that we get here (it's hard to reason about fixing up the
- // AST). Nevertheless, it's a bug.
- if pkg.hasFixedFiles() {
- bug.Reportf("internal error: type checker error %q outside its Fset (fixed files)", e)
- } else {
+ // We should fix the parser, but in the meantime type errors are not
+ // significant if there are parse errors, so we can safely ignore this
+ // case.
+ if len(pkg.parseErrors) == 0 {
bug.Reportf("internal error: type checker error %q outside its Fset", e)
}
continue
@@ -2075,6 +2056,9 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs [
if end == start {
// Expand the end position to a more meaningful span.
+ //
+ // TODO(adonovan): It is the type checker's responsibility
+ // to ensure that (start, end) are meaningful; see #71803.
end = analysisinternal.TypeErrorEndPos(e.Fset, pgf.Src, start)
// debugging golang/go#65960
diff --git a/gopls/internal/cache/constraints.go b/gopls/internal/cache/constraints.go
index 9503abc1ebd..a9a87ae6d4b 100644
--- a/gopls/internal/cache/constraints.go
+++ b/gopls/internal/cache/constraints.go
@@ -9,6 +9,7 @@ import (
"go/build/constraint"
"go/parser"
"go/token"
+ "slices"
)
// isStandaloneFile reports whether a file with the given contents should be
@@ -27,11 +28,9 @@ func isStandaloneFile(src []byte, standaloneTags []string) bool {
found := false
walkConstraints(f, func(c constraint.Expr) bool {
if tag, ok := c.(*constraint.TagExpr); ok {
- for _, t := range standaloneTags {
- if t == tag.Tag {
- found = true
- return false
- }
+ if slices.Contains(standaloneTags, tag.Tag) {
+ found = true
+ return false
}
}
return true
diff --git a/gopls/internal/cache/filterer.go b/gopls/internal/cache/filterer.go
index 0ec18369bdf..9f911ec9de8 100644
--- a/gopls/internal/cache/filterer.go
+++ b/gopls/internal/cache/filterer.go
@@ -11,45 +11,55 @@ import (
"strings"
)
-type Filterer struct {
- // Whether a filter is excluded depends on the operator (first char of the raw filter).
- // Slices filters and excluded then should have the same length.
- filters []*regexp.Regexp
- excluded []bool
-}
-
-// NewFilterer computes regular expression form of all raw filters
-func NewFilterer(rawFilters []string) *Filterer {
- var f Filterer
- for _, filter := range rawFilters {
+// PathIncludeFunc creates a function that determines if a given file path
+// should be included based on a set of inclusion/exclusion rules.
+//
+// The `rules` parameter is a slice of strings, where each string represents a
+// filtering rule. Each rule consists of an operator (`+` for inclusion, `-`
+// for exclusion) followed by a path pattern. See more detail of rules syntax
+// at [settings.BuildOptions.DirectoryFilters].
+//
+// Rules are evaluated in order, and the last matching rule determines
+// whether a path is included or excluded.
+//
+// Examples:
+// - []{"-foo"}: Exclude "foo" at the current depth.
+// - []{"-**foo"}: Exclude "foo" at any depth.
+// - []{"+bar"}: Include "bar" at the current depth.
+// - []{"-foo", "+foo/**/bar"}: Exclude all "foo" at current depth except
+// directory "bar" under "foo" at any depth.
+func PathIncludeFunc(rules []string) func(string) bool {
+ var matchers []*regexp.Regexp
+ var included []bool
+ for _, filter := range rules {
filter = path.Clean(filepath.ToSlash(filter))
// TODO(dungtuanle): fix: validate [+-] prefix.
op, prefix := filter[0], filter[1:]
- // convertFilterToRegexp adds "/" at the end of prefix to handle cases where a filter is a prefix of another filter.
+ // convertFilterToRegexp adds "/" at the end of prefix to handle cases
+ // where a filter is a prefix of another filter.
// For example, it prevents [+foobar, -foo] from excluding "foobar".
- f.filters = append(f.filters, convertFilterToRegexp(filepath.ToSlash(prefix)))
- f.excluded = append(f.excluded, op == '-')
+ matchers = append(matchers, convertFilterToRegexp(filepath.ToSlash(prefix)))
+ included = append(included, op == '+')
}
- return &f
-}
-
-// Disallow return true if the path is excluded from the filterer's filters.
-func (f *Filterer) Disallow(path string) bool {
- // Ensure trailing but not leading slash.
- path = strings.TrimPrefix(path, "/")
- if !strings.HasSuffix(path, "/") {
- path += "/"
- }
+ return func(path string) bool {
+ // Ensure leading and trailing slashes.
+ if !strings.HasPrefix(path, "/") {
+ path = "/" + path
+ }
+ if !strings.HasSuffix(path, "/") {
+ path += "/"
+ }
- // TODO(adonovan): opt: iterate in reverse and break at first match.
- excluded := false
- for i, filter := range f.filters {
- if filter.MatchString(path) {
- excluded = f.excluded[i] // last match wins
+ // TODO(adonovan): opt: iterate in reverse and break at first match.
+ include := true
+ for i, filter := range matchers {
+ if filter.MatchString(path) {
+ include = included[i] // last match wins
+ }
}
+ return include
}
- return excluded
}
// convertFilterToRegexp replaces glob-like operator substrings in a string file path to their equivalent regex forms.
@@ -60,9 +70,9 @@ func convertFilterToRegexp(filter string) *regexp.Regexp {
return regexp.MustCompile(".*")
}
var ret strings.Builder
- ret.WriteString("^")
- segs := strings.Split(filter, "/")
- for _, seg := range segs {
+ ret.WriteString("^/")
+ segs := strings.SplitSeq(filter, "/")
+ for seg := range segs {
// Inv: seg != "" since path is clean.
if seg == "**" {
ret.WriteString(".*")
@@ -77,7 +87,7 @@ func convertFilterToRegexp(filter string) *regexp.Regexp {
// BenchmarkWorkspaceSymbols time by ~20% (even though
// filter CPU time increased by only by ~2.5%) when the
// default filter was changed to "**/node_modules".
- pattern = strings.TrimPrefix(pattern, "^.*")
+ pattern = strings.TrimPrefix(pattern, "^/.*")
return regexp.MustCompile(pattern)
}
diff --git a/gopls/internal/cache/fs_memoized.go b/gopls/internal/cache/fs_memoized.go
index 9f156e3e153..a179b0ce7f5 100644
--- a/gopls/internal/cache/fs_memoized.go
+++ b/gopls/internal/cache/fs_memoized.go
@@ -41,6 +41,8 @@ type diskFile struct {
err error
}
+func (h *diskFile) String() string { return h.uri.Path() }
+
func (h *diskFile) URI() protocol.DocumentURI { return h.uri }
func (h *diskFile) Identity() file.Identity {
diff --git a/gopls/internal/cache/fs_overlay.go b/gopls/internal/cache/fs_overlay.go
index 265598bb967..b18d6d3f154 100644
--- a/gopls/internal/cache/fs_overlay.go
+++ b/gopls/internal/cache/fs_overlay.go
@@ -64,6 +64,8 @@ type overlay struct {
saved bool
}
+func (o *overlay) String() string { return o.uri.Path() }
+
func (o *overlay) URI() protocol.DocumentURI { return o.uri }
func (o *overlay) Identity() file.Identity {
diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go
index 140cbc45490..b45669b3b79 100644
--- a/gopls/internal/cache/load.go
+++ b/gopls/internal/cache/load.go
@@ -365,7 +365,7 @@ func (s *Snapshot) config(ctx context.Context, allowNetwork AllowNetwork) *packa
packages.NeedForTest,
Fset: nil, // we do our own parsing
Overlay: s.buildOverlays(),
- Logf: func(format string, args ...interface{}) {
+ Logf: func(format string, args ...any) {
if s.view.folder.Options.VerboseOutput {
event.Log(ctx, fmt.Sprintf(format, args...))
}
@@ -791,7 +791,7 @@ func computeWorkspacePackagesLocked(ctx context.Context, s *Snapshot, meta *meta
func allFilesHaveRealPackages(g *metadata.Graph, mp *metadata.Package) bool {
n := len(mp.CompiledGoFiles)
checkURIs:
- for _, uri := range append(mp.CompiledGoFiles[0:n:n], mp.GoFiles...) {
+ for _, uri := range slices.Concat(mp.CompiledGoFiles[0:n:n], mp.GoFiles) {
for _, id := range g.IDs[uri] {
if !metadata.IsCommandLineArguments(id) {
continue checkURIs
diff --git a/gopls/internal/cache/metadata/cycle_test.go b/gopls/internal/cache/metadata/cycle_test.go
index 09628d881e9..5f935f603c8 100644
--- a/gopls/internal/cache/metadata/cycle_test.go
+++ b/gopls/internal/cache/metadata/cycle_test.go
@@ -5,6 +5,7 @@
package metadata
import (
+ "maps"
"sort"
"strings"
"testing"
@@ -40,11 +41,11 @@ func TestBreakImportCycles(t *testing.T) {
return n
}
if s != "" {
- for _, item := range strings.Split(s, ";") {
+ for item := range strings.SplitSeq(s, ";") {
nodeID, succIDs, ok := strings.Cut(item, "->")
node := makeNode(nodeID)
if ok {
- for _, succID := range strings.Split(succIDs, ",") {
+ for succID := range strings.SplitSeq(succIDs, ",") {
node.DepsByPkgPath[PackagePath(succID)] = PackageID(succID)
}
}
@@ -119,9 +120,7 @@ func TestBreakImportCycles(t *testing.T) {
// Apply updates.
// (parse doesn't have a way to express node deletions,
// but they aren't very interesting.)
- for id, mp := range updates {
- metadata[id] = mp
- }
+ maps.Copy(metadata, updates)
t.Log("updated", format(metadata))
diff --git a/gopls/internal/cache/metadata/graph.go b/gopls/internal/cache/metadata/graph.go
index 4b846df53be..b029b51aa7e 100644
--- a/gopls/internal/cache/metadata/graph.go
+++ b/gopls/internal/cache/metadata/graph.go
@@ -5,7 +5,10 @@
package metadata
import (
+ "iter"
+ "maps"
"sort"
+ "strings"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/gopls/internal/protocol"
@@ -61,9 +64,7 @@ func (g *Graph) Update(updates map[PackageID]*Package) *Graph {
// Copy pkgs map then apply updates.
pkgs := make(map[PackageID]*Package, len(g.Packages))
- for id, mp := range g.Packages {
- pkgs[id] = mp
- }
+ maps.Copy(pkgs, g.Packages)
for id, mp := range updates {
if mp == nil {
delete(pkgs, id)
@@ -99,6 +100,11 @@ func newGraph(pkgs map[PackageID]*Package) *Graph {
for _, uri := range mp.GoFiles {
uris[uri] = struct{}{}
}
+ for _, uri := range mp.OtherFiles {
+ if strings.HasSuffix(string(uri), ".s") { // assembly
+ uris[uri] = struct{}{}
+ }
+ }
for uri := range uris {
uriIDs[uri] = append(uriIDs[uri], id)
}
@@ -160,6 +166,35 @@ func (g *Graph) ReverseReflexiveTransitiveClosure(ids ...PackageID) map[PackageI
return seen
}
+// ForwardReflexiveTransitiveClosure returns an iterator over the
+// specified nodes and all their forward dependencies, in an arbitrary
+// topological (dependencies-first) order. The order may vary.
+func (g *Graph) ForwardReflexiveTransitiveClosure(ids ...PackageID) iter.Seq[*Package] {
+ return func(yield func(*Package) bool) {
+ seen := make(map[PackageID]bool)
+ var visit func(PackageID) bool
+ visit = func(id PackageID) bool {
+ if !seen[id] {
+ seen[id] = true
+ if mp := g.Packages[id]; mp != nil {
+ for _, depID := range mp.DepsByPkgPath {
+ if !visit(depID) {
+ return false
+ }
+ }
+ if !yield(mp) {
+ return false
+ }
+ }
+ }
+ return true
+ }
+ for _, id := range ids {
+ visit(id)
+ }
+ }
+}
+
// breakImportCycles breaks import cycles in the metadata by deleting
// Deps* edges. It modifies only metadata present in the 'updates'
// subset. This function has an internal test.
diff --git a/gopls/internal/cache/methodsets/methodsets.go b/gopls/internal/cache/methodsets/methodsets.go
index 3026819ee81..873d2d01289 100644
--- a/gopls/internal/cache/methodsets/methodsets.go
+++ b/gopls/internal/cache/methodsets/methodsets.go
@@ -51,7 +51,9 @@ import (
"sync/atomic"
"golang.org/x/tools/go/types/objectpath"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/util/bug"
+ "golang.org/x/tools/gopls/internal/util/fingerprint"
"golang.org/x/tools/gopls/internal/util/frob"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/typesinternal"
@@ -61,14 +63,15 @@ import (
// types in a package in a form that permits assignability queries
// without the type checker.
type Index struct {
- pkg gobPackage
+ pkg gobPackage
+ PkgPath metadata.PackagePath
}
// Decode decodes the given gob-encoded data as an Index.
-func Decode(data []byte) *Index {
+func Decode(pkgpath metadata.PackagePath, data []byte) *Index {
var pkg gobPackage
packageCodec.Decode(data, &pkg)
- return &Index{pkg}
+ return &Index{pkg: pkg, PkgPath: pkgpath}
}
// Encode encodes the receiver as gob-encoded data.
@@ -109,36 +112,61 @@ func KeyOf(t types.Type) (Key, bool) {
// A Result reports a matching type or method in a method-set search.
type Result struct {
- Location Location // location of the type or method
+ TypeName string // name of the named type
+ IsInterface bool // matched type (or method) is abstract
+ Location Location // location of the type or method
// methods only:
PkgPath string // path of declaring package (may differ due to embedding)
ObjectPath objectpath.Path // path of method within declaring package
}
-// Search reports each type that implements (or is implemented by) the
-// type that produced the search key. If methodID is nonempty, only
-// that method of each type is reported.
+// TypeRelation indicates the direction of subtyping relation,
+// if any, between two types.
+//
+// It is a bitset, so that clients of Implementations may use
+// Supertype|Subtype to request an undirected match.
+type TypeRelation int8
+
+const (
+ Supertype TypeRelation = 0x1
+ Subtype TypeRelation = 0x2
+)
+
+// Search reports each type that implements (Supertype ∈ want) or is
+// implemented by (Subtype ∈ want) the type that produced the search key.
+//
+// If method is non-nil, only that method of each type is reported.
//
// The result does not include the error.Error method.
// TODO(adonovan): give this special case a more systematic treatment.
-func (index *Index) Search(key Key, method *types.Func) []Result {
+func (index *Index) Search(key Key, want TypeRelation, method *types.Func) []Result {
var results []Result
for _, candidate := range index.pkg.MethodSets {
- // Traditionally this feature doesn't report
- // interface/interface elements of the relation.
- // I think that's a mistake.
- // TODO(adonovan): UX: change it, here and in the local implementation.
- if candidate.IsInterface && key.mset.IsInterface {
- continue
+ // Test the direction of the relation.
+ // The client may request either direction or both
+ // (e.g. when the client is References),
+ // and the Result reports each test independently;
+ // both tests succeed when comparing identical
+ // interface types.
+ var got TypeRelation
+ if want&Subtype != 0 && implements(candidate, key.mset) {
+ got |= Subtype
}
-
- if !implements(candidate, key.mset) && !implements(key.mset, candidate) {
+ if want&Supertype != 0 && implements(key.mset, candidate) {
+ got |= Supertype
+ }
+ if got == 0 {
continue
}
+ typeName := index.pkg.Strings[candidate.TypeName]
if method == nil {
- results = append(results, Result{Location: index.location(candidate.Posn)})
+ results = append(results, Result{
+ TypeName: typeName,
+ IsInterface: candidate.IsInterface,
+ Location: index.location(candidate.Posn),
+ })
} else {
for _, m := range candidate.Methods {
if m.ID == method.Id() {
@@ -153,9 +181,11 @@ func (index *Index) Search(key Key, method *types.Func) []Result {
}
results = append(results, Result{
- Location: index.location(m.Posn),
- PkgPath: index.pkg.Strings[m.PkgPath],
- ObjectPath: objectpath.Path(index.pkg.Strings[m.ObjectPath]),
+ TypeName: typeName,
+ IsInterface: candidate.IsInterface,
+ Location: index.location(m.Posn),
+ PkgPath: index.pkg.Strings[m.PkgPath],
+ ObjectPath: objectpath.Path(index.pkg.Strings[m.ObjectPath]),
})
break
}
@@ -195,7 +225,7 @@ func implements(x, y *gobMethodSet) bool {
// so a string match is sufficient.
match = mx.Sum&my.Sum == my.Sum && mx.Fingerprint == my.Fingerprint
} else {
- match = unify(mx.parse(), my.parse())
+ match = fingerprint.Matches(mx.parse(), my.parse())
}
return !match
}
@@ -284,6 +314,7 @@ func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index {
for _, name := range scope.Names() {
if tname, ok := scope.Lookup(name).(*types.TypeName); ok && !tname.IsAlias() {
if mset := methodSetInfo(tname.Type(), setIndexInfo); mset.Mask != 0 {
+ mset.TypeName = b.string(name)
mset.Posn = objectPos(tname)
// Only record types with non-trivial method sets.
b.MethodSets = append(b.MethodSets, mset)
@@ -291,7 +322,10 @@ func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index {
}
}
- return &Index{pkg: b.gobPackage}
+ return &Index{
+ pkg: b.gobPackage,
+ PkgPath: metadata.PackagePath(pkg.Path()),
+ }
}
// string returns a small integer that encodes the string.
@@ -326,7 +360,7 @@ func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) *go
for i := 0; i < mset.Len(); i++ {
m := mset.At(i).Obj().(*types.Func)
id := m.Id()
- fp, isTricky := fingerprint(m.Signature())
+ fp, isTricky := fingerprint.Encode(m.Signature())
if isTricky {
tricky = true
}
@@ -369,6 +403,7 @@ type gobPackage struct {
// A gobMethodSet records the method set of a single type.
type gobMethodSet struct {
+ TypeName int // name (string index) of the package-level type
Posn gobPosition
IsInterface bool
Tricky bool // at least one method is tricky; fingerprint must be parsed + unified
@@ -389,7 +424,7 @@ type gobMethod struct {
ObjectPath int // object path of method relative to PkgPath
// internal fields (not serialized)
- tree atomic.Pointer[sexpr] // fingerprint tree, parsed on demand
+ tree atomic.Pointer[fingerprint.Tree] // fingerprint tree, parsed on demand
}
// A gobPosition records the file, offset, and length of an identifier.
@@ -400,10 +435,10 @@ type gobPosition struct {
// parse returns the method's parsed fingerprint tree.
// It may return a new instance or a cached one.
-func (m *gobMethod) parse() sexpr {
+func (m *gobMethod) parse() fingerprint.Tree {
ptr := m.tree.Load()
if ptr == nil {
- tree := parseFingerprint(m.Fingerprint)
+ tree := fingerprint.Parse(m.Fingerprint)
ptr = &tree
m.tree.Store(ptr) // may race; that's ok
}
diff --git a/gopls/internal/cache/mod.go b/gopls/internal/cache/mod.go
index f16cfbfe1af..ddbe516f165 100644
--- a/gopls/internal/cache/mod.go
+++ b/gopls/internal/cache/mod.go
@@ -13,6 +13,7 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
+ "golang.org/x/tools/go/packages"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/label"
"golang.org/x/tools/gopls/internal/protocol"
@@ -25,6 +26,7 @@ import (
type ParsedModule struct {
URI protocol.DocumentURI
File *modfile.File
+ ReplaceMap map[module.Version]module.Version
Mapper *protocol.Mapper
ParseErrors []*Diagnostic
}
@@ -45,14 +47,14 @@ func (s *Snapshot) ParseMod(ctx context.Context, fh file.Handle) (*ParsedModule,
// cache miss?
if !hit {
- promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} {
+ promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ any) any {
parsed, err := parseModImpl(ctx, fh)
return parseModResult{parsed, err}
})
entry = promise
s.mu.Lock()
- s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() })
+ s.parseModHandles.Set(uri, entry, func(_, _ any) { release() })
s.mu.Unlock()
}
@@ -98,10 +100,19 @@ func parseModImpl(ctx context.Context, fh file.Handle) (*ParsedModule, error) {
})
}
}
+
+ replaceMap := make(map[module.Version]module.Version)
+ if parseErr == nil {
+ for _, rep := range file.Replace {
+ replaceMap[rep.Old] = rep.New
+ }
+ }
+
return &ParsedModule{
URI: fh.URI(),
Mapper: m,
File: file,
+ ReplaceMap: replaceMap,
ParseErrors: parseErrors,
}, parseErr
}
@@ -131,14 +142,14 @@ func (s *Snapshot) ParseWork(ctx context.Context, fh file.Handle) (*ParsedWorkFi
// cache miss?
if !hit {
- handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} {
+ handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ any) any {
parsed, err := parseWorkImpl(ctx, fh)
return parseWorkResult{parsed, err}
})
entry = handle
s.mu.Lock()
- s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() })
+ s.parseWorkHandles.Set(uri, entry, func(_, _ any) { release() })
s.mu.Unlock()
}
@@ -212,7 +223,7 @@ func (s *Snapshot) ModWhy(ctx context.Context, fh file.Handle) (map[string]strin
// cache miss?
if !hit {
- handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg interface{}) interface{} {
+ handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg any) any {
why, err := modWhyImpl(ctx, arg.(*Snapshot), fh)
return modWhyResult{why, err}
})
@@ -487,3 +498,31 @@ func findModuleReference(mf *modfile.File, ver module.Version) *modfile.Line {
}
return nil
}
+
+// ResolvedVersion returns the version used for a module, which considers replace directive.
+func ResolvedVersion(module *packages.Module) string {
+ // don't visit replace recursively as src/cmd/go/internal/modinfo/info.go
+ // visits replace field only once.
+ if module.Replace != nil {
+ return module.Replace.Version
+ }
+ return module.Version
+}
+
+// ResolvedPath returns the the module path, which considers replace directive.
+func ResolvedPath(module *packages.Module) string {
+ if module.Replace != nil {
+ return module.Replace.Path
+ }
+ return module.Path
+}
+
+// ResolvedString returns a representation of the Version suitable for logging
+// (Path@Version, or just Path if Version is empty),
+// which considers replace directive.
+func ResolvedString(module *packages.Module) string {
+ if ResolvedVersion(module) == "" {
+ ResolvedPath(module)
+ }
+ return ResolvedPath(module) + "@" + ResolvedVersion(module)
+}
diff --git a/gopls/internal/cache/mod_tidy.go b/gopls/internal/cache/mod_tidy.go
index 4d473d39b12..6d9a3e56b81 100644
--- a/gopls/internal/cache/mod_tidy.go
+++ b/gopls/internal/cache/mod_tidy.go
@@ -76,7 +76,7 @@ func (s *Snapshot) ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule
return nil, err
}
- handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} {
+ handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg any) any {
tidied, err := modTidyImpl(ctx, arg.(*Snapshot), pm)
return modTidyResult{tidied, err}
})
diff --git a/gopls/internal/cache/mod_vuln.go b/gopls/internal/cache/mod_vuln.go
index a92f5b5abe1..5b7d679fa48 100644
--- a/gopls/internal/cache/mod_vuln.go
+++ b/gopls/internal/cache/mod_vuln.go
@@ -40,7 +40,7 @@ func (s *Snapshot) ModVuln(ctx context.Context, modURI protocol.DocumentURI) (*v
// Cache miss?
if !hit {
- handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg interface{}) interface{} {
+ handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg any) any {
result, err := modVulnImpl(ctx, arg.(*Snapshot))
return modVuln{result, err}
})
@@ -126,7 +126,6 @@ func modVulnImpl(ctx context.Context, snapshot *Snapshot) (*vulncheck.Result, er
var group errgroup.Group
group.SetLimit(10) // limit govulncheck api runs
for _, mps := range packagesByModule {
- mps := mps
group.Go(func() error {
effectiveModule := stdlibModule
if m := mps[0].Module; m != nil {
diff --git a/gopls/internal/cache/parse_cache.go b/gopls/internal/cache/parse_cache.go
index 8586f655d28..015510b881d 100644
--- a/gopls/internal/cache/parse_cache.go
+++ b/gopls/internal/cache/parse_cache.go
@@ -195,7 +195,7 @@ func (c *parseCache) startParse(mode parser.Mode, purgeFuncBodies bool, fhs ...f
}
uri := fh.URI()
- promise := memoize.NewPromise("parseCache.parse", func(ctx context.Context, _ interface{}) interface{} {
+ promise := memoize.NewPromise("parseCache.parse", func(ctx context.Context, _ any) any {
// Allocate 2*len(content)+parsePadding to allow for re-parsing once
// inside of parseGoSrc without exceeding the allocated space.
base, nextBase := c.allocateSpace(2*len(content) + parsePadding)
@@ -404,13 +404,13 @@ func (q queue) Swap(i, j int) {
q[j].lruIndex = j
}
-func (q *queue) Push(x interface{}) {
+func (q *queue) Push(x any) {
e := x.(*parseCacheEntry)
e.lruIndex = len(*q)
*q = append(*q, e)
}
-func (q *queue) Pop() interface{} {
+func (q *queue) Pop() any {
last := len(*q) - 1
e := (*q)[last]
(*q)[last] = nil // aid GC
diff --git a/gopls/internal/cache/parse_cache_test.go b/gopls/internal/cache/parse_cache_test.go
index 7aefac77c38..4e3a7cf32b7 100644
--- a/gopls/internal/cache/parse_cache_test.go
+++ b/gopls/internal/cache/parse_cache_test.go
@@ -195,9 +195,9 @@ func TestParseCache_Duplicates(t *testing.T) {
func dummyFileHandles(n int) []file.Handle {
var fhs []file.Handle
- for i := 0; i < n; i++ {
+ for i := range n {
uri := protocol.DocumentURI(fmt.Sprintf("file:///_%d", i))
- src := []byte(fmt.Sprintf("package p\nvar _ = %d", i))
+ src := fmt.Appendf(nil, "package p\nvar _ = %d", i)
fhs = append(fhs, makeFakeFileHandle(uri, src))
}
return fhs
@@ -218,6 +218,10 @@ type fakeFileHandle struct {
hash file.Hash
}
+func (h fakeFileHandle) String() string {
+ return h.uri.Path()
+}
+
func (h fakeFileHandle) URI() protocol.DocumentURI {
return h.uri
}
diff --git a/gopls/internal/cache/parsego/file.go b/gopls/internal/cache/parsego/file.go
index 41fd1937ec1..2be4ed4b2ca 100644
--- a/gopls/internal/cache/parsego/file.go
+++ b/gopls/internal/cache/parsego/file.go
@@ -14,6 +14,7 @@ import (
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/internal/astutil/cursor"
)
// A File contains the results of parsing a Go file.
@@ -32,6 +33,8 @@ type File struct {
// actual content of the file if we have fixed the AST.
Src []byte
+ Cursor cursor.Cursor // cursor of *ast.File, sans sibling files
+
// fixedSrc and fixedAST report on "fixing" that occurred during parsing of
// this file.
//
@@ -71,6 +74,11 @@ func (pgf *File) PositionPos(p protocol.Position) (token.Pos, error) {
return safetoken.Pos(pgf.Tok, offset)
}
+// PosPosition returns a protocol Position for the token.Pos in this file.
+func (pgf *File) PosPosition(pos token.Pos) (protocol.Position, error) {
+ return pgf.Mapper.PosPosition(pgf.Tok, pos)
+}
+
// PosRange returns a protocol Range for the token.Pos interval in this file.
func (pgf *File) PosRange(start, end token.Pos) (protocol.Range, error) {
return pgf.Mapper.PosRange(pgf.Tok, start, end)
diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go
index df167314b04..bc5483fc166 100644
--- a/gopls/internal/cache/parsego/parse.go
+++ b/gopls/internal/cache/parsego/parse.go
@@ -23,11 +23,12 @@ import (
"reflect"
"slices"
+ "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/gopls/internal/label"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/astutil"
- "golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
)
@@ -48,7 +49,7 @@ const (
// Parse parses a buffer of Go source, repairing the tree if necessary.
//
// The provided ctx is used only for logging.
-func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, src []byte, mode parser.Mode, purgeFuncBodies bool) (res *File, fixes []fixType) {
+func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, src []byte, mode parser.Mode, purgeFuncBodies bool) (res *File, fixes []FixType) {
if purgeFuncBodies {
src = astutil.PurgeFuncBodies(src)
}
@@ -63,39 +64,8 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s
}
// Inv: file != nil.
- // Workaround for #70162 (missing File{Start,End} when
- // parsing empty file) with go1.23.
- //
- // When parsing an empty file, or one without a valid
- // package declaration, the go1.23 parser bails out before
- // setting FileStart and End.
- //
- // This leaves us no way to find the original
- // token.File that ParseFile created, so as a
- // workaround, we recreate the token.File, and
- // populate the FileStart and FileEnd fields.
- //
- // See also #53202.
tokenFile := func(file *ast.File) *token.File {
- tok := fset.File(file.FileStart)
- if tok == nil {
- // Invalid File.FileStart (also File.{Package,Name.Pos}).
- if file.Package.IsValid() {
- bug.Report("ast.File has valid Package but no FileStart")
- }
- if file.Name.Pos().IsValid() {
- bug.Report("ast.File has valid Name.Pos but no FileStart")
- }
- tok = fset.AddFile(uri.Path(), -1, len(src))
- tok.SetLinesForContent(src)
- // If the File contained any valid token.Pos values,
- // they would all be invalid wrt the new token.File,
- // but we have established that it lacks FileStart,
- // Package, and Name.Pos.
- file.FileStart = token.Pos(tok.Base())
- file.FileEnd = token.Pos(tok.Base() + tok.Size())
- }
- return tok
+ return fset.File(file.FileStart)
}
tok := tokenFile(file)
@@ -111,7 +81,7 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s
fixes = append(fixes, astFixes...)
}
- for i := 0; i < 10; i++ {
+ for i := range 10 {
// Fix certain syntax errors that render the file unparseable.
newSrc, srcFix := fixSrc(file, tok, src)
if newSrc == nil {
@@ -153,6 +123,11 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s
}
assert(file != nil, "nil *ast.File")
+ // Provide a cursor for fast and convenient navigation.
+ inspect := inspector.New([]*ast.File{file})
+ curFile, _ := cursor.Root(inspect).FirstChild()
+ _ = curFile.Node().(*ast.File)
+
return &File{
URI: uri,
Mode: mode,
@@ -161,6 +136,7 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s
fixedAST: fixedAST,
File: file,
Tok: tok,
+ Cursor: curFile,
Mapper: protocol.NewMapper(uri, src),
ParseErr: parseErr,
}, fixes
@@ -171,13 +147,13 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s
//
// If fixAST returns true, the resulting AST is considered "fixed", meaning
// positions have been mangled, and type checker errors may not make sense.
-func fixAST(n ast.Node, tok *token.File, src []byte) (fixes []fixType) {
+func fixAST(n ast.Node, tok *token.File, src []byte) (fixes []FixType) {
var err error
walkASTWithParent(n, func(n, parent ast.Node) bool {
switch n := n.(type) {
case *ast.BadStmt:
if fixDeferOrGoStmt(n, parent, tok, src) {
- fixes = append(fixes, fixedDeferOrGo)
+ fixes = append(fixes, FixedDeferOrGo)
// Recursively fix in our fixed node.
moreFixes := fixAST(parent, tok, src)
fixes = append(fixes, moreFixes...)
@@ -187,7 +163,7 @@ func fixAST(n ast.Node, tok *token.File, src []byte) (fixes []fixType) {
return false
case *ast.BadExpr:
if fixArrayType(n, parent, tok, src) {
- fixes = append(fixes, fixedArrayType)
+ fixes = append(fixes, FixedArrayType)
// Recursively fix in our fixed node.
moreFixes := fixAST(parent, tok, src)
fixes = append(fixes, moreFixes...)
@@ -201,7 +177,7 @@ func fixAST(n ast.Node, tok *token.File, src []byte) (fixes []fixType) {
// for i := foo
//
if fixInitStmt(n, parent, tok, src) {
- fixes = append(fixes, fixedInit)
+ fixes = append(fixes, FixedInit)
}
return false
case *ast.SelectorExpr:
@@ -210,7 +186,7 @@ func fixAST(n ast.Node, tok *token.File, src []byte) (fixes []fixType) {
// foo.var<> // want to complete to "foo.variance"
//
if fixPhantomSelector(n, tok, src) {
- fixes = append(fixes, fixedPhantomSelector)
+ fixes = append(fixes, FixedPhantomSelector)
}
return true
@@ -220,7 +196,7 @@ func fixAST(n ast.Node, tok *token.File, src []byte) (fixes []fixType) {
// Adjust closing curly brace of empty switch/select
// statements so we can complete inside them.
if fixEmptySwitch(n, tok, src) {
- fixes = append(fixes, fixedEmptySwitch)
+ fixes = append(fixes, FixedEmptySwitch)
}
}
@@ -259,24 +235,24 @@ func walkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) {
// TODO(rfindley): revert this intrumentation once we're certain the crash in
// #59097 is fixed.
-type fixType int
+type FixType int
const (
- noFix fixType = iota
- fixedCurlies
- fixedDanglingSelector
- fixedDeferOrGo
- fixedArrayType
- fixedInit
- fixedPhantomSelector
- fixedEmptySwitch
+ noFix FixType = iota
+ FixedCurlies
+ FixedDanglingSelector
+ FixedDeferOrGo
+ FixedArrayType
+ FixedInit
+ FixedPhantomSelector
+ FixedEmptySwitch
)
// fixSrc attempts to modify the file's source code to fix certain
// syntax errors that leave the rest of the file unparsed.
//
// fixSrc returns a non-nil result if and only if a fix was applied.
-func fixSrc(f *ast.File, tf *token.File, src []byte) (newSrc []byte, fix fixType) {
+func fixSrc(f *ast.File, tf *token.File, src []byte) (newSrc []byte, fix FixType) {
walkASTWithParent(f, func(n, parent ast.Node) bool {
if newSrc != nil {
return false
@@ -286,12 +262,12 @@ func fixSrc(f *ast.File, tf *token.File, src []byte) (newSrc []byte, fix fixType
case *ast.BlockStmt:
newSrc = fixMissingCurlies(f, n, parent, tf, src)
if newSrc != nil {
- fix = fixedCurlies
+ fix = FixedCurlies
}
case *ast.SelectorExpr:
newSrc = fixDanglingSelector(n, tf, src)
if newSrc != nil {
- fix = fixedDanglingSelector
+ fix = FixedDanglingSelector
}
}
@@ -552,11 +528,12 @@ func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte)
}
// Try to extract a statement from the BadExpr.
- start, end, err := safetoken.Offsets(tok, bad.Pos(), bad.End()-1)
+ start, end, err := safetoken.Offsets(tok, bad.Pos(), bad.End())
if err != nil {
return false
}
- stmtBytes := src[start : end+1]
+ assert(end <= len(src), "offset overflow") // golang/go#72026
+ stmtBytes := src[start:end]
stmt, err := parseStmt(tok, bad.Pos(), stmtBytes)
if err != nil {
return false
@@ -926,10 +903,7 @@ func offsetPositions(tok *token.File, n ast.Node, offset token.Pos) {
//
// TODO(golang/go#64335): this is a hack, because our fixes should not
// produce positions that overflow (but they do: golang/go#64488).
- pos := f.Int() + int64(offset)
- if pos < fileBase {
- pos = fileBase
- }
+ pos := max(f.Int()+int64(offset), fileBase)
if pos > fileEnd {
pos = fileEnd
}
diff --git a/gopls/internal/cache/parsego/parse_test.go b/gopls/internal/cache/parsego/parse_test.go
index c64125427b1..db78b596042 100644
--- a/gopls/internal/cache/parsego/parse_test.go
+++ b/gopls/internal/cache/parsego/parse_test.go
@@ -6,12 +6,17 @@ package parsego_test
import (
"context"
+ "fmt"
"go/ast"
+ "go/parser"
"go/token"
+ "reflect"
+ "slices"
"testing"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/tokeninternal"
)
@@ -44,3 +49,319 @@ func _() {
return true
})
}
+
+func TestFixGoAndDefer(t *testing.T) {
+ var testCases = []struct {
+ source string
+ fixes []parsego.FixType
+ wantFix string
+ }{
+ {source: "", fixes: nil}, // keyword alone
+ {source: "a.b(", fixes: nil},
+ {source: "a.b()", fixes: nil},
+ {source: "func {", fixes: nil},
+ {
+ source: "f",
+ fixes: []parsego.FixType{parsego.FixedDeferOrGo},
+ wantFix: "f()",
+ },
+ {
+ source: "func",
+ fixes: []parsego.FixType{parsego.FixedDeferOrGo},
+ wantFix: "(func())()",
+ },
+ {
+ source: "func {}",
+ fixes: []parsego.FixType{parsego.FixedDeferOrGo},
+ wantFix: "(func())()",
+ },
+ {
+ source: "func {}(",
+ fixes: []parsego.FixType{parsego.FixedDeferOrGo},
+ wantFix: "(func())()",
+ },
+ {
+ source: "func {}()",
+ fixes: []parsego.FixType{parsego.FixedDeferOrGo},
+ wantFix: "(func())()",
+ },
+ {
+ source: "a.",
+ fixes: []parsego.FixType{parsego.FixedDeferOrGo, parsego.FixedDanglingSelector, parsego.FixedDeferOrGo},
+ wantFix: "a._()",
+ },
+ {
+ source: "a.b",
+ fixes: []parsego.FixType{parsego.FixedDeferOrGo},
+ wantFix: "a.b()",
+ },
+ }
+
+ for _, keyword := range []string{"go", "defer"} {
+ for _, tc := range testCases {
+ source := fmt.Sprintf("%s %s", keyword, tc.source)
+ t.Run(source, func(t *testing.T) {
+ src := filesrc(source)
+ pgf, fixes := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", src, parsego.Full, false)
+ if !slices.Equal(fixes, tc.fixes) {
+ t.Fatalf("got %v want %v", fixes, tc.fixes)
+ }
+ if tc.fixes == nil {
+ return
+ }
+
+ fset := tokeninternal.FileSetFor(pgf.Tok)
+ inspect(t, pgf, func(stmt ast.Stmt) {
+ var call *ast.CallExpr
+ switch stmt := stmt.(type) {
+ case *ast.DeferStmt:
+ call = stmt.Call
+ case *ast.GoStmt:
+ call = stmt.Call
+ default:
+ return
+ }
+
+ if got := analysisinternal.Format(fset, call); got != tc.wantFix {
+ t.Fatalf("got %v want %v", got, tc.wantFix)
+ }
+ })
+ })
+ }
+ }
+}
+
+// TestFixInit tests the init stmt after if/for/switch which is put under cond after parsing
+// will be fixed and moved to Init.
+func TestFixInit(t *testing.T) {
+ var testCases = []struct {
+ name string
+ source string
+ fixes []parsego.FixType
+ wantInitFix string
+ }{
+ {
+ name: "simple define",
+ source: "i := 0",
+ fixes: []parsego.FixType{parsego.FixedInit},
+ wantInitFix: "i := 0",
+ },
+ {
+ name: "simple assign",
+ source: "i = 0",
+ fixes: []parsego.FixType{parsego.FixedInit},
+ wantInitFix: "i = 0",
+ },
+ {
+ name: "define with function call",
+ source: "i := f()",
+ fixes: []parsego.FixType{parsego.FixedInit},
+ wantInitFix: "i := f()",
+ },
+ {
+ name: "assign with function call",
+ source: "i = f()",
+ fixes: []parsego.FixType{parsego.FixedInit},
+ wantInitFix: "i = f()",
+ },
+ {
+ name: "assign with receiving chan",
+ source: "i = <-ch",
+ fixes: []parsego.FixType{parsego.FixedInit},
+ wantInitFix: "i = <-ch",
+ },
+
+ // fixInitStmt won't fix the following cases.
+ {
+ name: "call in if",
+ source: `fmt.Println("helloworld")`,
+ fixes: nil,
+ },
+ {
+ name: "receive chan",
+ source: `<- ch`,
+ fixes: nil,
+ },
+ }
+
+ // currently, switch will leave its Tag empty after fix because it allows empty,
+ // and if and for will leave an underscore in Cond.
+ getWantCond := func(keyword string) string {
+ if keyword == "switch" {
+ return ""
+ }
+ return "_"
+ }
+
+ for _, keyword := range []string{"if", "for", "switch"} {
+ for _, tc := range testCases {
+ caseName := fmt.Sprintf("%s %s", keyword, tc.name)
+ t.Run(caseName, func(t *testing.T) {
+ // the init stmt is treated as a cond.
+ src := filesrc(fmt.Sprintf("%s %s {}", keyword, tc.source))
+ pgf, fixes := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", src, parsego.Full, false)
+ if !slices.Equal(fixes, tc.fixes) {
+ t.Fatalf("TestFixArrayType(): got %v want %v", fixes, tc.fixes)
+ }
+ if tc.fixes == nil {
+ return
+ }
+
+ // ensure the init stmt is parsed to a BadExpr.
+ ensureSource(t, src, func(bad *ast.BadExpr) {})
+
+ info := func(n ast.Node, wantStmt string) (init ast.Stmt, cond ast.Expr, has bool) {
+ switch wantStmt {
+ case "if":
+ if e, ok := n.(*ast.IfStmt); ok {
+ return e.Init, e.Cond, true
+ }
+ case "switch":
+ if e, ok := n.(*ast.SwitchStmt); ok {
+ return e.Init, e.Tag, true
+ }
+ case "for":
+ if e, ok := n.(*ast.ForStmt); ok {
+ return e.Init, e.Cond, true
+ }
+ }
+ return nil, nil, false
+ }
+ fset := tokeninternal.FileSetFor(pgf.Tok)
+ inspect(t, pgf, func(n ast.Stmt) {
+ if init, cond, ok := info(n, keyword); ok {
+ if got := analysisinternal.Format(fset, init); got != tc.wantInitFix {
+ t.Fatalf("%s: Init got %v want %v", tc.source, got, tc.wantInitFix)
+ }
+
+ wantCond := getWantCond(keyword)
+ if got := analysisinternal.Format(fset, cond); got != wantCond {
+ t.Fatalf("%s: Cond got %v want %v", tc.source, got, wantCond)
+ }
+ }
+ })
+ })
+ }
+ }
+}
+
+func TestFixPhantomSelector(t *testing.T) {
+ wantFixes := []parsego.FixType{parsego.FixedPhantomSelector}
+ var testCases = []struct {
+ source string
+ fixes []parsego.FixType
+ }{
+ {source: "a.break", fixes: wantFixes},
+ {source: "_.break", fixes: wantFixes},
+ {source: "a.case", fixes: wantFixes},
+ {source: "a.chan", fixes: wantFixes},
+ {source: "a.const", fixes: wantFixes},
+ {source: "a.continue", fixes: wantFixes},
+ {source: "a.default", fixes: wantFixes},
+ {source: "a.defer", fixes: wantFixes},
+ {source: "a.else", fixes: wantFixes},
+ {source: "a.fallthrough", fixes: wantFixes},
+ {source: "a.for", fixes: wantFixes},
+ {source: "a.func", fixes: wantFixes},
+ {source: "a.go", fixes: wantFixes},
+ {source: "a.goto", fixes: wantFixes},
+ {source: "a.if", fixes: wantFixes},
+ {source: "a.import", fixes: wantFixes},
+ {source: "a.interface", fixes: wantFixes},
+ {source: "a.map", fixes: wantFixes},
+ {source: "a.package", fixes: wantFixes},
+ {source: "a.range", fixes: wantFixes},
+ {source: "a.return", fixes: wantFixes},
+ {source: "a.select", fixes: wantFixes},
+ {source: "a.struct", fixes: wantFixes},
+ {source: "a.switch", fixes: wantFixes},
+ {source: "a.type", fixes: wantFixes},
+ {source: "a.var", fixes: wantFixes},
+
+ {source: "break.break"},
+ {source: "a.BREAK"},
+ {source: "a.break_"},
+ {source: "a.breaka"},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.source, func(t *testing.T) {
+ src := filesrc(tc.source)
+ pgf, fixes := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", src, parsego.Full, false)
+ if !slices.Equal(fixes, tc.fixes) {
+ t.Fatalf("got %v want %v", fixes, tc.fixes)
+ }
+
+ // some fixes don't fit the fix scenario, but we want to confirm it.
+ if fixes == nil {
+ return
+ }
+
+ // ensure the selector has been converted to underscore by parser.
+ ensureSource(t, src, func(sel *ast.SelectorExpr) {
+ if sel.Sel.Name != "_" {
+ t.Errorf("%s: the input doesn't cause a blank selector after parser", tc.source)
+ }
+ })
+
+ fset := tokeninternal.FileSetFor(pgf.Tok)
+ inspect(t, pgf, func(sel *ast.SelectorExpr) {
+ // the fix should restore the selector as is.
+ if got, want := fmt.Sprintf("%s", analysisinternal.Format(fset, sel)), tc.source; got != want {
+ t.Fatalf("got %v want %v", got, want)
+ }
+ })
+ })
+ }
+}
+
+// inspect helps to go through each node of pgf and trigger checkFn if the type matches T.
+func inspect[T ast.Node](t *testing.T, pgf *parsego.File, checkFn func(n T)) {
+ fset := tokeninternal.FileSetFor(pgf.Tok)
+ var visited bool
+ ast.Inspect(pgf.File, func(node ast.Node) bool {
+ if node != nil {
+ posn := safetoken.StartPosition(fset, node.Pos())
+ if !posn.IsValid() {
+ t.Fatalf("invalid position for %T (%v): %v not in [%d, %d]", node, node, node.Pos(), pgf.Tok.Base(), pgf.Tok.Base()+pgf.Tok.Size())
+ }
+ if n, ok := node.(T); ok {
+ visited = true
+ checkFn(n)
+ }
+ }
+ return true
+ })
+ if !visited {
+ var n T
+ t.Fatalf("got no %s node but want at least one", reflect.TypeOf(n))
+ }
+}
+
+// ensureSource helps to parse src into an ast.File by go/parser and trigger checkFn if the type matches T.
+func ensureSource[T ast.Node](t *testing.T, src []byte, checkFn func(n T)) {
+ // tolerate error as usually the src is problematic.
+ originFile, _ := parser.ParseFile(token.NewFileSet(), "file://foo.go", src, parsego.Full)
+ var visited bool
+ ast.Inspect(originFile, func(node ast.Node) bool {
+ if n, ok := node.(T); ok {
+ visited = true
+ checkFn(n)
+ }
+ return true
+ })
+
+ if !visited {
+ var n T
+ t.Fatalf("got no %s node but want at least one", reflect.TypeOf(n))
+ }
+}
+
+func filesrc(expressions string) []byte {
+ const srcTmpl = `package foo
+
+func _() {
+ %s
+}`
+ return fmt.Appendf(nil, srcTmpl, expressions)
+}
diff --git a/gopls/internal/cache/port.go b/gopls/internal/cache/port.go
index 40005bcf6d4..8caaa801b68 100644
--- a/gopls/internal/cache/port.go
+++ b/gopls/internal/cache/port.go
@@ -7,6 +7,7 @@ package cache
import (
"bytes"
"go/build"
+ "go/build/constraint"
"go/parser"
"go/token"
"io"
@@ -173,12 +174,16 @@ func (p port) matches(path string, content []byte) bool {
// without trimming content.
func trimContentForPortMatch(content []byte) []byte {
buildComment := buildComment(content)
- return []byte(buildComment + "\npackage p") // package name does not matter
+ // The package name does not matter, but +build lines
+ // require a blank line before the package declaration.
+ return []byte(buildComment + "\n\npackage p")
}
// buildComment returns the first matching //go:build comment in the given
// content, or "" if none exists.
func buildComment(content []byte) string {
+ var lines []string
+
f, err := parser.ParseFile(token.NewFileSet(), "", content, parser.PackageClauseOnly|parser.ParseComments)
if err != nil {
return ""
@@ -186,24 +191,15 @@ func buildComment(content []byte) string {
for _, cg := range f.Comments {
for _, c := range cg.List {
- if isGoBuildComment(c.Text) {
+ if constraint.IsGoBuild(c.Text) {
+ // A file must have only one //go:build line.
return c.Text
}
+ if constraint.IsPlusBuild(c.Text) {
+ // A file may have several // +build lines.
+ lines = append(lines, c.Text)
+ }
}
}
- return ""
-}
-
-// Adapted from go/build/build.go.
-//
-// TODO(rfindley): use constraint.IsGoBuild once we are on 1.19+.
-func isGoBuildComment(line string) bool {
- const goBuildComment = "//go:build"
- if !strings.HasPrefix(line, goBuildComment) {
- return false
- }
- // Report whether //go:build is followed by a word boundary.
- line = strings.TrimSpace(line)
- rest := line[len(goBuildComment):]
- return len(rest) == 0 || len(strings.TrimSpace(rest)) < len(rest)
+ return strings.Join(lines, "\n")
}
diff --git a/gopls/internal/cache/port_test.go b/gopls/internal/cache/port_test.go
index a92056a9c22..5d0c5d4a50f 100644
--- a/gopls/internal/cache/port_test.go
+++ b/gopls/internal/cache/port_test.go
@@ -46,7 +46,6 @@ func TestMatchingPortsStdlib(t *testing.T) {
var g errgroup.Group
packages.Visit(pkgs, nil, func(pkg *packages.Package) {
for _, f := range pkg.CompiledGoFiles {
- f := f
g.Go(func() error {
content, err := os.ReadFile(f)
// We report errors via t.Error, not by returning,
@@ -118,7 +117,7 @@ func getFileID(filename string) (FileID, time.Time, error) {
}
`
fh := makeFakeFileHandle("file:///path/to/test/file.go", []byte(src))
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
_ = matchingPreferredPorts(b, fh, true)
}
}
diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go
index a7fb618f679..f0d8f062138 100644
--- a/gopls/internal/cache/session.go
+++ b/gopls/internal/cache/session.go
@@ -139,11 +139,18 @@ func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, *Snapshot
}
view, snapshot, release := s.createView(ctx, def)
s.views = append(s.views, view)
- // we always need to drop the view map
- s.viewMap = make(map[protocol.DocumentURI]*View)
+ s.viewMap[protocol.Clean(folder.Dir)] = view
return view, snapshot, release, nil
}
+// HasView checks whether the uri's view exists.
+func (s *Session) HasView(uri protocol.DocumentURI) bool {
+ s.viewMu.Lock()
+ defer s.viewMu.Unlock()
+ _, ok := s.viewMap[protocol.Clean(uri)]
+ return ok
+}
+
// createView creates a new view, with an initial snapshot that retains the
// supplied context, detached from events and cancelation.
//
@@ -169,14 +176,14 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
// Compute a prefix match, respecting segment boundaries, by ensuring
// the pattern (dir) has a trailing slash.
dirPrefix := strings.TrimSuffix(string(def.folder.Dir), "/") + "/"
- filterer := NewFilterer(def.folder.Options.DirectoryFilters)
+ pathIncluded := PathIncludeFunc(def.folder.Options.DirectoryFilters)
skipPath = func(dir string) bool {
uri := strings.TrimSuffix(string(protocol.URIFromPath(dir)), "/")
// Note that the logic below doesn't handle the case where uri ==
// v.folder.Dir, because there is no point in excluding the entire
// workspace folder!
- if rel := strings.TrimPrefix(uri, dirPrefix); rel != uri {
- return filterer.Disallow(rel)
+ if rel, ok := strings.CutPrefix(uri, dirPrefix); ok {
+ return !pathIncluded(rel)
}
return false
}
@@ -238,7 +245,12 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
viewDefinition: def,
importsState: newImportsState(backgroundCtx, s.cache.modCache, pe),
}
- if def.folder.Options.ImportsSource != settings.ImportsSourceOff {
+
+ // Keep this in sync with golang.computeImportEdits.
+ //
+ // TODO(rfindley): encapsulate the imports state logic so that the handling
+ // for Options.ImportsSource is in a single location.
+ if def.folder.Options.ImportsSource == settings.ImportsSourceGopls {
v.modcacheState = newModcacheState(def.folder.Env.GOMODCACHE)
}
@@ -384,7 +396,7 @@ func (s *Session) SnapshotOf(ctx context.Context, uri protocol.DocumentURI) (*Sn
// View is shut down. Forget this association.
s.viewMu.Lock()
if s.viewMap[uri] == v {
- delete(s.viewMap, uri)
+ delete(s.viewMap, protocol.Clean(uri))
}
s.viewMu.Unlock()
}
@@ -473,7 +485,7 @@ func (s *Session) viewOfLocked(ctx context.Context, uri protocol.DocumentURI) (*
// (as in golang/go#60776).
v = relevantViews[0]
}
- s.viewMap[uri] = v // may be nil
+ s.viewMap[protocol.Clean(uri)] = v // may be nil
}
return v, nil
}
@@ -1079,6 +1091,7 @@ type brokenFile struct {
err error
}
+func (b brokenFile) String() string { return b.uri.Path() }
func (b brokenFile) URI() protocol.DocumentURI { return b.uri }
func (b brokenFile) Identity() file.Identity { return file.Identity{URI: b.uri} }
func (b brokenFile) SameContentsOnDisk() bool { return false }
diff --git a/gopls/internal/cache/session_test.go b/gopls/internal/cache/session_test.go
index 5f9a59a4945..1b7472af605 100644
--- a/gopls/internal/cache/session_test.go
+++ b/gopls/internal/cache/session_test.go
@@ -337,7 +337,8 @@ replace (
for _, f := range test.folders {
opts := settings.DefaultOptions()
if f.options != nil {
- for _, err := range opts.Set(f.options(dir)) {
+ _, errs := opts.Set(f.options(dir))
+ for _, err := range errs {
t.Fatal(err)
}
}
diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go
index c341ac6e85a..f936bbfc458 100644
--- a/gopls/internal/cache/snapshot.go
+++ b/gopls/internal/cache/snapshot.go
@@ -323,6 +323,8 @@ func fileKind(fh file.Handle) file.Kind {
return file.Sum
case ".work":
return file.Work
+ case ".s":
+ return file.Asm
}
return file.UnknownKind
}
@@ -606,7 +608,7 @@ func (s *Snapshot) MethodSets(ctx context.Context, ids ...PackageID) ([]*methods
pre := func(i int, ph *packageHandle) bool {
data, err := filecache.Get(methodSetsKind, ph.key)
if err == nil { // hit
- indexes[i] = methodsets.Decode(data)
+ indexes[i] = methodsets.Decode(ph.mp.PkgPath, data)
return false
} else if err != filecache.ErrNotFound {
event.Error(ctx, "reading methodsets from filecache", err)
@@ -645,6 +647,21 @@ func (s *Snapshot) Tests(ctx context.Context, ids ...PackageID) ([]*testfuncs.In
return indexes, s.forEachPackage(ctx, ids, pre, post)
}
+// NarrowestMetadataForFile returns metadata for the narrowest package
+// (the one with the fewest files) that encloses the specified file.
+// The result may be a test variant, but never an intermediate test variant.
+func (snapshot *Snapshot) NarrowestMetadataForFile(ctx context.Context, uri protocol.DocumentURI) (*metadata.Package, error) {
+ mps, err := snapshot.MetadataForFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ metadata.RemoveIntermediateTestVariants(&mps)
+ if len(mps) == 0 {
+ return nil, fmt.Errorf("no package metadata for file %s", uri)
+ }
+ return mps[0], nil
+}
+
// MetadataForFile returns a new slice containing metadata for each
// package containing the Go file identified by uri, ordered by the
// number of CompiledGoFiles (i.e. "narrowest" to "widest" package),
@@ -652,6 +669,10 @@ func (s *Snapshot) Tests(ctx context.Context, ids ...PackageID) ([]*testfuncs.In
// The result may include tests and intermediate test variants of
// importable packages.
// It returns an error if the context was cancelled.
+//
+// TODO(adonovan): in nearly all cases the caller must use
+// [metadata.RemoveIntermediateTestVariants]. Make this a parameter to
+// force the caller to consider it (and reduce code).
func (s *Snapshot) MetadataForFile(ctx context.Context, uri protocol.DocumentURI) ([]*metadata.Package, error) {
if s.view.typ == AdHocView {
// As described in golang/go#57209, in ad-hoc workspaces (where we load ./
@@ -895,10 +916,12 @@ func (s *Snapshot) watchSubdirs() bool {
// requirements that client names do not change. We should update the VS
// Code extension to set a default value of "subdirWatchPatterns" to "on",
// so that this workaround is only temporary.
- if s.Options().ClientInfo.Name == "Visual Studio Code" {
+ switch s.Options().ClientInfo.Name {
+ case "Visual Studio Code", "Visual Studio Code - Insiders":
return true
+ default:
+ return false
}
- return false
default:
bug.Reportf("invalid subdirWatchPatterns: %q", p)
return false
@@ -1301,7 +1324,7 @@ searchOverlays:
// where the file is inside a workspace module, but perhaps no packages
// were loaded for that module.
_, loadedMod := loadedModFiles[goMod]
- _, workspaceMod := s.view.viewDefinition.workspaceModFiles[goMod]
+ _, workspaceMod := s.view.workspaceModFiles[goMod]
// If we have a relevant go.mod file, check whether the file is orphaned
// due to its go.mod file being inactive. We could also offer a
// prescriptive diagnostic in the case that there is no go.mod file, but
diff --git a/gopls/internal/cache/source.go b/gopls/internal/cache/source.go
index 3e21c641651..047cc3971d8 100644
--- a/gopls/internal/cache/source.go
+++ b/gopls/internal/cache/source.go
@@ -61,9 +61,7 @@ func (s *goplsSource) ResolveReferences(ctx context.Context, filename string, mi
// collect the ones that are still
needed := maps.Clone(missing)
for _, a := range fromWS {
- if _, ok := needed[a.Package.Name]; ok {
- delete(needed, a.Package.Name)
- }
+ delete(needed, a.Package.Name)
}
// when debug (below) is gone, change this to: if len(needed) == 0 {return fromWS, nil}
var fromCache []*result
@@ -105,7 +103,7 @@ func (s *goplsSource) ResolveReferences(ctx context.Context, filename string, mi
}
dbgpr := func(hdr string, v []*imports.Result) {
- for i := 0; i < len(v); i++ {
+ for i := range v {
log.Printf("%s%d %+v %+v", hdr, i, v[i].Import, v[i].Package)
}
}
@@ -184,10 +182,13 @@ type found struct {
func (s *goplsSource) resolveWorkspaceReferences(filename string, missing imports.References) ([]*imports.Result, error) {
uri := protocol.URIFromPath(filename)
mypkgs, err := s.S.MetadataForFile(s.ctx, uri)
- if len(mypkgs) != 1 {
- // what does this mean? can it happen?
+ if err != nil {
+ return nil, err
+ }
+ if len(mypkgs) == 0 {
+ return nil, nil
}
- mypkg := mypkgs[0]
+ mypkg := mypkgs[0] // narrowest package
// search the metadata graph for package ids correstponding to missing
g := s.S.MetadataGraph()
var ids []metadata.PackageID
@@ -211,7 +212,7 @@ func (s *goplsSource) resolveWorkspaceReferences(filename string, missing import
// keep track of used syms and found results by package name
// TODO: avoid import cycles (is current package in forward closure)
founds := make(map[string][]found)
- for i := 0; i < len(ids); i++ {
+ for i := range len(ids) {
nm := string(pkgs[i].Name)
if satisfies(syms[i], missing[nm]) {
got := &imports.Result{
diff --git a/gopls/internal/cache/testfuncs/tests.go b/gopls/internal/cache/testfuncs/tests.go
index 1182795b37b..e0e3ce1beca 100644
--- a/gopls/internal/cache/testfuncs/tests.go
+++ b/gopls/internal/cache/testfuncs/tests.go
@@ -57,6 +57,7 @@ func NewIndex(files []*parsego.File, info *types.Info) *Index {
b := &indexBuilder{
fileIndex: make(map[protocol.DocumentURI]int),
subNames: make(map[string]int),
+ visited: make(map[*types.Func]bool),
}
return b.build(files, info)
}
@@ -101,6 +102,7 @@ func (b *indexBuilder) build(files []*parsego.File, info *types.Info) *Index {
}
b.Files[i].Tests = append(b.Files[i].Tests, t)
+ b.visited[obj] = true
// Check for subtests
if isTest {
@@ -168,27 +170,48 @@ func (b *indexBuilder) findSubtests(parent gobTest, typ *ast.FuncType, body *ast
t.Location.Range, _ = file.NodeRange(call)
tests = append(tests, t)
- if typ, body := findFunc(files, info, body, call.Args[1]); typ != nil {
+ fn, typ, body := findFunc(files, info, body, call.Args[1])
+ if typ == nil {
+ continue
+ }
+
+ // Function literals don't have an associated object
+ if fn == nil {
tests = append(tests, b.findSubtests(t, typ, body, file, files, info)...)
+ continue
+ }
+
+ // Never recurse if the second argument is a top-level test function
+ if isTest, _ := isTestOrExample(fn); isTest {
+ continue
+ }
+
+ // Don't recurse into functions that have already been visited
+ if b.visited[fn] {
+ continue
}
+
+ b.visited[fn] = true
+ tests = append(tests, b.findSubtests(t, typ, body, file, files, info)...)
}
return tests
}
// findFunc finds the type and body of the given expr, which may be a function
-// literal or reference to a declared function.
-//
-// If no function is found, findFunc returns (nil, nil).
-func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr ast.Expr) (*ast.FuncType, *ast.BlockStmt) {
+// literal or reference to a declared function. If the expression is a declared
+// function, findFunc returns its [types.Func]. If the expression is a function
+// literal, findFunc returns nil for the first return value. If no function is
+// found, findFunc returns (nil, nil, nil).
+func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr ast.Expr) (*types.Func, *ast.FuncType, *ast.BlockStmt) {
var obj types.Object
switch arg := expr.(type) {
case *ast.FuncLit:
- return arg.Type, arg.Body
+ return nil, arg.Type, arg.Body
case *ast.Ident:
obj = info.ObjectOf(arg)
if obj == nil {
- return nil, nil
+ return nil, nil, nil
}
case *ast.SelectorExpr:
@@ -198,12 +221,12 @@ func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr
// complex. However, those cases should be rare.
sel, ok := info.Selections[arg]
if !ok {
- return nil, nil
+ return nil, nil, nil
}
obj = sel.Obj()
default:
- return nil, nil
+ return nil, nil, nil
}
if v, ok := obj.(*types.Var); ok {
@@ -211,7 +234,7 @@ func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr
// the file), but that doesn't account for assignment. If the variable
// is assigned multiple times, we could easily get the wrong one.
_, _ = v, body
- return nil, nil
+ return nil, nil, nil
}
for _, file := range files {
@@ -228,11 +251,11 @@ func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr
}
if info.ObjectOf(decl.Name) == obj {
- return decl.Type, decl.Body
+ return obj.(*types.Func), decl.Type, decl.Body
}
}
}
- return nil, nil
+ return nil, nil, nil
}
// isTestOrExample reports whether the given func is a testing func or an
@@ -308,6 +331,7 @@ type indexBuilder struct {
gobPackage
fileIndex map[protocol.DocumentURI]int
subNames map[string]int
+ visited map[*types.Func]bool
}
// -- serial format of index --
diff --git a/gopls/internal/cache/typerefs/packageset.go b/gopls/internal/cache/typerefs/packageset.go
index f4f7c94f712..af495d1573c 100644
--- a/gopls/internal/cache/typerefs/packageset.go
+++ b/gopls/internal/cache/typerefs/packageset.go
@@ -124,7 +124,7 @@ func (s *PackageSet) Contains(id metadata.PackageID) bool {
// Elems calls f for each element of the set in ascending order.
func (s *PackageSet) Elems(f func(IndexID)) {
for i, v := range moremaps.Sorted(s.sparse) {
- for b := 0; b < blockSize; b++ {
+ for b := range blockSize {
if (v & (1 << b)) != 0 {
f(IndexID(i*blockSize + b))
}
diff --git a/gopls/internal/cache/typerefs/pkggraph_test.go b/gopls/internal/cache/typerefs/pkggraph_test.go
index 20e34ce1aa9..f205da85b35 100644
--- a/gopls/internal/cache/typerefs/pkggraph_test.go
+++ b/gopls/internal/cache/typerefs/pkggraph_test.go
@@ -84,7 +84,6 @@ func BuildPackageGraph(ctx context.Context, meta metadata.Source, ids []metadata
var eg errgroup.Group
eg.SetLimit(workers)
for _, id := range ids {
- id := id
eg.Go(func() error {
_, err := g.Package(ctx, id)
return err
diff --git a/gopls/internal/cache/typerefs/pkgrefs_test.go b/gopls/internal/cache/typerefs/pkgrefs_test.go
index 3f9a976ccf7..ce297e4380b 100644
--- a/gopls/internal/cache/typerefs/pkgrefs_test.go
+++ b/gopls/internal/cache/typerefs/pkgrefs_test.go
@@ -12,7 +12,7 @@ import (
"go/token"
"go/types"
"os"
- "sort"
+ "slices"
"strings"
"sync"
"testing"
@@ -81,9 +81,7 @@ func TestBuildPackageGraph(t *testing.T) {
for id := range exports {
ids = append(ids, id)
}
- sort.Slice(ids, func(i, j int) bool {
- return ids[i] < ids[j]
- })
+ slices.Sort(ids)
t0 = time.Now()
g, err := BuildPackageGraph(ctx, meta, ids, newParser().parse)
@@ -259,9 +257,8 @@ func BenchmarkBuildPackageGraph(b *testing.B) {
for id := range exports {
ids = append(ids, id)
}
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
_, err := BuildPackageGraph(ctx, meta, ids, newParser().parse)
if err != nil {
b.Fatal(err)
diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go
index 26f0de86125..4e8375a77db 100644
--- a/gopls/internal/cache/view.go
+++ b/gopls/internal/cache/view.go
@@ -20,7 +20,6 @@ import (
"os/exec"
"path"
"path/filepath"
- "regexp"
"slices"
"sort"
"strings"
@@ -109,7 +108,10 @@ type View struct {
// importsState is for the old imports code
importsState *importsState
- // maintain the current module cache index
+ // modcacheState is the replacement for importsState, to be used for
+ // goimports operations when the imports source is "gopls".
+ //
+ // It may be nil, if the imports source is not "gopls".
modcacheState *modcacheState
// pkgIndex is an index of package IDs, for efficient storage of typerefs.
@@ -471,15 +473,15 @@ func (v *View) filterFunc() func(protocol.DocumentURI) bool {
gomodcache := v.folder.Env.GOMODCACHE
var filters []string
filters = append(filters, v.folder.Options.DirectoryFilters...)
- if pref := strings.TrimPrefix(gomodcache, folderDir); pref != gomodcache {
+ if pref, ok := strings.CutPrefix(gomodcache, folderDir); ok {
modcacheFilter := "-" + strings.TrimPrefix(filepath.ToSlash(pref), "/")
filters = append(filters, modcacheFilter)
}
- filterer := NewFilterer(filters)
+ pathIncluded := PathIncludeFunc(filters)
v._filterFunc = func(uri protocol.DocumentURI) bool {
// Only filter relative to the configured root directory.
if pathutil.InDir(folderDir, uri.Path()) {
- return relPathExcludedByFilter(strings.TrimPrefix(uri.Path(), folderDir), filterer)
+ return relPathExcludedByFilter(strings.TrimPrefix(uri.Path(), folderDir), pathIncluded)
}
return false
}
@@ -492,7 +494,9 @@ func (v *View) shutdown() {
// Cancel the initial workspace load if it is still running.
v.cancelInitialWorkspaceLoad()
v.importsState.stopTimer()
- v.modcacheState.stopTimer()
+ if v.modcacheState != nil {
+ v.modcacheState.stopTimer()
+ }
v.snapshotMu.Lock()
if v.snapshot != nil {
@@ -546,7 +550,7 @@ func newIgnoreFilter(dirs []string) *ignoreFilter {
func (f *ignoreFilter) ignored(filename string) bool {
for _, prefix := range f.prefixes {
- if suffix := strings.TrimPrefix(filename, prefix); suffix != filename {
+ if suffix, ok := strings.CutPrefix(filename, prefix); ok {
if checkIgnored(suffix) {
return true
}
@@ -563,7 +567,7 @@ func (f *ignoreFilter) ignored(filename string) bool {
func checkIgnored(suffix string) bool {
// Note: this could be further optimized by writing a HasSegment helper, a
// segment-boundary respecting variant of strings.Contains.
- for _, component := range strings.Split(suffix, string(filepath.Separator)) {
+ for component := range strings.SplitSeq(suffix, string(filepath.Separator)) {
if len(component) == 0 {
continue
}
@@ -1248,8 +1252,6 @@ func globsMatchPath(globs, target string) bool {
return false
}
-var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`)
-
// TODO(rfindley): clean up the redundancy of allFilesExcluded,
// pathExcludedByFilterFunc, pathExcludedByFilter, view.filterFunc...
func allFilesExcluded(files []string, filterFunc func(protocol.DocumentURI) bool) bool {
@@ -1262,7 +1264,7 @@ func allFilesExcluded(files []string, filterFunc func(protocol.DocumentURI) bool
return true
}
-func relPathExcludedByFilter(path string, filterer *Filterer) bool {
+func relPathExcludedByFilter(path string, pathIncluded func(string) bool) bool {
path = strings.TrimPrefix(filepath.ToSlash(path), "/")
- return filterer.Disallow(path)
+ return !pathIncluded(path)
}
diff --git a/gopls/internal/cache/view_test.go b/gopls/internal/cache/view_test.go
index 992a3d61828..46000191e42 100644
--- a/gopls/internal/cache/view_test.go
+++ b/gopls/internal/cache/view_test.go
@@ -90,14 +90,14 @@ func TestFilters(t *testing.T) {
}
for _, tt := range tests {
- filterer := NewFilterer(tt.filters)
+ pathIncluded := PathIncludeFunc(tt.filters)
for _, inc := range tt.included {
- if relPathExcludedByFilter(inc, filterer) {
+ if relPathExcludedByFilter(inc, pathIncluded) {
t.Errorf("filters %q excluded %v, wanted included", tt.filters, inc)
}
}
for _, exc := range tt.excluded {
- if !relPathExcludedByFilter(exc, filterer) {
+ if !relPathExcludedByFilter(exc, pathIncluded) {
t.Errorf("filters %q included %v, wanted excluded", tt.filters, exc)
}
}
diff --git a/gopls/internal/cache/xrefs/xrefs.go b/gopls/internal/cache/xrefs/xrefs.go
index 2115322bfdc..d9b7051737a 100644
--- a/gopls/internal/cache/xrefs/xrefs.go
+++ b/gopls/internal/cache/xrefs/xrefs.go
@@ -44,8 +44,8 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte {
objectpathFor := new(objectpath.Encoder).For
for fileIndex, pgf := range files {
- ast.Inspect(pgf.File, func(n ast.Node) bool {
- switch n := n.(type) {
+ for cur := range pgf.Cursor.Preorder((*ast.Ident)(nil), (*ast.ImportSpec)(nil)) {
+ switch n := cur.Node().(type) {
case *ast.Ident:
// Report a reference for each identifier that
// uses a symbol exported from another package.
@@ -68,7 +68,7 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte {
if err != nil {
// Capitalized but not exported
// (e.g. local const/var/type).
- return true
+ continue
}
gobObj = &gobObject{Path: path}
objects[obj] = gobObj
@@ -91,7 +91,7 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte {
// string to the imported package.
pkgname := info.PkgNameOf(n)
if pkgname == nil {
- return true // missing import
+ continue // missing import
}
objects := getObjects(pkgname.Imported())
gobObj, ok := objects[nil]
@@ -109,8 +109,7 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte {
bug.Reportf("out of bounds import spec %+v", n.Path)
}
}
- return true
- })
+ }
}
// Flatten the maps into slices, and sort for determinism.
diff --git a/gopls/internal/clonetest/clonetest.go b/gopls/internal/clonetest/clonetest.go
index 3542476ae09..773bc170fe7 100644
--- a/gopls/internal/clonetest/clonetest.go
+++ b/gopls/internal/clonetest/clonetest.go
@@ -13,6 +13,7 @@ package clonetest
import (
"fmt"
"reflect"
+ "slices"
)
// NonZero returns a T set to some appropriate nonzero value:
@@ -36,11 +37,9 @@ func NonZero[T any]() T {
// nonZeroValue returns a non-zero, addressable value of the given type.
func nonZeroValue(t reflect.Type, seen []reflect.Type) reflect.Value {
- for _, t2 := range seen {
- if t == t2 {
- // Cycle: return the zero value.
- return reflect.Zero(t)
- }
+ if slices.Contains(seen, t) {
+ // Cycle: return the zero value.
+ return reflect.Zero(t)
}
seen = append(seen, t)
v := reflect.New(t).Elem()
diff --git a/gopls/internal/cmd/check.go b/gopls/internal/cmd/check.go
index d256fa9de2a..8c0362b148a 100644
--- a/gopls/internal/cmd/check.go
+++ b/gopls/internal/cmd/check.go
@@ -16,7 +16,8 @@ import (
// check implements the check verb for gopls.
type check struct {
- app *Application
+ app *Application
+ Severity string `flag:"severity" help:"minimum diagnostic severity (hint, info, warning, or error)"`
}
func (c *check) Name() string { return "check" }
@@ -35,6 +36,20 @@ Example: show the diagnostic results of this file:
// Run performs the check on the files specified by args and prints the
// results to stdout.
func (c *check) Run(ctx context.Context, args ...string) error {
+ severityCutoff := protocol.SeverityWarning
+ switch c.Severity {
+ case "hint":
+ severityCutoff = protocol.SeverityHint
+ case "info":
+ severityCutoff = protocol.SeverityInformation
+ case "warning":
+ // default
+ case "error":
+ severityCutoff = protocol.SeverityError
+ default:
+ return fmt.Errorf("unrecognized -severity value %q", c.Severity)
+ }
+
if len(args) == 0 {
return nil
}
@@ -95,6 +110,9 @@ func (c *check) Run(ctx context.Context, args ...string) error {
file.diagnosticsMu.Unlock()
for _, diag := range diags {
+ if diag.Severity > severityCutoff { // lower severity value => greater severity, counterintuitively
+ continue
+ }
if err := print(file.uri, diag.Range, diag.Message); err != nil {
return err
}
diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go
index a647b3198df..fed96388fb4 100644
--- a/gopls/internal/cmd/cmd.go
+++ b/gopls/internal/cmd/cmd.go
@@ -63,9 +63,6 @@ type Application struct {
// VeryVerbose enables a higher level of verbosity in logging output.
VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"`
- // Control ocagent export of telemetry
- OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"`
-
// PrepareOptions is called to update the options when a new view is built.
// It is primarily to allow the behavior of gopls to be modified by hooks.
PrepareOptions func(*settings.Options)
@@ -98,8 +95,6 @@ func (app *Application) verbose() bool {
// New returns a new Application ready to run.
func New() *Application {
app := &Application{
- OCAgent: "off", //TODO: Remove this line to default the exporter to on
-
Serve: Serve{
RemoteListenTimeout: 1 * time.Minute,
},
@@ -238,7 +233,7 @@ func (app *Application) Run(ctx context.Context, args ...string) error {
// executable, and immediately runs a gc.
filecache.Start()
- ctx = debug.WithInstance(ctx, app.OCAgent)
+ ctx = debug.WithInstance(ctx)
if len(args) == 0 {
s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
return tool.Run(ctx, s, &app.Serve, args)
@@ -284,7 +279,7 @@ func (app *Application) internalCommands() []tool.Application {
func (app *Application) featureCommands() []tool.Application {
return []tool.Application{
&callHierarchy{app: app},
- &check{app: app},
+ &check{app: app, Severity: "warning"},
&codeaction{app: app},
&codelens{app: app},
&definition{app: app},
@@ -310,11 +305,6 @@ func (app *Application) featureCommands() []tool.Application {
}
}
-var (
- internalMu sync.Mutex
- internalConnections = make(map[string]*connection)
-)
-
// connect creates and initializes a new in-process gopls session.
func (app *Application) connect(ctx context.Context) (*connection, error) {
client := newClient(app)
@@ -353,7 +343,8 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti
// Make sure to respect configured options when sending initialize request.
opts := settings.DefaultOptions(options)
- // If you add an additional option here, you must update the map key in connect.
+ // If you add an additional option here,
+ // you must update the map key of settings.DefaultOptions called in (*Application).connect.
params.Capabilities.TextDocument.Hover = &protocol.HoverClientCapabilities{
ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
}
@@ -361,7 +352,7 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti
params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{}
params.Capabilities.TextDocument.SemanticTokens.Formats = []protocol.TokenFormat{"relative"}
params.Capabilities.TextDocument.SemanticTokens.Requests.Range = &protocol.Or_ClientSemanticTokensRequestOptions_range{Value: true}
- //params.Capabilities.TextDocument.SemanticTokens.Requests.Range.Value = true
+ // params.Capabilities.TextDocument.SemanticTokens.Requests.Range.Value = true
params.Capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true}
params.Capabilities.TextDocument.SemanticTokens.TokenTypes = moreslices.ConvertStrings[string](semtok.TokenTypes)
params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = moreslices.ConvertStrings[string](semtok.TokenModifiers)
@@ -373,14 +364,17 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti
},
}
params.Capabilities.Window.WorkDoneProgress = true
+ params.Capabilities.Workspace.FileOperations = &protocol.FileOperationClientCapabilities{
+ DidCreate: true,
+ }
- params.InitializationOptions = map[string]interface{}{
+ params.InitializationOptions = map[string]any{
"symbolMatcher": string(opts.SymbolMatcher),
}
- if c.initializeResult, err = c.Server.Initialize(ctx, params); err != nil {
+ if c.initializeResult, err = c.Initialize(ctx, params); err != nil {
return err
}
- if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
+ if err := c.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
return err
}
return nil
@@ -473,7 +467,7 @@ func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams
return nil
}
-func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil }
+func (c *cmdClient) Event(ctx context.Context, t *any) error { return nil }
func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error {
return nil
@@ -487,13 +481,13 @@ func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceF
return nil, nil
}
-func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
- results := make([]interface{}, len(p.Items))
+func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]any, error) {
+ results := make([]any, len(p.Items))
for i, item := range p.Items {
if item.Section != "gopls" {
continue
}
- m := map[string]interface{}{
+ m := map[string]any{
"analyses": map[string]any{
"fillreturns": true,
"nonewvars": true,
@@ -663,7 +657,7 @@ func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishD
// TODO(golang/go#60122): replace the gopls.diagnose_files
// command with support for textDocument/diagnostic,
// so that we don't need to do this de-duplication.
- type key [6]interface{}
+ type key [6]any
seen := make(map[key]bool)
out := file.diagnostics[:0]
for _, d := range file.diagnostics {
@@ -778,10 +772,25 @@ func (c *connection) openFile(ctx context.Context, uri protocol.DocumentURI) (*c
return nil, file.err
}
+ // Choose language ID from file extension.
+ var langID protocol.LanguageKind // "" eventually maps to file.UnknownKind
+ switch filepath.Ext(uri.Path()) {
+ case ".go":
+ langID = "go"
+ case ".mod":
+ langID = "go.mod"
+ case ".sum":
+ langID = "go.sum"
+ case ".work":
+ langID = "go.work"
+ case ".s":
+ langID = "go.s"
+ }
+
p := &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{
URI: uri,
- LanguageID: "go",
+ LanguageID: langID,
Version: 1,
Text: string(file.mapper.Content),
},
@@ -812,10 +821,10 @@ func (c *connection) diagnoseFiles(ctx context.Context, files []protocol.Documen
}
func (c *connection) terminate(ctx context.Context) {
- //TODO: do we need to handle errors on these calls?
+ // TODO: do we need to handle errors on these calls?
c.Shutdown(ctx)
- //TODO: right now calling exit terminates the process, we should rethink that
- //server.Exit(ctx)
+ // TODO: right now calling exit terminates the process, we should rethink that
+ // server.Exit(ctx)
}
// Implement io.Closer.
diff --git a/gopls/internal/cmd/codeaction.go b/gopls/internal/cmd/codeaction.go
index 2096a153681..6931af37d40 100644
--- a/gopls/internal/cmd/codeaction.go
+++ b/gopls/internal/cmd/codeaction.go
@@ -142,7 +142,7 @@ func (cmd *codeaction) Run(ctx context.Context, args ...string) error {
// Request code actions of the desired kinds.
var kinds []protocol.CodeActionKind
if cmd.Kind != "" {
- for _, kind := range strings.Split(cmd.Kind, ",") {
+ for kind := range strings.SplitSeq(cmd.Kind, ",") {
kinds = append(kinds, protocol.CodeActionKind(kind))
}
} else {
diff --git a/gopls/internal/cmd/definition.go b/gopls/internal/cmd/definition.go
index d9cd98554e3..71e8b1511bd 100644
--- a/gopls/internal/cmd/definition.go
+++ b/gopls/internal/cmd/definition.go
@@ -96,7 +96,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
}
if len(locs) == 0 {
- return fmt.Errorf("%v: not an identifier", from)
+ return fmt.Errorf("%v: no definition location (not an identifier?)", from)
}
file, err = conn.openFile(ctx, locs[0].URI)
if err != nil {
diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go
index 42812a870a4..6e4b450635b 100644
--- a/gopls/internal/cmd/integration_test.go
+++ b/gopls/internal/cmd/integration_test.go
@@ -108,6 +108,12 @@ var C int
-- c/c2.go --
package c
var C int
+-- d/d.go --
+package d
+
+import "io/ioutil"
+
+var _ = ioutil.ReadFile
`)
// no files
@@ -141,6 +147,22 @@ var C int
res.checkStdout(`c2.go:2:5-6: C redeclared in this block`)
res.checkStdout(`c.go:2:5-6: - other declaration of C`)
}
+
+ // No deprecated (hint) diagnostic without -severity.
+ {
+ res := gopls(t, tree, "check", "./d/d.go")
+ res.checkExit(true)
+ if len(res.stdout) > 0 {
+ t.Errorf("check ./d/d.go returned unexpected output:\n%s", res.stdout)
+ }
+ }
+
+ // Deprecated (hint) diagnostics with -severity=hint
+ {
+ res := gopls(t, tree, "check", "-severity=hint", "./d/d.go")
+ res.checkExit(true)
+ res.checkStdout(`ioutil.ReadFile is deprecated`)
+ }
}
// TestCallHierarchy tests the 'call_hierarchy' subcommand (call_hierarchy.go).
@@ -486,6 +508,14 @@ func f() {
func TestImplementations(t *testing.T) {
t.Parallel()
+ // types.CheckExpr, now used in the rangeint modernizer, had a
+ // data race (#71817) that was fixed in go1.25 and backported
+ // to go1.24 but not to go1.23. Although in principle it could
+ // affect a lot of tests, it (weirdly) only seems to show up
+ // in this one (#72082). Rather than backport again, we
+ // suppress this test.
+ testenv.NeedsGo1Point(t, 24)
+
tree := writeTree(t, `
-- a.go --
package a
@@ -930,7 +960,7 @@ package foo
res3 := goplsWithEnv(t, tree, []string{"GOPACKAGESDRIVER=off"}, "stats", "-anon")
res3.checkExit(true)
- var statsAsMap3 map[string]interface{}
+ var statsAsMap3 map[string]any
if err := json.Unmarshal([]byte(res3.stdout), &statsAsMap3); err != nil {
t.Fatalf("failed to unmarshal JSON output of stats command: %v", err)
}
@@ -980,9 +1010,9 @@ type C struct{}
res := gopls(t, tree, "codeaction", "-title=Browse.*doc", "a/a.go")
res.checkExit(true)
got := res.stdout
- want := `command "Browse gopls feature documentation" [gopls.doc.features]` +
+ want := `command "Browse documentation for package a" [source.doc]` +
"\n" +
- `command "Browse documentation for package a" [source.doc]` +
+ `command "Browse gopls feature documentation" [gopls.doc.features]` +
"\n"
if got != want {
t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr)
@@ -1212,7 +1242,7 @@ func (res *result) checkOutput(pattern, name, content string) {
}
// toJSON decodes res.stdout as JSON into to *ptr and reports its success.
-func (res *result) toJSON(ptr interface{}) bool {
+func (res *result) toJSON(ptr any) bool {
if err := json.Unmarshal([]byte(res.stdout), ptr); err != nil {
res.t.Errorf("invalid JSON %v", err)
return false
diff --git a/gopls/internal/cmd/stats.go b/gopls/internal/cmd/stats.go
index cc19a94fb84..1ba43ccee83 100644
--- a/gopls/internal/cmd/stats.go
+++ b/gopls/internal/cmd/stats.go
@@ -164,7 +164,7 @@ func (s *stats) Run(ctx context.Context, args ...string) error {
}
// Filter JSON output to fields that are consistent with s.Anon.
- okFields := make(map[string]interface{})
+ okFields := make(map[string]any)
{
v := reflect.ValueOf(stats)
t := v.Type()
diff --git a/gopls/internal/cmd/symbols.go b/gopls/internal/cmd/symbols.go
index 663a08f4be1..15c593b0e74 100644
--- a/gopls/internal/cmd/symbols.go
+++ b/gopls/internal/cmd/symbols.go
@@ -53,7 +53,7 @@ func (r *symbols) Run(ctx context.Context, args ...string) error {
return err
}
for _, s := range symbols {
- if m, ok := s.(map[string]interface{}); ok {
+ if m, ok := s.(map[string]any); ok {
s, err = mapToSymbol(m)
if err != nil {
return err
@@ -69,7 +69,7 @@ func (r *symbols) Run(ctx context.Context, args ...string) error {
return nil
}
-func mapToSymbol(m map[string]interface{}) (interface{}, error) {
+func mapToSymbol(m map[string]any) (any, error) {
b, err := json.Marshal(m)
if err != nil {
return nil, err
diff --git a/gopls/internal/cmd/usage/check.hlp b/gopls/internal/cmd/usage/check.hlp
index eda1a25a191..c387c2cf5d9 100644
--- a/gopls/internal/cmd/usage/check.hlp
+++ b/gopls/internal/cmd/usage/check.hlp
@@ -6,3 +6,5 @@ Usage:
Example: show the diagnostic results of this file:
$ gopls check internal/cmd/check.go
+ -severity=string
+ minimum diagnostic severity (hint, info, warning, or error) (default "warning")
diff --git a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp
index 64f99a3387e..044d4251e89 100644
--- a/gopls/internal/cmd/usage/usage-v.hlp
+++ b/gopls/internal/cmd/usage/usage-v.hlp
@@ -61,8 +61,6 @@ flags:
filename to log to. if value is "auto", then logging to a default output file is enabled
-mode=string
no effect
- -ocagent=string
- the address of the ocagent (e.g. http://localhost:55678), or off (default "off")
-port=int
port on which to run gopls for debugging purposes
-profile.alloc=string
diff --git a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp
index c801a467626..b918b24a411 100644
--- a/gopls/internal/cmd/usage/usage.hlp
+++ b/gopls/internal/cmd/usage/usage.hlp
@@ -58,8 +58,6 @@ flags:
filename to log to. if value is "auto", then logging to a default output file is enabled
-mode=string
no effect
- -ocagent=string
- the address of the ocagent (e.g. http://localhost:55678), or off (default "off")
-port=int
port on which to run gopls for debugging purposes
-profile.alloc=string
diff --git a/gopls/internal/debug/log/log.go b/gopls/internal/debug/log/log.go
index d015f9bfdd3..9e7efa7bf17 100644
--- a/gopls/internal/debug/log/log.go
+++ b/gopls/internal/debug/log/log.go
@@ -33,7 +33,7 @@ func (l Level) Log(ctx context.Context, msg string) {
}
// Logf formats and exports a log event labeled with level l.
-func (l Level) Logf(ctx context.Context, format string, args ...interface{}) {
+func (l Level) Logf(ctx context.Context, format string, args ...any) {
l.Log(ctx, fmt.Sprintf(format, args...))
}
diff --git a/gopls/internal/debug/rpc.go b/gopls/internal/debug/rpc.go
index 8a696f848d0..5b8e1dbbbd0 100644
--- a/gopls/internal/debug/rpc.go
+++ b/gopls/internal/debug/rpc.go
@@ -209,7 +209,7 @@ func getStatusCode(span *export.Span) string {
return ""
}
-func (r *Rpcs) getData(req *http.Request) interface{} {
+func (r *Rpcs) getData(req *http.Request) any {
return r
}
diff --git a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go
index 058254b755b..7cfe2b3d23e 100644
--- a/gopls/internal/debug/serve.go
+++ b/gopls/internal/debug/serve.go
@@ -33,7 +33,6 @@ import (
"golang.org/x/tools/internal/event/core"
"golang.org/x/tools/internal/event/export"
"golang.org/x/tools/internal/event/export/metric"
- "golang.org/x/tools/internal/event/export/ocagent"
"golang.org/x/tools/internal/event/export/prometheus"
"golang.org/x/tools/internal/event/keys"
"golang.org/x/tools/internal/event/label"
@@ -51,13 +50,11 @@ type Instance struct {
Logfile string
StartTime time.Time
ServerAddress string
- OCAgentConfig string
LogWriter io.Writer
exporter event.Exporter
- ocagent *ocagent.Exporter
prometheus *prometheus.Exporter
rpcs *Rpcs
traces *traces
@@ -280,23 +277,23 @@ func cmdline(w http.ResponseWriter, r *http.Request) {
pprof.Cmdline(fake, r)
}
-func (i *Instance) getCache(r *http.Request) interface{} {
+func (i *Instance) getCache(r *http.Request) any {
return i.State.Cache(path.Base(r.URL.Path))
}
-func (i *Instance) getAnalysis(r *http.Request) interface{} {
+func (i *Instance) getAnalysis(r *http.Request) any {
return i.State.Analysis()
}
-func (i *Instance) getSession(r *http.Request) interface{} {
+func (i *Instance) getSession(r *http.Request) any {
return i.State.Session(path.Base(r.URL.Path))
}
-func (i *Instance) getClient(r *http.Request) interface{} {
+func (i *Instance) getClient(r *http.Request) any {
return i.State.Client(path.Base(r.URL.Path))
}
-func (i *Instance) getServer(r *http.Request) interface{} {
+func (i *Instance) getServer(r *http.Request) any {
i.State.mu.Lock()
defer i.State.mu.Unlock()
id := path.Base(r.URL.Path)
@@ -308,7 +305,7 @@ func (i *Instance) getServer(r *http.Request) interface{} {
return nil
}
-func (i *Instance) getFile(r *http.Request) interface{} {
+func (i *Instance) getFile(r *http.Request) any {
identifier := path.Base(r.URL.Path)
sid := path.Base(path.Dir(r.URL.Path))
s := i.State.Session(sid)
@@ -324,7 +321,7 @@ func (i *Instance) getFile(r *http.Request) interface{} {
return nil
}
-func (i *Instance) getInfo(r *http.Request) interface{} {
+func (i *Instance) getInfo(r *http.Request) any {
buf := &bytes.Buffer{}
i.PrintServerInfo(r.Context(), buf)
return template.HTML(buf.String())
@@ -340,7 +337,7 @@ func (i *Instance) AddService(s protocol.Server, session *cache.Session) {
stdlog.Printf("unable to find a Client to add the protocol.Server to")
}
-func getMemory(_ *http.Request) interface{} {
+func getMemory(_ *http.Request) any {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m
@@ -363,16 +360,11 @@ func GetInstance(ctx context.Context) *Instance {
// WithInstance creates debug instance ready for use using the supplied
// configuration and stores it in the returned context.
-func WithInstance(ctx context.Context, agent string) context.Context {
+func WithInstance(ctx context.Context) context.Context {
i := &Instance{
- StartTime: time.Now(),
- OCAgentConfig: agent,
+ StartTime: time.Now(),
}
i.LogWriter = os.Stderr
- ocConfig := ocagent.Discover()
- //TODO: we should not need to adjust the discovered configuration
- ocConfig.Address = i.OCAgentConfig
- i.ocagent = ocagent.Connect(ocConfig)
i.prometheus = prometheus.New()
i.rpcs = &Rpcs{}
i.traces = &traces{}
@@ -439,7 +431,7 @@ func (i *Instance) Serve(ctx context.Context, addr string) (string, error) {
event.Log(ctx, "Debug serving", label1.Port.Of(port))
go func() {
mux := http.NewServeMux()
- mux.HandleFunc("/", render(MainTmpl, func(*http.Request) interface{} { return i }))
+ mux.HandleFunc("/", render(MainTmpl, func(*http.Request) any { return i }))
mux.HandleFunc("/debug/", render(DebugTmpl, nil))
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", cmdline)
@@ -541,9 +533,6 @@ func messageType(l log.Level) protocol.MessageType {
func makeInstanceExporter(i *Instance) event.Exporter {
exporter := func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
- if i.ocagent != nil {
- ctx = i.ocagent.ProcessEvent(ctx, ev, lm)
- }
if i.prometheus != nil {
ctx = i.prometheus.ProcessEvent(ctx, ev, lm)
}
@@ -594,11 +583,11 @@ func makeInstanceExporter(i *Instance) event.Exporter {
return exporter
}
-type dataFunc func(*http.Request) interface{}
+type dataFunc func(*http.Request) any
func render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
- var data interface{}
+ var data any
if fun != nil {
data = fun(r)
}
diff --git a/gopls/internal/debug/template_test.go b/gopls/internal/debug/template_test.go
index d4d9071c140..52c60244776 100644
--- a/gopls/internal/debug/template_test.go
+++ b/gopls/internal/debug/template_test.go
@@ -29,7 +29,7 @@ import (
var templates = map[string]struct {
tmpl *template.Template
- data interface{} // a value of the needed type
+ data any // a value of the needed type
}{
"MainTmpl": {debug.MainTmpl, &debug.Instance{}},
"DebugTmpl": {debug.DebugTmpl, nil},
diff --git a/gopls/internal/debug/trace.go b/gopls/internal/debug/trace.go
index 9314a04d241..d80a32eecbe 100644
--- a/gopls/internal/debug/trace.go
+++ b/gopls/internal/debug/trace.go
@@ -11,6 +11,7 @@ import (
"html/template"
"net/http"
"runtime/trace"
+ "slices"
"sort"
"strings"
"sync"
@@ -271,13 +272,13 @@ func (t *traces) addRecentLocked(span *traceSpan, start bool) {
// as Go's GC cannot collect the ever-growing unused prefix.
// So, compact it periodically.
if t.recentEvictions%maxRecent == 0 {
- t.recent = append([]spanStartEnd(nil), t.recent...)
+ t.recent = slices.Clone(t.recent)
}
}
}
// getData returns the TraceResults rendered by TraceTmpl for the /trace[/name] endpoint.
-func (t *traces) getData(req *http.Request) interface{} {
+func (t *traces) getData(req *http.Request) any {
// TODO(adonovan): the HTTP request doesn't acquire the mutex
// for t or for each span! Audit and fix.
diff --git a/gopls/internal/doc/api.go b/gopls/internal/doc/api.go
index 258f90d49ae..52101dda8c9 100644
--- a/gopls/internal/doc/api.go
+++ b/gopls/internal/doc/api.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:generate go run ../../doc/generate
+//go:generate go run ./generate
// The doc package provides JSON metadata that documents gopls' public
// interfaces.
@@ -47,11 +47,13 @@ type EnumKey struct {
Name string // in JSON syntax (quoted)
Doc string
Default string
+ Status string // = "" | "advanced" | "experimental" | "deprecated"
}
type EnumValue struct {
- Value string // in JSON syntax (quoted)
- Doc string // doc comment; always starts with `Value`
+ Value string // in JSON syntax (quoted)
+ Doc string // doc comment; always starts with `Value`
+ Status string // = "" | "advanced" | "experimental" | "deprecated"
}
type Lens struct {
@@ -60,6 +62,7 @@ type Lens struct {
Title string
Doc string
Default bool
+ Status string // = "" | "advanced" | "experimental" | "deprecated"
}
type Analyzer struct {
@@ -73,4 +76,5 @@ type Hint struct {
Name string
Doc string
Default bool
+ Status string // = "" | "advanced" | "experimental" | "deprecated"
}
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index 8f101079a9c..37a996950be 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -124,19 +124,28 @@
"EnumValues": [
{
"Value": "\"FullDocumentation\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"NoDocumentation\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"SingleLine\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
+ },
+ {
+ "Value": "\"Structured\"",
+ "Doc": "`\"Structured\"` is a misguided experimental setting that returns a JSON\nhover format. This setting should not be used, as it will be removed in a\nfuture release of gopls.\n",
+ "Status": ""
},
{
"Value": "\"SynopsisDocumentation\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
}
],
"Default": "\"FullDocumentation\"",
@@ -169,15 +178,18 @@
"EnumValues": [
{
"Value": "false",
- "Doc": "false: do not show links"
+ "Doc": "false: do not show links",
+ "Status": ""
},
{
"Value": "true",
- "Doc": "true: show links to the `linkTarget` domain"
+ "Doc": "true: show links to the `linkTarget` domain",
+ "Status": ""
},
{
"Value": "\"gopls\"",
- "Doc": "`\"gopls\"`: show links to gopls' internal documentation viewer"
+ "Doc": "`\"gopls\"`: show links to gopls' internal documentation viewer",
+ "Status": ""
}
],
"Default": "true",
@@ -224,15 +236,18 @@
"EnumValues": [
{
"Value": "\"CaseInsensitive\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"CaseSensitive\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"Fuzzy\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
}
],
"Default": "\"Fuzzy\"",
@@ -279,15 +294,18 @@
"EnumValues": [
{
"Value": "\"Both\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"Definition\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"Link\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
}
],
"Default": "\"Both\"",
@@ -306,19 +324,23 @@
"EnumValues": [
{
"Value": "\"CaseInsensitive\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"CaseSensitive\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"FastFuzzy\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
},
{
"Value": "\"Fuzzy\"",
- "Doc": ""
+ "Doc": "",
+ "Status": ""
}
],
"Default": "\"FastFuzzy\"",
@@ -337,15 +359,18 @@
"EnumValues": [
{
"Value": "\"Dynamic\"",
- "Doc": "`\"Dynamic\"` uses whichever qualifier results in the highest scoring\nmatch for the given symbol query. Here a \"qualifier\" is any \"/\" or \".\"\ndelimited suffix of the fully qualified symbol. i.e. \"to/pkg.Foo.Field\" or\njust \"Foo.Field\".\n"
+ "Doc": "`\"Dynamic\"` uses whichever qualifier results in the highest scoring\nmatch for the given symbol query. Here a \"qualifier\" is any \"/\" or \".\"\ndelimited suffix of the fully qualified symbol. i.e. \"to/pkg.Foo.Field\" or\njust \"Foo.Field\".\n",
+ "Status": ""
},
{
"Value": "\"Full\"",
- "Doc": "`\"Full\"` is fully qualified symbols, i.e.\n\"path/to/pkg.Foo.Field\".\n"
+ "Doc": "`\"Full\"` is fully qualified symbols, i.e.\n\"path/to/pkg.Foo.Field\".\n",
+ "Status": ""
},
{
"Value": "\"Package\"",
- "Doc": "`\"Package\"` is package qualified symbols i.e.\n\"pkg.Foo.Field\".\n"
+ "Doc": "`\"Package\"` is package qualified symbols i.e.\n\"pkg.Foo.Field\".\n",
+ "Status": ""
}
],
"Default": "\"Dynamic\"",
@@ -364,11 +389,13 @@
"EnumValues": [
{
"Value": "\"all\"",
- "Doc": "`\"all\"` matches symbols in any loaded package, including\ndependencies.\n"
+ "Doc": "`\"all\"` matches symbols in any loaded package, including\ndependencies.\n",
+ "Status": ""
},
{
"Value": "\"workspace\"",
- "Doc": "`\"workspace\"` matches symbols in workspace packages only.\n"
+ "Doc": "`\"workspace\"` matches symbols in workspace packages only.\n",
+ "Status": ""
}
],
"Default": "\"all\"",
@@ -384,659 +411,2655 @@
"ValueType": "bool",
"Keys": [
{
- "Name": "\"appends\"",
- "Doc": "check for missing values after append\n\nThis checker reports calls to append that pass\nno values to be appended to the slice.\n\n\ts := []string{\"a\", \"b\", \"c\"}\n\t_ = append(s)\n\nSuch calls are always no-ops and often indicate an\nunderlying mistake.",
- "Default": "true"
+ "Name": "\"QF1001\"",
+ "Doc": "Apply De Morgan's law\n\nAvailable since\n 2021.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"asmdecl\"",
- "Doc": "report mismatches between assembly files and Go declarations",
- "Default": "true"
+ "Name": "\"QF1002\"",
+ "Doc": "Convert untagged switch to tagged switch\n\nAn untagged switch that compares a single variable against a series of\nvalues can be replaced with a tagged switch.\n\nBefore:\n\n switch {\n case x == 1 || x == 2, x == 3:\n ...\n case x == 4:\n ...\n default:\n ...\n }\n\nAfter:\n\n switch x {\n case 1, 2, 3:\n ...\n case 4:\n ...\n default:\n ...\n }\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"assign\"",
- "Doc": "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.",
- "Default": "true"
+ "Name": "\"QF1003\"",
+ "Doc": "Convert if/else-if chain to tagged switch\n\nA series of if/else-if checks comparing the same variable against\nvalues can be replaced with a tagged switch.\n\nBefore:\n\n if x == 1 || x == 2 {\n ...\n } else if x == 3 {\n ...\n } else {\n ...\n }\n\nAfter:\n\n switch x {\n case 1, 2:\n ...\n case 3:\n ...\n default:\n ...\n }\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"atomic\"",
- "Doc": "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(\u0026x, 1)\n\nwhich are not atomic.",
- "Default": "true"
+ "Name": "\"QF1004\"",
+ "Doc": "Use strings.ReplaceAll instead of strings.Replace with n == -1\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"atomicalign\"",
- "Doc": "check for non-64-bits-aligned arguments to sync/atomic functions",
- "Default": "true"
+ "Name": "\"QF1005\"",
+ "Doc": "Expand call to math.Pow\n\nSome uses of math.Pow can be simplified to basic multiplication.\n\nBefore:\n\n math.Pow(x, 2)\n\nAfter:\n\n x * x\n\nAvailable since\n 2021.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"bools\"",
- "Doc": "check for common mistakes involving boolean operators",
- "Default": "true"
+ "Name": "\"QF1006\"",
+ "Doc": "Lift if+break into loop condition\n\nBefore:\n\n for {\n if done {\n break\n }\n ...\n }\n\nAfter:\n\n for !done {\n ...\n }\n\nAvailable since\n 2021.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"buildtag\"",
- "Doc": "check //go:build and // +build directives",
- "Default": "true"
+ "Name": "\"QF1007\"",
+ "Doc": "Merge conditional assignment into variable declaration\n\nBefore:\n\n x := false\n if someCondition {\n x = true\n }\n\nAfter:\n\n x := someCondition\n\nAvailable since\n 2021.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"cgocall\"",
- "Doc": "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.",
- "Default": "true"
+ "Name": "\"QF1008\"",
+ "Doc": "Omit embedded fields from selector expression\n\nAvailable since\n 2021.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"composites\"",
- "Doc": "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = \u0026net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = \u0026net.DNSConfigError{Err: err}\n",
- "Default": "true"
+ "Name": "\"QF1009\"",
+ "Doc": "Use time.Time.Equal instead of == operator\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"copylocks\"",
- "Doc": "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.",
- "Default": "true"
+ "Name": "\"QF1010\"",
+ "Doc": "Convert slice of bytes to string when printing it\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"deepequalerrors\"",
- "Doc": "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.",
- "Default": "true"
+ "Name": "\"QF1011\"",
+ "Doc": "Omit redundant type from variable declaration\n\nAvailable since\n 2021.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"defers\"",
- "Doc": "report common mistakes in defer statements\n\nThe defers analyzer reports a diagnostic when a defer statement would\nresult in a non-deferred call to time.Since, as experience has shown\nthat this is nearly always a mistake.\n\nFor example:\n\n\tstart := time.Now()\n\t...\n\tdefer recordLatency(time.Since(start)) // error: call to time.Since is not deferred\n\nThe correct code is:\n\n\tdefer func() { recordLatency(time.Since(start)) }()",
- "Default": "true"
+ "Name": "\"QF1012\"",
+ "Doc": "Use fmt.Fprintf(x, ...) instead of x.Write(fmt.Sprintf(...))\n\nAvailable since\n 2022.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"deprecated\"",
- "Doc": "check for use of deprecated identifiers\n\nThe deprecated analyzer looks for deprecated symbols and package\nimports.\n\nSee https://go.dev/wiki/Deprecated to learn about Go's convention\nfor documenting and signaling deprecated identifiers.",
- "Default": "true"
+ "Name": "\"S1000\"",
+ "Doc": "Use plain channel send or receive instead of single-case select\n\nSelect statements with a single case can be replaced with a simple\nsend or receive.\n\nBefore:\n\n select {\n case x := \u003c-ch:\n fmt.Println(x)\n }\n\nAfter:\n\n x := \u003c-ch\n fmt.Println(x)\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"directive\"",
- "Doc": "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n",
- "Default": "true"
+ "Name": "\"S1001\"",
+ "Doc": "Replace for loop with call to copy\n\nUse copy() for copying elements from one slice to another. For\narrays of identical size, you can use simple assignment.\n\nBefore:\n\n for i, x := range src {\n dst[i] = x\n }\n\nAfter:\n\n copy(dst, src)\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"embed\"",
- "Doc": "check //go:embed directive usage\n\nThis analyzer checks that the embed package is imported if //go:embed\ndirectives are present, providing a suggested fix to add the import if\nit is missing.\n\nThis analyzer also checks that //go:embed directives precede the\ndeclaration of a single variable.",
- "Default": "true"
+ "Name": "\"S1002\"",
+ "Doc": "Omit comparison with boolean constant\n\nBefore:\n\n if x == true {}\n\nAfter:\n\n if x {}\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"errorsas\"",
- "Doc": "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.",
- "Default": "true"
+ "Name": "\"S1003\"",
+ "Doc": "Replace call to strings.Index with strings.Contains\n\nBefore:\n\n if strings.Index(x, y) != -1 {}\n\nAfter:\n\n if strings.Contains(x, y) {}\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"fillreturns\"",
- "Doc": "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.",
- "Default": "true"
+ "Name": "\"S1004\"",
+ "Doc": "Replace call to bytes.Compare with bytes.Equal\n\nBefore:\n\n if bytes.Compare(x, y) == 0 {}\n\nAfter:\n\n if bytes.Equal(x, y) {}\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"framepointer\"",
- "Doc": "report assembly that clobbers the frame pointer before saving it",
- "Default": "true"
+ "Name": "\"S1005\"",
+ "Doc": "Drop unnecessary use of the blank identifier\n\nIn many cases, assigning to the blank identifier is unnecessary.\n\nBefore:\n\n for _ = range s {}\n x, _ = someMap[key]\n _ = \u003c-ch\n\nAfter:\n\n for range s{}\n x = someMap[key]\n \u003c-ch\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"gofix\"",
- "Doc": "apply fixes based on go:fix comment directives\n\nThe gofix analyzer inlines functions and constants that are marked for inlining.",
- "Default": "true"
+ "Name": "\"S1006\"",
+ "Doc": "Use 'for { ... }' for infinite loops\n\nFor infinite loops, using for { ... } is the most idiomatic choice.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"hostport\"",
- "Doc": "check format of addresses passed to net.Dial\n\nThis analyzer flags code that produce network address strings using\nfmt.Sprintf, as in this example:\n\n addr := fmt.Sprintf(\"%s:%d\", host, 12345) // \"will not work with IPv6\"\n ...\n conn, err := net.Dial(\"tcp\", addr) // \"when passed to dial here\"\n\nThe analyzer suggests a fix to use the correct approach, a call to\nnet.JoinHostPort:\n\n addr := net.JoinHostPort(host, \"12345\")\n ...\n conn, err := net.Dial(\"tcp\", addr)\n\nA similar diagnostic and fix are produced for a format string of \"%s:%s\".\n",
- "Default": "true"
+ "Name": "\"S1007\"",
+ "Doc": "Simplify regular expression by using raw string literal\n\nRaw string literals use backticks instead of quotation marks and do not support\nany escape sequences. This means that the backslash can be used\nfreely, without the need of escaping.\n\nSince regular expressions have their own escape sequences, raw strings\ncan improve their readability.\n\nBefore:\n\n regexp.Compile(\"\\\\A(\\\\w+) profile: total \\\\d+\\\\n\\\\z\")\n\nAfter:\n\n regexp.Compile(`\\A(\\w+) profile: total \\d+\\n\\z`)\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"httpresponse\"",
- "Doc": "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.",
- "Default": "true"
+ "Name": "\"S1008\"",
+ "Doc": "Simplify returning boolean expression\n\nBefore:\n\n if \u003cexpr\u003e {\n return true\n }\n return false\n\nAfter:\n\n return \u003cexpr\u003e\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"ifaceassert\"",
- "Doc": "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.",
- "Default": "true"
+ "Name": "\"S1009\"",
+ "Doc": "Omit redundant nil check on slices, maps, and channels\n\nThe len function is defined for all slices, maps, and\nchannels, even nil ones, which have a length of zero. It is not necessary to\ncheck for nil before checking that their length is not zero.\n\nBefore:\n\n if x != nil \u0026\u0026 len(x) != 0 {}\n\nAfter:\n\n if len(x) != 0 {}\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"infertypeargs\"",
- "Doc": "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n",
- "Default": "true"
+ "Name": "\"S1010\"",
+ "Doc": "Omit default slice index\n\nWhen slicing, the second index defaults to the length of the value,\nmaking s[n:len(s)] and s[n:] equivalent.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"loopclosure\"",
- "Doc": "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nNote: An iteration variable can only outlive a loop iteration in Go versions \u003c=1.21.\nIn Go 1.22 and later, the loop variable lifetimes changed to create a new\niteration variable per loop iteration. (See go.dev/issue/60078.)\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v [\u003cgo1.22].\n\n\tfor _, v := range list {\n\t defer func() {\n\t use(v) // incorrect\n\t }()\n\t}\n\nOne fix is to create a new variable for each iteration of the loop:\n\n\tfor _, v := range list {\n\t v := v // new var per iteration\n\t defer func() {\n\t use(v) // ok\n\t }()\n\t}\n\nAfter Go version 1.22, the previous two for loops are equivalent\nand both are correct.\n\nThe next example uses a go statement and has a similar problem [\u003cgo1.22].\nIn addition, it has a data race because the loop updates v\nconcurrent with the goroutines accessing it.\n\n\tfor _, v := range elem {\n\t go func() {\n\t use(v) // incorrect, and a data race\n\t }()\n\t}\n\nA fix is the same as before. The checker also reports problems\nin goroutines started by golang.org/x/sync/errgroup.Group.\nA hard-to-spot variant of this form is common in parallel tests:\n\n\tfunc Test(t *testing.T) {\n\t for _, test := range tests {\n\t t.Run(test.name, func(t *testing.T) {\n\t t.Parallel()\n\t use(test) // incorrect, and a data race\n\t })\n\t }\n\t}\n\nThe t.Parallel() call causes the rest of the function to execute\nconcurrent with the loop [\u003cgo1.22].\n\nThe analyzer reports references only in the last statement,\nas it is not deep enough to understand the effects of subsequent\nstatements that might render the reference benign.\n(\"Last statement\" is defined recursively in compound\nstatements such as if, switch, and select.)\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines",
- "Default": "true"
+ "Name": "\"S1011\"",
+ "Doc": "Use a single append to concatenate two slices\n\nBefore:\n\n for _, e := range y {\n x = append(x, e)\n }\n \n for i := range y {\n x = append(x, y[i])\n }\n \n for i := range y {\n v := y[i]\n x = append(x, v)\n }\n\nAfter:\n\n x = append(x, y...)\n x = append(x, y...)\n x = append(x, y...)\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"lostcancel\"",
- "Doc": "check cancel func returned by context.WithCancel is called\n\nThe cancellation function returned by context.WithCancel, WithTimeout,\nWithDeadline and variants such as WithCancelCause must be called,\nor the new context will remain live until its parent context is cancelled.\n(The background context is never cancelled.)",
- "Default": "true"
+ "Name": "\"S1012\"",
+ "Doc": "Replace time.Now().Sub(x) with time.Since(x)\n\nThe time.Since helper has the same effect as using time.Now().Sub(x)\nbut is easier to read.\n\nBefore:\n\n time.Now().Sub(x)\n\nAfter:\n\n time.Since(x)\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"modernize\"",
- "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;",
- "Default": "true"
+ "Name": "\"S1016\"",
+ "Doc": "Use a type conversion instead of manually copying struct fields\n\nTwo struct types with identical fields can be converted between each\nother. In older versions of Go, the fields had to have identical\nstruct tags. Since Go 1.8, however, struct tags are ignored during\nconversions. It is thus not necessary to manually copy every field\nindividually.\n\nBefore:\n\n var x T1\n y := T2{\n Field1: x.Field1,\n Field2: x.Field2,\n }\n\nAfter:\n\n var x T1\n y := T2(x)\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"nilfunc\"",
- "Doc": "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.",
- "Default": "true"
+ "Name": "\"S1017\"",
+ "Doc": "Replace manual trimming with strings.TrimPrefix\n\nInstead of using strings.HasPrefix and manual slicing, use the\nstrings.TrimPrefix function. If the string doesn't start with the\nprefix, the original string will be returned. Using strings.TrimPrefix\nreduces complexity, and avoids common bugs, such as off-by-one\nmistakes.\n\nBefore:\n\n if strings.HasPrefix(str, prefix) {\n str = str[len(prefix):]\n }\n\nAfter:\n\n str = strings.TrimPrefix(str, prefix)\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"nilness\"",
- "Doc": "check for redundant or impossible nil comparisons\n\nThe nilness checker inspects the control-flow graph of each function in\na package and reports nil pointer dereferences, degenerate nil\npointers, and panics with nil values. A degenerate comparison is of the form\nx==nil or x!=nil where x is statically known to be nil or non-nil. These are\noften a mistake, especially in control flow related to errors. Panics with nil\nvalues are checked because they are not detectable by\n\n\tif r := recover(); r != nil {\n\nThis check reports conditions such as:\n\n\tif f == nil { // impossible condition (f is a function)\n\t}\n\nand:\n\n\tp := \u0026v\n\t...\n\tif p != nil { // tautological condition\n\t}\n\nand:\n\n\tif p == nil {\n\t\tprint(*p) // nil dereference\n\t}\n\nand:\n\n\tif p == nil {\n\t\tpanic(p)\n\t}\n\nSometimes the control flow may be quite complex, making bugs hard\nto spot. In the example below, the err.Error expression is\nguaranteed to panic because, after the first return, err must be\nnil. The intervening loop is just a distraction.\n\n\t...\n\terr := g.Wait()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpartialSuccess := false\n\tfor _, err := range errs {\n\t\tif err == nil {\n\t\t\tpartialSuccess = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif partialSuccess {\n\t\treportStatus(StatusMessage{\n\t\t\tCode: code.ERROR,\n\t\t\tDetail: err.Error(), // \"nil dereference in dynamic method call\"\n\t\t})\n\t\treturn nil\n\t}\n\n...",
- "Default": "true"
+ "Name": "\"S1018\"",
+ "Doc": "Use 'copy' for sliding elements\n\ncopy() permits using the same source and destination slice, even with\noverlapping ranges. This makes it ideal for sliding elements in a\nslice.\n\nBefore:\n\n for i := 0; i \u003c n; i++ {\n bs[i] = bs[offset+i]\n }\n\nAfter:\n\n copy(bs[:n], bs[offset:])\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"nonewvars\"",
- "Doc": "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\n\tz := 1\n\tz := 2\n\nwill turn into\n\n\tz := 1\n\tz = 2",
- "Default": "true"
+ "Name": "\"S1019\"",
+ "Doc": "Simplify 'make' call by omitting redundant arguments\n\nThe 'make' function has default values for the length and capacity\narguments. For channels, the length defaults to zero, and for slices,\nthe capacity defaults to the length.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"noresultvalues\"",
- "Doc": "suggested fixes for unexpected return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\" or \"too many return values\".\nFor example:\n\n\tfunc z() { return nil }\n\nwill turn into\n\n\tfunc z() { return }",
- "Default": "true"
+ "Name": "\"S1020\"",
+ "Doc": "Omit redundant nil check in type assertion\n\nBefore:\n\n if _, ok := i.(T); ok \u0026\u0026 i != nil {}\n\nAfter:\n\n if _, ok := i.(T); ok {}\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"printf\"",
- "Doc": "check consistency of Printf format strings and arguments\n\nThe check applies to calls of the formatting functions such as\n[fmt.Printf] and [fmt.Sprintf], as well as any detected wrappers of\nthose functions such as [log.Printf]. It reports a variety of\nmistakes such as syntax errors in the format string and mismatches\n(of number and type) between the verbs and their arguments.\n\nSee the documentation of the fmt package for the complete set of\nformat operators and their operand types.",
- "Default": "true"
+ "Name": "\"S1021\"",
+ "Doc": "Merge variable declaration and assignment\n\nBefore:\n\n var x uint\n x = 1\n\nAfter:\n\n var x uint = 1\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"shadow\"",
- "Doc": "check for possible unintended shadowing of variables\n\nThis analyzer check for shadowed variables.\nA shadowed variable is a variable declared in an inner scope\nwith the same name and type as a variable in an outer scope,\nand where the outer variable is mentioned after the inner one\nis declared.\n\n(This definition can be refined; the module generates too many\nfalse positives and is not yet enabled by default.)\n\nFor example:\n\n\tfunc BadRead(f *os.File, buf []byte) error {\n\t\tvar err error\n\t\tfor {\n\t\t\tn, err := f.Read(buf) // shadows the function variable 'err'\n\t\t\tif err != nil {\n\t\t\t\tbreak // causes return of wrong value\n\t\t\t}\n\t\t\tfoo(buf)\n\t\t}\n\t\treturn err\n\t}",
- "Default": "false"
+ "Name": "\"S1023\"",
+ "Doc": "Omit redundant control flow\n\nFunctions that have no return value do not need a return statement as\nthe final statement of the function.\n\nSwitches in Go do not have automatic fallthrough, unlike languages\nlike C. It is not necessary to have a break statement as the final\nstatement in a case block.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"shift\"",
- "Doc": "check for shifts that equal or exceed the width of the integer",
- "Default": "true"
+ "Name": "\"S1024\"",
+ "Doc": "Replace x.Sub(time.Now()) with time.Until(x)\n\nThe time.Until helper has the same effect as using x.Sub(time.Now())\nbut is easier to read.\n\nBefore:\n\n x.Sub(time.Now())\n\nAfter:\n\n time.Until(x)\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"sigchanyzer\"",
- "Doc": "check for unbuffered channel of os.Signal\n\nThis checker reports call expression of the form\n\n\tsignal.Notify(c \u003c-chan os.Signal, sig ...os.Signal),\n\nwhere c is an unbuffered channel, which can be at risk of missing the signal.",
- "Default": "true"
+ "Name": "\"S1025\"",
+ "Doc": "Don't use fmt.Sprintf(\"%s\", x) unnecessarily\n\nIn many instances, there are easier and more efficient ways of getting\na value's string representation. Whenever a value's underlying type is\na string already, or the type has a String method, they should be used\ndirectly.\n\nGiven the following shared definitions\n\n type T1 string\n type T2 int\n\n func (T2) String() string { return \"Hello, world\" }\n\n var x string\n var y T1\n var z T2\n\nwe can simplify\n\n fmt.Sprintf(\"%s\", x)\n fmt.Sprintf(\"%s\", y)\n fmt.Sprintf(\"%s\", z)\n\nto\n\n x\n string(y)\n z.String()\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"simplifycompositelit\"",
- "Doc": "check for composite literal simplifications\n\nAn array, slice, or map composite literal of the form:\n\n\t[]T{T{}, T{}}\n\nwill be simplified to:\n\n\t[]T{{}, {}}\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.",
- "Default": "true"
+ "Name": "\"S1028\"",
+ "Doc": "Simplify error construction with fmt.Errorf\n\nBefore:\n\n errors.New(fmt.Sprintf(...))\n\nAfter:\n\n fmt.Errorf(...)\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"simplifyrange\"",
- "Doc": "check for range statement simplifications\n\nA range of the form:\n\n\tfor x, _ = range v {...}\n\nwill be simplified to:\n\n\tfor x = range v {...}\n\nA range of the form:\n\n\tfor _ = range v {...}\n\nwill be simplified to:\n\n\tfor range v {...}\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.",
- "Default": "true"
+ "Name": "\"S1029\"",
+ "Doc": "Range over the string directly\n\nRanging over a string will yield byte offsets and runes. If the offset\nisn't used, this is functionally equivalent to converting the string\nto a slice of runes and ranging over that. Ranging directly over the\nstring will be more performant, however, as it avoids allocating a new\nslice, the size of which depends on the length of the string.\n\nBefore:\n\n for _, r := range []rune(s) {}\n\nAfter:\n\n for _, r := range s {}\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"simplifyslice\"",
- "Doc": "check for slice simplifications\n\nA slice expression of the form:\n\n\ts[a:len(s)]\n\nwill be simplified to:\n\n\ts[a:]\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.",
- "Default": "true"
+ "Name": "\"S1030\"",
+ "Doc": "Use bytes.Buffer.String or bytes.Buffer.Bytes\n\nbytes.Buffer has both a String and a Bytes method. It is almost never\nnecessary to use string(buf.Bytes()) or []byte(buf.String()) – simply\nuse the other method.\n\nThe only exception to this are map lookups. Due to a compiler optimization,\nm[string(buf.Bytes())] is more efficient than m[buf.String()].\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"slog\"",
- "Doc": "check for invalid structured logging calls\n\nThe slog checker looks for calls to functions from the log/slog\npackage that take alternating key-value pairs. It reports calls\nwhere an argument in a key position is neither a string nor a\nslog.Attr, and where a final key is missing its value.\nFor example,it would report\n\n\tslog.Warn(\"message\", 11, \"k\") // slog.Warn arg \"11\" should be a string or a slog.Attr\n\nand\n\n\tslog.Info(\"message\", \"k1\", v1, \"k2\") // call to slog.Info missing a final value",
- "Default": "true"
+ "Name": "\"S1031\"",
+ "Doc": "Omit redundant nil check around loop\n\nYou can use range on nil slices and maps, the loop will simply never\nexecute. This makes an additional nil check around the loop\nunnecessary.\n\nBefore:\n\n if s != nil {\n for _, x := range s {\n ...\n }\n }\n\nAfter:\n\n for _, x := range s {\n ...\n }\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"sortslice\"",
- "Doc": "check the argument type of sort.Slice\n\nsort.Slice requires an argument of a slice type. Check that\nthe interface{} value passed to sort.Slice is actually a slice.",
- "Default": "true"
+ "Name": "\"S1032\"",
+ "Doc": "Use sort.Ints(x), sort.Float64s(x), and sort.Strings(x)\n\nThe sort.Ints, sort.Float64s and sort.Strings functions are easier to\nread than sort.Sort(sort.IntSlice(x)), sort.Sort(sort.Float64Slice(x))\nand sort.Sort(sort.StringSlice(x)).\n\nBefore:\n\n sort.Sort(sort.StringSlice(x))\n\nAfter:\n\n sort.Strings(x)\n\nAvailable since\n 2019.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"stdmethods\"",
- "Doc": "check signature of methods of well-known interfaces\n\nSometimes a type may be intended to satisfy an interface but may fail to\ndo so because of a mistake in its method signature.\nFor example, the result of this WriteTo method should be (int64, error),\nnot error, to satisfy io.WriterTo:\n\n\ttype myWriterTo struct{...}\n\tfunc (myWriterTo) WriteTo(w io.Writer) error { ... }\n\nThis check ensures that each method whose name matches one of several\nwell-known interface methods from the standard library has the correct\nsignature for that interface.\n\nChecked method names include:\n\n\tFormat GobEncode GobDecode MarshalJSON MarshalXML\n\tPeek ReadByte ReadFrom ReadRune Scan Seek\n\tUnmarshalJSON UnreadByte UnreadRune WriteByte\n\tWriteTo",
- "Default": "true"
+ "Name": "\"S1033\"",
+ "Doc": "Unnecessary guard around call to 'delete'\n\nCalling delete on a nil map is a no-op.\n\nAvailable since\n 2019.2\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"stdversion\"",
- "Doc": "report uses of too-new standard library symbols\n\nThe stdversion analyzer reports references to symbols in the standard\nlibrary that were introduced by a Go release higher than the one in\nforce in the referring file. (Recall that the file's Go version is\ndefined by the 'go' directive its module's go.mod file, or by a\n\"//go:build go1.X\" build tag at the top of the file.)\n\nThe analyzer does not report a diagnostic for a reference to a \"too\nnew\" field or method of a type that is itself \"too new\", as this may\nhave false positives, for example if fields or methods are accessed\nthrough a type alias that is guarded by a Go version constraint.\n",
- "Default": "true"
+ "Name": "\"S1034\"",
+ "Doc": "Use result of type assertion to simplify cases\n\nAvailable since\n 2019.2\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"stringintconv\"",
- "Doc": "check for string(int) conversions\n\nThis checker flags conversions of the form string(x) where x is an integer\n(but not byte or rune) type. Such conversions are discouraged because they\nreturn the UTF-8 representation of the Unicode code point x, and not a decimal\nstring representation of x as one might expect. Furthermore, if x denotes an\ninvalid code point, the conversion cannot be statically rejected.\n\nFor conversions that intend on using the code point, consider replacing them\nwith string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the\nstring representation of the value in the desired base.",
- "Default": "true"
+ "Name": "\"S1035\"",
+ "Doc": "Redundant call to net/http.CanonicalHeaderKey in method call on net/http.Header\n\nThe methods on net/http.Header, namely Add, Del, Get\nand Set, already canonicalize the given header name.\n\nAvailable since\n 2020.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"structtag\"",
- "Doc": "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.",
- "Default": "true"
+ "Name": "\"S1036\"",
+ "Doc": "Unnecessary guard around map access\n\nWhen accessing a map key that doesn't exist yet, one receives a zero\nvalue. Often, the zero value is a suitable value, for example when\nusing append or doing integer math.\n\nThe following\n\n if _, ok := m[\"foo\"]; ok {\n m[\"foo\"] = append(m[\"foo\"], \"bar\")\n } else {\n m[\"foo\"] = []string{\"bar\"}\n }\n\ncan be simplified to\n\n m[\"foo\"] = append(m[\"foo\"], \"bar\")\n\nand\n\n if _, ok := m2[\"k\"]; ok {\n m2[\"k\"] += 4\n } else {\n m2[\"k\"] = 4\n }\n\ncan be simplified to\n\n m[\"k\"] += 4\n\nAvailable since\n 2020.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"testinggoroutine\"",
- "Doc": "report calls to (*testing.T).Fatal from goroutines started by a test\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t go func() {\n\t t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t }()\n\t}",
- "Default": "true"
+ "Name": "\"S1037\"",
+ "Doc": "Elaborate way of sleeping\n\nUsing a select statement with a single case receiving\nfrom the result of time.After is a very elaborate way of sleeping that\ncan much simpler be expressed with a simple call to time.Sleep.\n\nAvailable since\n 2020.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"tests\"",
- "Doc": "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark, Fuzzing and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.",
- "Default": "true"
+ "Name": "\"S1038\"",
+ "Doc": "Unnecessarily complex way of printing formatted string\n\nInstead of using fmt.Print(fmt.Sprintf(...)), one can use fmt.Printf(...).\n\nAvailable since\n 2020.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"timeformat\"",
- "Doc": "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.",
- "Default": "true"
+ "Name": "\"S1039\"",
+ "Doc": "Unnecessary use of fmt.Sprint\n\nCalling fmt.Sprint with a single string argument is unnecessary\nand identical to using the string directly.\n\nAvailable since\n 2020.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"unmarshal\"",
- "Doc": "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.",
- "Default": "true"
+ "Name": "\"S1040\"",
+ "Doc": "Type assertion to current type\n\nThe type assertion x.(SomeInterface), when x already has type\nSomeInterface, can only fail if x is nil. Usually, this is\nleft-over code from when x had a different type and you can safely\ndelete the type assertion. If you want to check that x is not nil,\nconsider being explicit and using an actual if x == nil comparison\ninstead of relying on the type assertion panicking.\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"unreachable\"",
- "Doc": "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by a return statement, a call to panic, an\ninfinite loop, or similar constructs.",
- "Default": "true"
+ "Name": "\"SA1000\"",
+ "Doc": "Invalid regular expression\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"unsafeptr\"",
- "Doc": "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.",
- "Default": "true"
+ "Name": "\"SA1001\"",
+ "Doc": "Invalid template\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"unusedfunc\"",
- "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report a false positive for a declaration of an\nunexported function that is referenced from another package using\nthe go:linkname mechanism, if the declaration's doc comment does\nnot also have a go:linkname comment. (Such code is in any case\nstrongly discouraged: linkname annotations, if they must be used at\nall, should be used on both the declaration and the alias.)\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.",
- "Default": "true"
+ "Name": "\"SA1002\"",
+ "Doc": "Invalid format in time.Parse\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"unusedparams\"",
- "Doc": "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.\n\nThis analyzer ignores generated code.",
- "Default": "true"
+ "Name": "\"SA1003\"",
+ "Doc": "Unsupported argument to functions in encoding/binary\n\nThe encoding/binary package can only serialize types with known sizes.\nThis precludes the use of the int and uint types, as their sizes\ndiffer on different architectures. Furthermore, it doesn't support\nserializing maps, channels, strings, or functions.\n\nBefore Go 1.8, bool wasn't supported, either.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"unusedresult\"",
- "Doc": "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side\neffects, so it is always a mistake to discard the result. Other\nfunctions may return an error that must not be ignored, or a cleanup\noperation that must be called. This analyzer reports calls to\nfunctions like these when the result of the call is ignored.\n\nThe set of functions may be controlled using flags.",
- "Default": "true"
+ "Name": "\"SA1004\"",
+ "Doc": "Suspiciously small untyped constant in time.Sleep\n\nThe time.Sleep function takes a time.Duration as its only argument.\nDurations are expressed in nanoseconds. Thus, calling time.Sleep(1)\nwill sleep for 1 nanosecond. This is a common source of bugs, as sleep\nfunctions in other languages often accept seconds or milliseconds.\n\nThe time package provides constants such as time.Second to express\nlarge durations. These can be combined with arithmetic to express\narbitrary durations, for example 5 * time.Second for 5 seconds.\n\nIf you truly meant to sleep for a tiny amount of time, use\nn * time.Nanosecond to signal to Staticcheck that you did mean to sleep\nfor some amount of nanoseconds.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"unusedvariable\"",
- "Doc": "check for unused variables and suggest fixes",
- "Default": "true"
+ "Name": "\"SA1005\"",
+ "Doc": "Invalid first argument to exec.Command\n\nos/exec runs programs directly (using variants of the fork and exec\nsystem calls on Unix systems). This shouldn't be confused with running\na command in a shell. The shell will allow for features such as input\nredirection, pipes, and general scripting. The shell is also\nresponsible for splitting the user's input into a program name and its\narguments. For example, the equivalent to\n\n ls / /tmp\n\nwould be\n\n exec.Command(\"ls\", \"/\", \"/tmp\")\n\nIf you want to run a command in a shell, consider using something like\nthe following – but be aware that not all systems, particularly\nWindows, will have a /bin/sh program:\n\n exec.Command(\"/bin/sh\", \"-c\", \"ls | grep Awesome\")\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"unusedwrite\"",
- "Doc": "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}",
- "Default": "true"
+ "Name": "\"SA1007\"",
+ "Doc": "Invalid URL in net/url.Parse\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"waitgroup\"",
- "Doc": "check for misuses of sync.WaitGroup\n\nThis analyzer detects mistaken calls to the (*sync.WaitGroup).Add\nmethod from inside a new goroutine, causing Add to race with Wait:\n\n\t// WRONG\n\tvar wg sync.WaitGroup\n\tgo func() {\n\t wg.Add(1) // \"WaitGroup.Add called from inside new goroutine\"\n\t defer wg.Done()\n\t ...\n\t}()\n\twg.Wait() // (may return prematurely before new goroutine starts)\n\nThe correct code calls Add before starting the goroutine:\n\n\t// RIGHT\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t...\n\t}()\n\twg.Wait()",
- "Default": "true"
+ "Name": "\"SA1008\"",
+ "Doc": "Non-canonical key in http.Header map\n\nKeys in http.Header maps are canonical, meaning they follow a specific\ncombination of uppercase and lowercase letters. Methods such as\nhttp.Header.Add and http.Header.Del convert inputs into this canonical\nform before manipulating the map.\n\nWhen manipulating http.Header maps directly, as opposed to using the\nprovided methods, care should be taken to stick to canonical form in\norder to avoid inconsistencies. The following piece of code\ndemonstrates one such inconsistency:\n\n h := http.Header{}\n h[\"etag\"] = []string{\"1234\"}\n h.Add(\"etag\", \"5678\")\n fmt.Println(h)\n\n // Output:\n // map[Etag:[5678] etag:[1234]]\n\nThe easiest way of obtaining the canonical form of a key is to use\nhttp.CanonicalHeaderKey.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"yield\"",
- "Doc": "report calls to yield where the result is ignored\n\nAfter a yield function returns false, the caller should not call\nthe yield function again; generally the iterator should return\npromptly.\n\nThis example fails to check the result of the call to yield,\ncausing this analyzer to report a diagnostic:\n\n\tyield(1) // yield may be called again (on L2) after returning false\n\tyield(2)\n\nThe corrected code is either this:\n\n\tif yield(1) { yield(2) }\n\nor simply:\n\n\t_ = yield(1) \u0026\u0026 yield(2)\n\nIt is not always a mistake to ignore the result of yield.\nFor example, this is a valid single-element iterator:\n\n\tyield(1) // ok to ignore result\n\treturn\n\nIt is only a mistake when the yield call that returned false may be\nfollowed by another call.",
- "Default": "true"
- }
- ]
- },
- "EnumValues": null,
- "Default": "{}",
- "Status": "",
- "Hierarchy": "ui.diagnostic",
- "DeprecationMessage": ""
- },
- {
- "Name": "staticcheck",
- "Type": "bool",
- "Doc": "staticcheck enables additional analyses from staticcheck.io.\nThese analyses are documented on\n[Staticcheck's website](https://staticcheck.io/docs/checks/).\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "false",
- "Status": "experimental",
- "Hierarchy": "ui.diagnostic",
- "DeprecationMessage": ""
- },
- {
- "Name": "vulncheck",
- "Type": "enum",
- "Doc": "vulncheck enables vulnerability scanning.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": [
- {
- "Value": "\"Imports\"",
- "Doc": "`\"Imports\"`: In Imports mode, `gopls` will report vulnerabilities that affect packages\ndirectly and indirectly used by the analyzed main module.\n"
- },
- {
- "Value": "\"Off\"",
- "Doc": "`\"Off\"`: Disable vulnerability analysis.\n"
- }
- ],
- "Default": "\"Off\"",
- "Status": "experimental",
- "Hierarchy": "ui.diagnostic",
- "DeprecationMessage": ""
- },
- {
- "Name": "diagnosticsDelay",
- "Type": "time.Duration",
- "Doc": "diagnosticsDelay controls the amount of time that gopls waits\nafter the most recent file modification before computing deep diagnostics.\nSimple diagnostics (parsing and type-checking) are always run immediately\non recently modified packages.\n\nThis option must be set to a valid duration string, for example `\"250ms\"`.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "\"1s\"",
- "Status": "advanced",
- "Hierarchy": "ui.diagnostic",
- "DeprecationMessage": ""
- },
- {
- "Name": "diagnosticsTrigger",
- "Type": "enum",
- "Doc": "diagnosticsTrigger controls when to run diagnostics.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": [
- {
- "Value": "\"Edit\"",
- "Doc": "`\"Edit\"`: Trigger diagnostics on file edit and save. (default)\n"
- },
- {
- "Value": "\"Save\"",
- "Doc": "`\"Save\"`: Trigger diagnostics only on file save. Events like initial workspace load\nor configuration change will still trigger diagnostics.\n"
- }
- ],
- "Default": "\"Edit\"",
- "Status": "experimental",
- "Hierarchy": "ui.diagnostic",
- "DeprecationMessage": ""
- },
- {
- "Name": "analysisProgressReporting",
- "Type": "bool",
- "Doc": "analysisProgressReporting controls whether gopls sends progress\nnotifications when construction of its index of analysis facts is taking a\nlong time. Cancelling these notifications will cancel the indexing task,\nthough it will restart after the next change in the workspace.\n\nWhen a package is opened for the first time and heavyweight analyses such as\nstaticcheck are enabled, it can take a while to construct the index of\nanalysis facts for all its dependencies. The index is cached in the\nfilesystem, so subsequent analysis should be faster.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "true",
- "Status": "",
- "Hierarchy": "ui.diagnostic",
- "DeprecationMessage": ""
- },
- {
- "Name": "hints",
- "Type": "map[enum]bool",
- "Doc": "hints specify inlay hints that users want to see. A full list of hints\nthat gopls uses can be found in\n[inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n",
- "EnumKeys": {
- "ValueType": "bool",
- "Keys": [
+ "Name": "\"SA1010\"",
+ "Doc": "(*regexp.Regexp).FindAll called with n == 0, which will always return zero results\n\nIf n \u003e= 0, the function returns at most n matches/submatches. To\nreturn all results, specify a negative number.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
{
- "Name": "\"assignVariableTypes\"",
- "Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```\n",
- "Default": "false"
+ "Name": "\"SA1011\"",
+ "Doc": "Various methods in the 'strings' package expect valid UTF-8, but invalid input is provided\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"compositeLiteralFields\"",
- "Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```\n",
- "Default": "false"
+ "Name": "\"SA1012\"",
+ "Doc": "A nil context.Context is being passed to a function, consider using context.TODO instead\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"compositeLiteralTypes\"",
- "Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n",
- "Default": "false"
+ "Name": "\"SA1013\"",
+ "Doc": "io.Seeker.Seek is being called with the whence constant as the first argument, but it should be the second\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"constantValues\"",
- "Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```\n",
- "Default": "false"
+ "Name": "\"SA1014\"",
+ "Doc": "Non-pointer value passed to Unmarshal or Decode\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"functionTypeParameters\"",
- "Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```\n",
- "Default": "false"
+ "Name": "\"SA1015\"",
+ "Doc": "Using time.Tick in a way that will leak. Consider using time.NewTicker, and only use time.Tick in tests, commands and endless functions\n\nBefore Go 1.23, time.Tickers had to be closed to be able to be garbage\ncollected. Since time.Tick doesn't make it possible to close the underlying\nticker, using it repeatedly would leak memory.\n\nGo 1.23 fixes this by allowing tickers to be collected even if they weren't closed.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"parameterNames\"",
- "Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n",
- "Default": "false"
+ "Name": "\"SA1016\"",
+ "Doc": "Trapping a signal that cannot be trapped\n\nNot all signals can be intercepted by a process. Specifically, on\nUNIX-like systems, the syscall.SIGKILL and syscall.SIGSTOP signals are\nnever passed to the process, but instead handled directly by the\nkernel. It is therefore pointless to try and handle these signals.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
},
{
- "Name": "\"rangeVariableTypes\"",
- "Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n",
- "Default": "false"
- }
- ]
- },
- "EnumValues": null,
- "Default": "{}",
- "Status": "experimental",
- "Hierarchy": "ui.inlayhint",
- "DeprecationMessage": ""
- },
- {
- "Name": "codelenses",
- "Type": "map[enum]bool",
- "Doc": "codelenses overrides the enabled/disabled state of each of gopls'\nsources of [Code Lenses](codelenses.md).\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"codelenses\": {\n \"generate\": false, // Don't show the `go generate` lens.\n }\n...\n}\n```\n",
- "EnumKeys": {
- "ValueType": "bool",
- "Keys": [
+ "Name": "\"SA1017\"",
+ "Doc": "Channels used with os/signal.Notify should be buffered\n\nThe os/signal package uses non-blocking channel sends when delivering\nsignals. If the receiving end of the channel isn't ready and the\nchannel is either unbuffered or full, the signal will be dropped. To\navoid missing signals, the channel should be buffered and of the\nappropriate size. For a channel used for notification of just one\nsignal value, a buffer of size 1 is sufficient.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
{
- "Name": "\"generate\"",
- "Doc": "`\"generate\"`: Run `go generate`\n\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n",
- "Default": "true"
+ "Name": "\"SA1018\"",
+ "Doc": "strings.Replace called with n == 0, which does nothing\n\nWith n == 0, zero instances will be replaced. To replace all\ninstances, use a negative number, or use strings.ReplaceAll.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"regenerate_cgo\"",
- "Doc": "`\"regenerate_cgo\"`: Re-generate cgo declarations\n\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n",
- "Default": "true"
+ "Name": "\"SA1020\"",
+ "Doc": "Using an invalid host:port pair with a net.Listen-related function\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"run_govulncheck\"",
- "Doc": "`\"run_govulncheck\"`: Run govulncheck (legacy)\n\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run Govulncheck asynchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
- "Default": "false"
+ "Name": "\"SA1021\"",
+ "Doc": "Using bytes.Equal to compare two net.IP\n\nA net.IP stores an IPv4 or IPv6 address as a slice of bytes. The\nlength of the slice for an IPv4 address, however, can be either 4 or\n16 bytes long, using different ways of representing IPv4 addresses. In\norder to correctly compare two net.IPs, the net.IP.Equal method should\nbe used, as it takes both representations into account.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"test\"",
- "Doc": "`\"test\"`: Run tests and benchmarks\n\nThis codelens source annotates each `Test` and `Benchmark`\nfunction in a `*_test.go` file with a command to run it.\n\nThis source is off by default because VS Code has\na client-side custom UI for testing, and because progress\nnotifications are not a great UX for streamed test output.\nSee:\n- golang/go#67400 for a discussion of this feature.\n- https://github.com/joaotavora/eglot/discussions/1402\n for an alternative approach.\n",
- "Default": "false"
+ "Name": "\"SA1023\"",
+ "Doc": "Modifying the buffer in an io.Writer implementation\n\nWrite must not modify the slice data, even temporarily.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"tidy\"",
- "Doc": "`\"tidy\"`: Tidy go.mod file\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\ntidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures\nthat the go.mod file matches the source code in the module.\n",
- "Default": "true"
+ "Name": "\"SA1024\"",
+ "Doc": "A string cutset contains duplicate characters\n\nThe strings.TrimLeft and strings.TrimRight functions take cutsets, not\nprefixes. A cutset is treated as a set of characters to remove from a\nstring. For example,\n\n strings.TrimLeft(\"42133word\", \"1234\")\n\nwill result in the string \"word\" – any characters that are 1, 2, 3 or\n4 are cut from the left of the string.\n\nIn order to remove one string from another, use strings.TrimPrefix instead.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"upgrade_dependency\"",
- "Doc": "`\"upgrade_dependency\"`: Update dependencies\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with commands to:\n\n- check for available upgrades,\n- upgrade direct dependencies, and\n- upgrade all dependencies transitively.\n",
- "Default": "true"
+ "Name": "\"SA1025\"",
+ "Doc": "It is not possible to use (*time.Timer).Reset's return value correctly\n\nAvailable since\n 2019.1\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"vendor\"",
- "Doc": "`\"vendor\"`: Update vendor directory\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n",
- "Default": "true"
+ "Name": "\"SA1026\"",
+ "Doc": "Cannot marshal channels or functions\n\nAvailable since\n 2019.2\n",
+ "Default": "false",
+ "Status": ""
},
{
- "Name": "\"vulncheck\"",
- "Doc": "`\"vulncheck\"`: Run govulncheck\n\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run govulncheck synchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
- "Default": "false"
- }
- ]
- },
- "EnumValues": null,
- "Default": "{\"generate\":true,\"regenerate_cgo\":true,\"run_govulncheck\":false,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}",
- "Status": "",
- "Hierarchy": "ui",
- "DeprecationMessage": ""
- },
- {
- "Name": "semanticTokens",
- "Type": "bool",
- "Doc": "semanticTokens controls whether the LSP server will send\nsemantic tokens to the client.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "false",
- "Status": "experimental",
- "Hierarchy": "ui",
- "DeprecationMessage": ""
- },
- {
- "Name": "noSemanticString",
- "Type": "bool",
- "Doc": "noSemanticString turns off the sending of the semantic token 'string'\n\nDeprecated: Use SemanticTokenTypes[\"string\"] = false instead. See\ngolang/vscode-go#3632\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "false",
- "Status": "experimental",
- "Hierarchy": "ui",
- "DeprecationMessage": "use SemanticTokenTypes[\"string\"] = false instead. See\ngolang/vscode-go#3632\n"
- },
- {
- "Name": "noSemanticNumber",
- "Type": "bool",
- "Doc": "noSemanticNumber turns off the sending of the semantic token 'number'\n\nDeprecated: Use SemanticTokenTypes[\"number\"] = false instead. See\ngolang/vscode-go#3632.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "false",
- "Status": "experimental",
- "Hierarchy": "ui",
- "DeprecationMessage": "use SemanticTokenTypes[\"number\"] = false instead. See\ngolang/vscode-go#3632.\n"
- },
- {
- "Name": "semanticTokenTypes",
- "Type": "map[string]bool",
- "Doc": "semanticTokenTypes configures the semantic token types. It allows\ndisabling types by setting each value to false.\nBy default, all types are enabled.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "{}",
- "Status": "experimental",
- "Hierarchy": "ui",
- "DeprecationMessage": ""
- },
- {
- "Name": "semanticTokenModifiers",
- "Type": "map[string]bool",
- "Doc": "semanticTokenModifiers configures the semantic token modifiers. It allows\ndisabling modifiers by setting each value to false.\nBy default, all modifiers are enabled.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "{}",
- "Status": "experimental",
- "Hierarchy": "ui",
- "DeprecationMessage": ""
- },
- {
- "Name": "local",
- "Type": "string",
- "Doc": "local is the equivalent of the `goimports -local` flag, which puts\nimports beginning with this string after third-party packages. It should\nbe the prefix of the import path whose imports should be grouped\nseparately.\n\nIt is used when tidying imports (during an LSP Organize\nImports request) or when inserting new ones (for example,\nduring completion); an LSP Formatting request merely sorts the\nexisting imports.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "\"\"",
- "Status": "",
- "Hierarchy": "formatting",
- "DeprecationMessage": ""
- },
- {
- "Name": "gofumpt",
- "Type": "bool",
- "Doc": "gofumpt indicates if we should run gofumpt formatting.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "false",
- "Status": "",
- "Hierarchy": "formatting",
- "DeprecationMessage": ""
- },
- {
- "Name": "verboseOutput",
- "Type": "bool",
- "Doc": "verboseOutput enables additional debug logging.\n",
- "EnumKeys": {
- "ValueType": "",
- "Keys": null
- },
- "EnumValues": null,
- "Default": "false",
- "Status": "debug",
- "Hierarchy": "",
- "DeprecationMessage": ""
- }
- ]
- },
- "Lenses": [
+ "Name": "\"SA1027\"",
+ "Doc": "Atomic access to 64-bit variable must be 64-bit aligned\n\nOn ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to\narrange for 64-bit alignment of 64-bit words accessed atomically. The\nfirst word in a variable or in an allocated struct, array, or slice\ncan be relied upon to be 64-bit aligned.\n\nYou can use the structlayout tool to inspect the alignment of fields\nin a struct.\n\nAvailable since\n 2019.2\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA1028\"",
+ "Doc": "sort.Slice can only be used on slices\n\nThe first argument of sort.Slice must be a slice.\n\nAvailable since\n 2020.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA1029\"",
+ "Doc": "Inappropriate key in call to context.WithValue\n\nThe provided key must be comparable and should not be\nof type string or any other built-in type to avoid collisions between\npackages using context. Users of WithValue should define their own\ntypes for keys.\n\nTo avoid allocating when assigning to an interface{},\ncontext keys often have concrete type struct{}. Alternatively,\nexported context key variables' static type should be a pointer or\ninterface.\n\nAvailable since\n 2020.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA1030\"",
+ "Doc": "Invalid argument in call to a strconv function\n\nThis check validates the format, number base and bit size arguments of\nthe various parsing and formatting functions in strconv.\n\nAvailable since\n 2021.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA1031\"",
+ "Doc": "Overlapping byte slices passed to an encoder\n\nIn an encoding function of the form Encode(dst, src), dst and\nsrc were found to reference the same memory. This can result in\nsrc bytes being overwritten before they are read, when the encoder\nwrites more than one byte per src byte.\n\nAvailable since\n 2024.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA1032\"",
+ "Doc": "Wrong order of arguments to errors.Is\n\nThe first argument of the function errors.Is is the error\nthat we have and the second argument is the error we're trying to match against.\nFor example:\n\n\tif errors.Is(err, io.EOF) { ... }\n\nThis check detects some cases where the two arguments have been swapped. It\nflags any calls where the first argument is referring to a package-level error\nvariable, such as\n\n\tif errors.Is(io.EOF, err) { /* this is wrong */ }\n\nAvailable since\n 2024.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA2001\"",
+ "Doc": "Empty critical section, did you mean to defer the unlock?\n\nEmpty critical sections of the kind\n\n mu.Lock()\n mu.Unlock()\n\nare very often a typo, and the following was intended instead:\n\n mu.Lock()\n defer mu.Unlock()\n\nDo note that sometimes empty critical sections can be useful, as a\nform of signaling to wait on another goroutine. Many times, there are\nsimpler ways of achieving the same effect. When that isn't the case,\nthe code should be amply commented to avoid confusion. Combining such\ncomments with a //lint:ignore directive can be used to suppress this\nrare false positive.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA2002\"",
+ "Doc": "Called testing.T.FailNow or SkipNow in a goroutine, which isn't allowed\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA2003\"",
+ "Doc": "Deferred Lock right after locking, likely meant to defer Unlock instead\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA3000\"",
+ "Doc": "TestMain doesn't call os.Exit, hiding test failures\n\nTest executables (and in turn 'go test') exit with a non-zero status\ncode if any tests failed. When specifying your own TestMain function,\nit is your responsibility to arrange for this, by calling os.Exit with\nthe correct code. The correct code is returned by (*testing.M).Run, so\nthe usual way of implementing TestMain is to end it with\nos.Exit(m.Run()).\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA3001\"",
+ "Doc": "Assigning to b.N in benchmarks distorts the results\n\nThe testing package dynamically sets b.N to improve the reliability of\nbenchmarks and uses it in computations to determine the duration of a\nsingle operation. Benchmark code must not alter b.N as this would\nfalsify results.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4000\"",
+ "Doc": "Binary operator has identical expressions on both sides\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4001\"",
+ "Doc": "\u0026*x gets simplified to x, it does not copy x\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4003\"",
+ "Doc": "Comparing unsigned values against negative values is pointless\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4004\"",
+ "Doc": "The loop exits unconditionally after one iteration\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4005\"",
+ "Doc": "Field assignment that will never be observed. Did you mean to use a pointer receiver?\n\nAvailable since\n 2021.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4006\"",
+ "Doc": "A value assigned to a variable is never read before being overwritten. Forgotten error check or dead code?\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4008\"",
+ "Doc": "The variable in the loop condition never changes, are you incrementing the wrong variable?\n\nFor example:\n\n\tfor i := 0; i \u003c 10; j++ { ... }\n\nThis may also occur when a loop can only execute once because of unconditional\ncontrol flow that terminates the loop. For example, when a loop body contains an\nunconditional break, return, or panic:\n\n\tfunc f() {\n\t\tpanic(\"oops\")\n\t}\n\tfunc g() {\n\t\tfor i := 0; i \u003c 10; i++ {\n\t\t\t// f unconditionally calls panic, which means \"i\" is\n\t\t\t// never incremented.\n\t\t\tf()\n\t\t}\n\t}\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4009\"",
+ "Doc": "A function argument is overwritten before its first use\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4010\"",
+ "Doc": "The result of append will never be observed anywhere\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4011\"",
+ "Doc": "Break statement with no effect. Did you mean to break out of an outer loop?\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4012\"",
+ "Doc": "Comparing a value against NaN even though no value is equal to NaN\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4013\"",
+ "Doc": "Negating a boolean twice (!!b) is the same as writing b. This is either redundant, or a typo.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4014\"",
+ "Doc": "An if/else if chain has repeated conditions and no side-effects; if the condition didn't match the first time, it won't match the second time, either\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4015\"",
+ "Doc": "Calling functions like math.Ceil on floats converted from integers doesn't do anything useful\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4016\"",
+ "Doc": "Certain bitwise operations, such as x ^ 0, do not do anything useful\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4017\"",
+ "Doc": "Discarding the return values of a function without side effects, making the call pointless\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4018\"",
+ "Doc": "Self-assignment of variables\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4019\"",
+ "Doc": "Multiple, identical build constraints in the same file\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4020\"",
+ "Doc": "Unreachable case clause in a type switch\n\nIn a type switch like the following\n\n type T struct{}\n func (T) Read(b []byte) (int, error) { return 0, nil }\n\n var v interface{} = T{}\n\n switch v.(type) {\n case io.Reader:\n // ...\n case T:\n // unreachable\n }\n\nthe second case clause can never be reached because T implements\nio.Reader and case clauses are evaluated in source order.\n\nAnother example:\n\n type T struct{}\n func (T) Read(b []byte) (int, error) { return 0, nil }\n func (T) Close() error { return nil }\n\n var v interface{} = T{}\n\n switch v.(type) {\n case io.Reader:\n // ...\n case io.ReadCloser:\n // unreachable\n }\n\nEven though T has a Close method and thus implements io.ReadCloser,\nio.Reader will always match first. The method set of io.Reader is a\nsubset of io.ReadCloser. Thus it is impossible to match the second\ncase without matching the first case.\n\n\nStructurally equivalent interfaces\n\nA special case of the previous example are structurally identical\ninterfaces. Given these declarations\n\n type T error\n type V error\n\n func doSomething() error {\n err, ok := doAnotherThing()\n if ok {\n return T(err)\n }\n\n return U(err)\n }\n\nthe following type switch will have an unreachable case clause:\n\n switch doSomething().(type) {\n case T:\n // ...\n case V:\n // unreachable\n }\n\nT will always match before V because they are structurally equivalent\nand therefore doSomething()'s return value implements both.\n\nAvailable since\n 2019.2\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4022\"",
+ "Doc": "Comparing the address of a variable against nil\n\nCode such as 'if \u0026x == nil' is meaningless, because taking the address of a variable always yields a non-nil pointer.\n\nAvailable since\n 2020.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4023\"",
+ "Doc": "Impossible comparison of interface value with untyped nil\n\nUnder the covers, interfaces are implemented as two elements, a\ntype T and a value V. V is a concrete value such as an int,\nstruct or pointer, never an interface itself, and has type T. For\ninstance, if we store the int value 3 in an interface, the\nresulting interface value has, schematically, (T=int, V=3). The\nvalue V is also known as the interface's dynamic value, since a\ngiven interface variable might hold different values V (and\ncorresponding types T) during the execution of the program.\n\nAn interface value is nil only if the V and T are both\nunset, (T=nil, V is not set), In particular, a nil interface will\nalways hold a nil type. If we store a nil pointer of type *int\ninside an interface value, the inner type will be *int regardless\nof the value of the pointer: (T=*int, V=nil). Such an interface\nvalue will therefore be non-nil even when the pointer value V\ninside is nil.\n\nThis situation can be confusing, and arises when a nil value is\nstored inside an interface value such as an error return:\n\n func returnsError() error {\n var p *MyError = nil\n if bad() {\n p = ErrBad\n }\n return p // Will always return a non-nil error.\n }\n\nIf all goes well, the function returns a nil p, so the return\nvalue is an error interface value holding (T=*MyError, V=nil).\nThis means that if the caller compares the returned error to nil,\nit will always look as if there was an error even if nothing bad\nhappened. To return a proper nil error to the caller, the\nfunction must return an explicit nil:\n\n func returnsError() error {\n if bad() {\n return ErrBad\n }\n return nil\n }\n\nIt's a good idea for functions that return errors always to use\nthe error type in their signature (as we did above) rather than a\nconcrete type such as *MyError, to help guarantee the error is\ncreated correctly. As an example, os.Open returns an error even\nthough, if not nil, it's always of concrete type *os.PathError.\n\nSimilar situations to those described here can arise whenever\ninterfaces are used. Just keep in mind that if any concrete value\nhas been stored in the interface, the interface will not be nil.\nFor more information, see The Laws of\nReflection at https://golang.org/doc/articles/laws_of_reflection.html.\n\nThis text has been copied from\nhttps://golang.org/doc/faq#nil_error, licensed under the Creative\nCommons Attribution 3.0 License.\n\nAvailable since\n 2020.2\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4024\"",
+ "Doc": "Checking for impossible return value from a builtin function\n\nReturn values of the len and cap builtins cannot be negative.\n\nSee https://golang.org/pkg/builtin/#len and https://golang.org/pkg/builtin/#cap.\n\nExample:\n\n if len(slice) \u003c 0 {\n fmt.Println(\"unreachable code\")\n }\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4025\"",
+ "Doc": "Integer division of literals that results in zero\n\nWhen dividing two integer constants, the result will\nalso be an integer. Thus, a division such as 2 / 3 results in 0.\nThis is true for all of the following examples:\n\n\t_ = 2 / 3\n\tconst _ = 2 / 3\n\tconst _ float64 = 2 / 3\n\t_ = float64(2 / 3)\n\nStaticcheck will flag such divisions if both sides of the division are\ninteger literals, as it is highly unlikely that the division was\nintended to truncate to zero. Staticcheck will not flag integer\ndivision involving named constants, to avoid noisy positives.\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4026\"",
+ "Doc": "Go constants cannot express negative zero\n\nIn IEEE 754 floating point math, zero has a sign and can be positive\nor negative. This can be useful in certain numerical code.\n\nGo constants, however, cannot express negative zero. This means that\nthe literals -0.0 and 0.0 have the same ideal value (zero) and\nwill both represent positive zero at runtime.\n\nTo explicitly and reliably create a negative zero, you can use the\nmath.Copysign function: math.Copysign(0, -1).\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4027\"",
+ "Doc": "(*net/url.URL).Query returns a copy, modifying it doesn't change the URL\n\n(*net/url.URL).Query parses the current value of net/url.URL.RawQuery\nand returns it as a map of type net/url.Values. Subsequent changes to\nthis map will not affect the URL unless the map gets encoded and\nassigned to the URL's RawQuery.\n\nAs a consequence, the following code pattern is an expensive no-op:\nu.Query().Add(key, value).\n\nAvailable since\n 2021.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4028\"",
+ "Doc": "x % 1 is always zero\n\nAvailable since\n 2022.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4029\"",
+ "Doc": "Ineffective attempt at sorting slice\n\nsort.Float64Slice, sort.IntSlice, and sort.StringSlice are\ntypes, not functions. Doing x = sort.StringSlice(x) does nothing,\nespecially not sort any values. The correct usage is\nsort.Sort(sort.StringSlice(x)) or sort.StringSlice(x).Sort(),\nbut there are more convenient helpers, namely sort.Float64s,\nsort.Ints, and sort.Strings.\n\nAvailable since\n 2022.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4030\"",
+ "Doc": "Ineffective attempt at generating random number\n\nFunctions in the math/rand package that accept upper limits, such\nas Intn, generate random numbers in the half-open interval [0,n). In\nother words, the generated numbers will be \u003e= 0 and \u003c n – they\ndon't include n. rand.Intn(1) therefore doesn't generate 0\nor 1, it always generates 0.\n\nAvailable since\n 2022.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4031\"",
+ "Doc": "Checking never-nil value against nil\n\nAvailable since\n 2022.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA4032\"",
+ "Doc": "Comparing runtime.GOOS or runtime.GOARCH against impossible value\n\nAvailable since\n 2024.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5000\"",
+ "Doc": "Assignment to nil map\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5001\"",
+ "Doc": "Deferring Close before checking for a possible error\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5002\"",
+ "Doc": "The empty for loop ('for {}') spins and can block the scheduler\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5003\"",
+ "Doc": "Defers in infinite loops will never execute\n\nDefers are scoped to the surrounding function, not the surrounding\nblock. In a function that never returns, i.e. one containing an\ninfinite loop, defers will never execute.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5004\"",
+ "Doc": "'for { select { ...' with an empty default branch spins\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5005\"",
+ "Doc": "The finalizer references the finalized object, preventing garbage collection\n\nA finalizer is a function associated with an object that runs when the\ngarbage collector is ready to collect said object, that is when the\nobject is no longer referenced by anything.\n\nIf the finalizer references the object, however, it will always remain\nas the final reference to that object, preventing the garbage\ncollector from collecting the object. The finalizer will never run,\nand the object will never be collected, leading to a memory leak. That\nis why the finalizer should instead use its first argument to operate\non the object. That way, the number of references can temporarily go\nto zero before the object is being passed to the finalizer.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5007\"",
+ "Doc": "Infinite recursive call\n\nA function that calls itself recursively needs to have an exit\ncondition. Otherwise it will recurse forever, until the system runs\nout of memory.\n\nThis issue can be caused by simple bugs such as forgetting to add an\nexit condition. It can also happen \"on purpose\". Some languages have\ntail call optimization which makes certain infinite recursive calls\nsafe to use. Go, however, does not implement TCO, and as such a loop\nshould be used instead.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5008\"",
+ "Doc": "Invalid struct tag\n\nAvailable since\n 2019.2\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5010\"",
+ "Doc": "Impossible type assertion\n\nSome type assertions can be statically proven to be\nimpossible. This is the case when the method sets of both\narguments of the type assertion conflict with each other, for\nexample by containing the same method with different\nsignatures.\n\nThe Go compiler already applies this check when asserting from an\ninterface value to a concrete type. If the concrete type misses\nmethods from the interface, or if function signatures don't match,\nthen the type assertion can never succeed.\n\nThis check applies the same logic when asserting from one interface to\nanother. If both interface types contain the same method but with\ndifferent signatures, then the type assertion can never succeed,\neither.\n\nAvailable since\n 2020.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5011\"",
+ "Doc": "Possible nil pointer dereference\n\nA pointer is being dereferenced unconditionally, while\nalso being checked against nil in another place. This suggests that\nthe pointer may be nil and dereferencing it may panic. This is\ncommonly a result of improperly ordered code or missing return\nstatements. Consider the following examples:\n\n func fn(x *int) {\n fmt.Println(*x)\n\n // This nil check is equally important for the previous dereference\n if x != nil {\n foo(*x)\n }\n }\n\n func TestFoo(t *testing.T) {\n x := compute()\n if x == nil {\n t.Errorf(\"nil pointer received\")\n }\n\n // t.Errorf does not abort the test, so if x is nil, the next line will panic.\n foo(*x)\n }\n\nStaticcheck tries to deduce which functions abort control flow.\nFor example, it is aware that a function will not continue\nexecution after a call to panic or log.Fatal. However, sometimes\nthis detection fails, in particular in the presence of\nconditionals. Consider the following example:\n\n func Log(msg string, level int) {\n fmt.Println(msg)\n if level == levelFatal {\n os.Exit(1)\n }\n }\n\n func Fatal(msg string) {\n Log(msg, levelFatal)\n }\n\n func fn(x *int) {\n if x == nil {\n Fatal(\"unexpected nil pointer\")\n }\n fmt.Println(*x)\n }\n\nStaticcheck will flag the dereference of x, even though it is perfectly\nsafe. Staticcheck is not able to deduce that a call to\nFatal will exit the program. For the time being, the easiest\nworkaround is to modify the definition of Fatal like so:\n\n func Fatal(msg string) {\n Log(msg, levelFatal)\n panic(\"unreachable\")\n }\n\nWe also hard-code functions from common logging packages such as\nlogrus. Please file an issue if we're missing support for a\npopular package.\n\nAvailable since\n 2020.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA5012\"",
+ "Doc": "Passing odd-sized slice to function expecting even size\n\nSome functions that take slices as parameters expect the slices to have an even number of elements. \nOften, these functions treat elements in a slice as pairs. \nFor example, strings.NewReplacer takes pairs of old and new strings, \nand calling it with an odd number of elements would be an error.\n\nAvailable since\n 2020.2\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA6000\"",
+ "Doc": "Using regexp.Match or related in a loop, should use regexp.Compile\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA6001\"",
+ "Doc": "Missing an optimization opportunity when indexing maps by byte slices\n\nMap keys must be comparable, which precludes the use of byte slices.\nThis usually leads to using string keys and converting byte slices to\nstrings.\n\nNormally, a conversion of a byte slice to a string needs to copy the data and\ncauses allocations. The compiler, however, recognizes m[string(b)] and\nuses the data of b directly, without copying it, because it knows that\nthe data can't change during the map lookup. This leads to the\ncounter-intuitive situation that\n\n k := string(b)\n println(m[k])\n println(m[k])\n\nwill be less efficient than\n\n println(m[string(b)])\n println(m[string(b)])\n\nbecause the first version needs to copy and allocate, while the second\none does not.\n\nFor some history on this optimization, check out commit\nf5f5a8b6209f84961687d993b93ea0d397f5d5bf in the Go repository.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA6002\"",
+ "Doc": "Storing non-pointer values in sync.Pool allocates memory\n\nA sync.Pool is used to avoid unnecessary allocations and reduce the\namount of work the garbage collector has to do.\n\nWhen passing a value that is not a pointer to a function that accepts\nan interface, the value needs to be placed on the heap, which means an\nadditional allocation. Slices are a common thing to put in sync.Pools,\nand they're structs with 3 fields (length, capacity, and a pointer to\nan array). In order to avoid the extra allocation, one should store a\npointer to the slice instead.\n\nSee the comments on https://go-review.googlesource.com/c/go/+/24371\nthat discuss this problem.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA6003\"",
+ "Doc": "Converting a string to a slice of runes before ranging over it\n\nYou may want to loop over the runes in a string. Instead of converting\nthe string to a slice of runes and looping over that, you can loop\nover the string itself. That is,\n\n for _, r := range s {}\n\nand\n\n for _, r := range []rune(s) {}\n\nwill yield the same values. The first version, however, will be faster\nand avoid unnecessary memory allocations.\n\nDo note that if you are interested in the indices, ranging over a\nstring and over a slice of runes will yield different indices. The\nfirst one yields byte offsets, while the second one yields indices in\nthe slice of runes.\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA6005\"",
+ "Doc": "Inefficient string comparison with strings.ToLower or strings.ToUpper\n\nConverting two strings to the same case and comparing them like so\n\n if strings.ToLower(s1) == strings.ToLower(s2) {\n ...\n }\n\nis significantly more expensive than comparing them with\nstrings.EqualFold(s1, s2). This is due to memory usage as well as\ncomputational complexity.\n\nstrings.ToLower will have to allocate memory for the new strings, as\nwell as convert both strings fully, even if they differ on the very\nfirst byte. strings.EqualFold, on the other hand, compares the strings\none character at a time. It doesn't need to create two intermediate\nstrings and can return as soon as the first non-matching character has\nbeen found.\n\nFor a more in-depth explanation of this issue, see\nhttps://blog.digitalocean.com/how-to-efficiently-compare-strings-in-go/\n\nAvailable since\n 2019.2\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA6006\"",
+ "Doc": "Using io.WriteString to write []byte\n\nUsing io.WriteString to write a slice of bytes, as in\n\n io.WriteString(w, string(b))\n\nis both unnecessary and inefficient. Converting from []byte to string\nhas to allocate and copy the data, and we could simply use w.Write(b)\ninstead.\n\nAvailable since\n 2024.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9001\"",
+ "Doc": "Defers in range loops may not run when you expect them to\n\nAvailable since\n 2017.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9002\"",
+ "Doc": "Using a non-octal os.FileMode that looks like it was meant to be in octal.\n\nAvailable since\n 2017.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9003\"",
+ "Doc": "Empty body in an if or else branch\n\nAvailable since\n 2017.1, non-default\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9004\"",
+ "Doc": "Only the first constant has an explicit type\n\nIn a constant declaration such as the following:\n\n const (\n First byte = 1\n Second = 2\n )\n\nthe constant Second does not have the same type as the constant First.\nThis construct shouldn't be confused with\n\n const (\n First byte = iota\n Second\n )\n\nwhere First and Second do indeed have the same type. The type is only\npassed on when no explicit value is assigned to the constant.\n\nWhen declaring enumerations with explicit values it is therefore\nimportant not to write\n\n const (\n EnumFirst EnumType = 1\n EnumSecond = 2\n EnumThird = 3\n )\n\nThis discrepancy in types can cause various confusing behaviors and\nbugs.\n\n\nWrong type in variable declarations\n\nThe most obvious issue with such incorrect enumerations expresses\nitself as a compile error:\n\n package pkg\n\n const (\n EnumFirst uint8 = 1\n EnumSecond = 2\n )\n\n func fn(useFirst bool) {\n x := EnumSecond\n if useFirst {\n x = EnumFirst\n }\n }\n\nfails to compile with\n\n ./const.go:11:5: cannot use EnumFirst (type uint8) as type int in assignment\n\n\nLosing method sets\n\nA more subtle issue occurs with types that have methods and optional\ninterfaces. Consider the following:\n\n package main\n\n import \"fmt\"\n\n type Enum int\n\n func (e Enum) String() string {\n return \"an enum\"\n }\n\n const (\n EnumFirst Enum = 1\n EnumSecond = 2\n )\n\n func main() {\n fmt.Println(EnumFirst)\n fmt.Println(EnumSecond)\n }\n\nThis code will output\n\n an enum\n 2\n\nas EnumSecond has no explicit type, and thus defaults to int.\n\nAvailable since\n 2019.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9005\"",
+ "Doc": "Trying to marshal a struct with no public fields nor custom marshaling\n\nThe encoding/json and encoding/xml packages only operate on exported\nfields in structs, not unexported ones. It is usually an error to try\nto (un)marshal structs that only consist of unexported fields.\n\nThis check will not flag calls involving types that define custom\nmarshaling behavior, e.g. via MarshalJSON methods. It will also not\nflag empty structs.\n\nAvailable since\n 2019.2\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9006\"",
+ "Doc": "Dubious bit shifting of a fixed size integer value\n\nBit shifting a value past its size will always clear the value.\n\nFor instance:\n\n v := int8(42)\n v \u003e\u003e= 8\n\nwill always result in 0.\n\nThis check flags bit shifting operations on fixed size integer values only.\nThat is, int, uint and uintptr are never flagged to avoid potential false\npositives in somewhat exotic but valid bit twiddling tricks:\n\n // Clear any value above 32 bits if integers are more than 32 bits.\n func f(i int) int {\n v := i \u003e\u003e 32\n v = v \u003c\u003c 32\n return i-v\n }\n\nAvailable since\n 2020.2\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9007\"",
+ "Doc": "Deleting a directory that shouldn't be deleted\n\nIt is virtually never correct to delete system directories such as\n/tmp or the user's home directory. However, it can be fairly easy to\ndo by mistake, for example by mistakenly using os.TempDir instead\nof ioutil.TempDir, or by forgetting to add a suffix to the result\nof os.UserHomeDir.\n\nWriting\n\n d := os.TempDir()\n defer os.RemoveAll(d)\n\nin your unit tests will have a devastating effect on the stability of your system.\n\nThis check flags attempts at deleting the following directories:\n\n- os.TempDir\n- os.UserCacheDir\n- os.UserConfigDir\n- os.UserHomeDir\n\nAvailable since\n 2022.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9008\"",
+ "Doc": "else branch of a type assertion is probably not reading the right value\n\nWhen declaring variables as part of an if statement (like in 'if\nfoo := ...; foo {'), the same variables will also be in the scope of\nthe else branch. This means that in the following example\n\n if x, ok := x.(int); ok {\n // ...\n } else {\n fmt.Printf(\"unexpected type %T\", x)\n }\n\nx in the else branch will refer to the x from x, ok\n:=; it will not refer to the x that is being type-asserted. The\nresult of a failed type assertion is the zero value of the type that\nis being asserted to, so x in the else branch will always have the\nvalue 0 and the type int.\n\nAvailable since\n 2022.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"SA9009\"",
+ "Doc": "Ineffectual Go compiler directive\n\nA potential Go compiler directive was found, but is ineffectual as it begins\nwith whitespace.\n\nAvailable since\n 2024.1\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1000\"",
+ "Doc": "Incorrect or missing package comment\n\nPackages must have a package comment that is formatted according to\nthe guidelines laid out in\nhttps://go.dev/wiki/CodeReviewComments#package-comments.\n\nAvailable since\n 2019.1, non-default\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1001\"",
+ "Doc": "Dot imports are discouraged\n\nDot imports that aren't in external test packages are discouraged.\n\nThe dot_import_whitelist option can be used to whitelist certain\nimports.\n\nQuoting Go Code Review Comments:\n\n\u003e The import . form can be useful in tests that, due to circular\n\u003e dependencies, cannot be made part of the package being tested:\n\u003e \n\u003e package foo_test\n\u003e \n\u003e import (\n\u003e \"bar/testutil\" // also imports \"foo\"\n\u003e . \"foo\"\n\u003e )\n\u003e \n\u003e In this case, the test file cannot be in package foo because it\n\u003e uses bar/testutil, which imports foo. So we use the import .\n\u003e form to let the file pretend to be part of package foo even though\n\u003e it is not. Except for this one case, do not use import . in your\n\u003e programs. It makes the programs much harder to read because it is\n\u003e unclear whether a name like Quux is a top-level identifier in the\n\u003e current package or in an imported package.\n\nAvailable since\n 2019.1\n\nOptions\n dot_import_whitelist\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1003\"",
+ "Doc": "Poorly chosen identifier\n\nIdentifiers, such as variable and package names, follow certain rules.\n\nSee the following links for details:\n\n- https://go.dev/doc/effective_go#package-names\n- https://go.dev/doc/effective_go#mixed-caps\n- https://go.dev/wiki/CodeReviewComments#initialisms\n- https://go.dev/wiki/CodeReviewComments#variable-names\n\nAvailable since\n 2019.1, non-default\n\nOptions\n initialisms\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1005\"",
+ "Doc": "Incorrectly formatted error string\n\nError strings follow a set of guidelines to ensure uniformity and good\ncomposability.\n\nQuoting Go Code Review Comments:\n\n\u003e Error strings should not be capitalized (unless beginning with\n\u003e proper nouns or acronyms) or end with punctuation, since they are\n\u003e usually printed following other context. That is, use\n\u003e fmt.Errorf(\"something bad\") not fmt.Errorf(\"Something bad\"), so\n\u003e that log.Printf(\"Reading %s: %v\", filename, err) formats without a\n\u003e spurious capital letter mid-message.\n\nAvailable since\n 2019.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1006\"",
+ "Doc": "Poorly chosen receiver name\n\nQuoting Go Code Review Comments:\n\n\u003e The name of a method's receiver should be a reflection of its\n\u003e identity; often a one or two letter abbreviation of its type\n\u003e suffices (such as \"c\" or \"cl\" for \"Client\"). Don't use generic\n\u003e names such as \"me\", \"this\" or \"self\", identifiers typical of\n\u003e object-oriented languages that place more emphasis on methods as\n\u003e opposed to functions. The name need not be as descriptive as that\n\u003e of a method argument, as its role is obvious and serves no\n\u003e documentary purpose. It can be very short as it will appear on\n\u003e almost every line of every method of the type; familiarity admits\n\u003e brevity. Be consistent, too: if you call the receiver \"c\" in one\n\u003e method, don't call it \"cl\" in another.\n\nAvailable since\n 2019.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1008\"",
+ "Doc": "A function's error value should be its last return value\n\nA function's error value should be its last return value.\n\nAvailable since\n 2019.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1011\"",
+ "Doc": "Poorly chosen name for variable of type time.Duration\n\ntime.Duration values represent an amount of time, which is represented\nas a count of nanoseconds. An expression like 5 * time.Microsecond\nyields the value 5000. It is therefore not appropriate to suffix a\nvariable of type time.Duration with any time unit, such as Msec or\nMilli.\n\nAvailable since\n 2019.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1012\"",
+ "Doc": "Poorly chosen name for error variable\n\nError variables that are part of an API should be called errFoo or\nErrFoo.\n\nAvailable since\n 2019.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1013\"",
+ "Doc": "Should use constants for HTTP error codes, not magic numbers\n\nHTTP has a tremendous number of status codes. While some of those are\nwell known (200, 400, 404, 500), most of them are not. The net/http\npackage provides constants for all status codes that are part of the\nvarious specifications. It is recommended to use these constants\ninstead of hard-coding magic numbers, to vastly improve the\nreadability of your code.\n\nAvailable since\n 2019.1\n\nOptions\n http_status_code_whitelist\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1015\"",
+ "Doc": "A switch's default case should be the first or last case\n\nAvailable since\n 2019.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1016\"",
+ "Doc": "Use consistent method receiver names\n\nAvailable since\n 2019.1, non-default\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1017\"",
+ "Doc": "Don't use Yoda conditions\n\nYoda conditions are conditions of the kind 'if 42 == x', where the\nliteral is on the left side of the comparison. These are a common\nidiom in languages in which assignment is an expression, to avoid bugs\nof the kind 'if (x = 42)'. In Go, which doesn't allow for this kind of\nbug, we prefer the more idiomatic 'if x == 42'.\n\nAvailable since\n 2019.2\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1018\"",
+ "Doc": "Avoid zero-width and control characters in string literals\n\nAvailable since\n 2019.2\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1019\"",
+ "Doc": "Importing the same package multiple times\n\nGo allows importing the same package multiple times, as long as\ndifferent import aliases are being used. That is, the following\nbit of code is valid:\n\n import (\n \"fmt\"\n fumpt \"fmt\"\n format \"fmt\"\n _ \"fmt\"\n )\n\nHowever, this is very rarely done on purpose. Usually, it is a\nsign of code that got refactored, accidentally adding duplicate\nimport statements. It is also a rarely known feature, which may\ncontribute to confusion.\n\nDo note that sometimes, this feature may be used\nintentionally (see for example\nhttps://github.com/golang/go/commit/3409ce39bfd7584523b7a8c150a310cea92d879d)\n– if you want to allow this pattern in your code base, you're\nadvised to disable this check.\n\nAvailable since\n 2020.1\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1020\"",
+ "Doc": "The documentation of an exported function should start with the function's name\n\nDoc comments work best as complete sentences, which\nallow a wide variety of automated presentations. The first sentence\nshould be a one-sentence summary that starts with the name being\ndeclared.\n\nIf every doc comment begins with the name of the item it describes,\nyou can use the doc subcommand of the go tool and run the output\nthrough grep.\n\nSee https://go.dev/doc/effective_go#commentary for more\ninformation on how to write good documentation.\n\nAvailable since\n 2020.1, non-default\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1021\"",
+ "Doc": "The documentation of an exported type should start with type's name\n\nDoc comments work best as complete sentences, which\nallow a wide variety of automated presentations. The first sentence\nshould be a one-sentence summary that starts with the name being\ndeclared.\n\nIf every doc comment begins with the name of the item it describes,\nyou can use the doc subcommand of the go tool and run the output\nthrough grep.\n\nSee https://go.dev/doc/effective_go#commentary for more\ninformation on how to write good documentation.\n\nAvailable since\n 2020.1, non-default\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1022\"",
+ "Doc": "The documentation of an exported variable or constant should start with variable's name\n\nDoc comments work best as complete sentences, which\nallow a wide variety of automated presentations. The first sentence\nshould be a one-sentence summary that starts with the name being\ndeclared.\n\nIf every doc comment begins with the name of the item it describes,\nyou can use the doc subcommand of the go tool and run the output\nthrough grep.\n\nSee https://go.dev/doc/effective_go#commentary for more\ninformation on how to write good documentation.\n\nAvailable since\n 2020.1, non-default\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"ST1023\"",
+ "Doc": "Redundant type in variable declaration\n\nAvailable since\n 2021.1, non-default\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"appends\"",
+ "Doc": "check for missing values after append\n\nThis checker reports calls to append that pass\nno values to be appended to the slice.\n\n\ts := []string{\"a\", \"b\", \"c\"}\n\t_ = append(s)\n\nSuch calls are always no-ops and often indicate an\nunderlying mistake.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"asmdecl\"",
+ "Doc": "report mismatches between assembly files and Go declarations",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"assign\"",
+ "Doc": "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"atomic\"",
+ "Doc": "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(\u0026x, 1)\n\nwhich are not atomic.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"atomicalign\"",
+ "Doc": "check for non-64-bits-aligned arguments to sync/atomic functions",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"bools\"",
+ "Doc": "check for common mistakes involving boolean operators",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"buildtag\"",
+ "Doc": "check //go:build and // +build directives",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"cgocall\"",
+ "Doc": "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"composites\"",
+ "Doc": "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = \u0026net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = \u0026net.DNSConfigError{Err: err}\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"copylocks\"",
+ "Doc": "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"deepequalerrors\"",
+ "Doc": "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"defers\"",
+ "Doc": "report common mistakes in defer statements\n\nThe defers analyzer reports a diagnostic when a defer statement would\nresult in a non-deferred call to time.Since, as experience has shown\nthat this is nearly always a mistake.\n\nFor example:\n\n\tstart := time.Now()\n\t...\n\tdefer recordLatency(time.Since(start)) // error: call to time.Since is not deferred\n\nThe correct code is:\n\n\tdefer func() { recordLatency(time.Since(start)) }()",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"deprecated\"",
+ "Doc": "check for use of deprecated identifiers\n\nThe deprecated analyzer looks for deprecated symbols and package\nimports.\n\nSee https://go.dev/wiki/Deprecated to learn about Go's convention\nfor documenting and signaling deprecated identifiers.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"directive\"",
+ "Doc": "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"embed\"",
+ "Doc": "check //go:embed directive usage\n\nThis analyzer checks that the embed package is imported if //go:embed\ndirectives are present, providing a suggested fix to add the import if\nit is missing.\n\nThis analyzer also checks that //go:embed directives precede the\ndeclaration of a single variable.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"errorsas\"",
+ "Doc": "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"fillreturns\"",
+ "Doc": "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"framepointer\"",
+ "Doc": "report assembly that clobbers the frame pointer before saving it",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"gofix\"",
+ "Doc": "apply fixes based on go:fix comment directives\n\nThe gofix analyzer inlines functions and constants that are marked for inlining.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"hostport\"",
+ "Doc": "check format of addresses passed to net.Dial\n\nThis analyzer flags code that produce network address strings using\nfmt.Sprintf, as in this example:\n\n addr := fmt.Sprintf(\"%s:%d\", host, 12345) // \"will not work with IPv6\"\n ...\n conn, err := net.Dial(\"tcp\", addr) // \"when passed to dial here\"\n\nThe analyzer suggests a fix to use the correct approach, a call to\nnet.JoinHostPort:\n\n addr := net.JoinHostPort(host, \"12345\")\n ...\n conn, err := net.Dial(\"tcp\", addr)\n\nA similar diagnostic and fix are produced for a format string of \"%s:%s\".\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"httpresponse\"",
+ "Doc": "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"ifaceassert\"",
+ "Doc": "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"infertypeargs\"",
+ "Doc": "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"loopclosure\"",
+ "Doc": "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nNote: An iteration variable can only outlive a loop iteration in Go versions \u003c=1.21.\nIn Go 1.22 and later, the loop variable lifetimes changed to create a new\niteration variable per loop iteration. (See go.dev/issue/60078.)\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v [\u003cgo1.22].\n\n\tfor _, v := range list {\n\t defer func() {\n\t use(v) // incorrect\n\t }()\n\t}\n\nOne fix is to create a new variable for each iteration of the loop:\n\n\tfor _, v := range list {\n\t v := v // new var per iteration\n\t defer func() {\n\t use(v) // ok\n\t }()\n\t}\n\nAfter Go version 1.22, the previous two for loops are equivalent\nand both are correct.\n\nThe next example uses a go statement and has a similar problem [\u003cgo1.22].\nIn addition, it has a data race because the loop updates v\nconcurrent with the goroutines accessing it.\n\n\tfor _, v := range elem {\n\t go func() {\n\t use(v) // incorrect, and a data race\n\t }()\n\t}\n\nA fix is the same as before. The checker also reports problems\nin goroutines started by golang.org/x/sync/errgroup.Group.\nA hard-to-spot variant of this form is common in parallel tests:\n\n\tfunc Test(t *testing.T) {\n\t for _, test := range tests {\n\t t.Run(test.name, func(t *testing.T) {\n\t t.Parallel()\n\t use(test) // incorrect, and a data race\n\t })\n\t }\n\t}\n\nThe t.Parallel() call causes the rest of the function to execute\nconcurrent with the loop [\u003cgo1.22].\n\nThe analyzer reports references only in the last statement,\nas it is not deep enough to understand the effects of subsequent\nstatements that might render the reference benign.\n(\"Last statement\" is defined recursively in compound\nstatements such as if, switch, and select.)\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"lostcancel\"",
+ "Doc": "check cancel func returned by context.WithCancel is called\n\nThe cancellation function returned by context.WithCancel, WithTimeout,\nWithDeadline and variants such as WithCancelCause must be called,\nor the new context will remain live until its parent context is cancelled.\n(The background context is never cancelled.)",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"modernize\"",
+ "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go and its standard\nlibrary.\n\nEach diagnostic provides a fix. Our intent is that these fixes may\nbe safely applied en masse without changing the behavior of your\nprogram. In some cases the suggested fixes are imperfect and may\nlead to (for example) unused imports or unused local variables,\ncausing build breakage. However, these problems are generally\ntrivial to fix. We regard any modernizer whose fix changes program\nbehavior to have a serious bug and will endeavor to fix it.\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...\n\n(Do not use \"go get -tool\" to add gopls as a dependency of your\nmodule; gopls commands must be built from their release branch.)\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.\n\nChanges produced by this tool should be reviewed as usual before\nbeing merged. In some cases, a loop may be replaced by a simple\nfunction call, causing comments within the loop to be discarded.\nHuman judgment may be required to avoid losing comments of value.\n\nEach diagnostic reported by modernize has a specific category. (The\ncategories are listed below.) Diagnostics in some categories, such\nas \"efaceany\" (which replaces \"interface{}\" with \"any\" where it is\nsafe to do so) are particularly numerous. It may ease the burden of\ncode review to apply fixes in two passes, the first change\nconsisting only of fixes of category \"efaceany\", the second\nconsisting of all others. This can be achieved using the -category flag:\n\n\t$ modernize -category=efaceany -fix -test ./...\n\t$ modernize -category=-efaceany -fix -test ./...\n\nCategories of modernize diagnostic:\n\n - forvar: remove x := x variable declarations made unnecessary by the new semantics of loops in go1.22.\n\n - slicescontains: replace 'for i, elem := range s { if elem == needle { ...; break }'\n by a call to slices.Contains, added in go1.21.\n\n - minmax: replace an if/else conditional assignment by a call to\n the built-in min or max functions added in go1.21.\n\n - sortslice: replace sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21.\n\n - efaceany: replace interface{} by the 'any' type added in go1.18.\n\n - slicesclone: replace append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21.\n\n - mapsloop: replace a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions from\n the maps package, added in go1.21.\n\n - fmtappendf: replace []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19.\n\n - testingcontext: replace uses of context.WithCancel in tests\n with t.Context, added in go1.24.\n\n - omitzero: replace omitempty by omitzero on structs, added in go1.24.\n\n - bloop: replace \"for i := range b.N\" or \"for range b.N\" in a\n benchmark with \"for b.Loop()\", and remove any preceding calls\n to b.StopTimer, b.StartTimer, and b.ResetTimer.\n\n - slicesdelete: replace append(s[:i], s[i+1]...) by\n slices.Delete(s, i, i+1), added in go1.21.\n\n - rangeint: replace a 3-clause \"for i := 0; i \u003c n; i++\" loop by\n \"for i := range n\", added in go1.22.\n\n - stringsseq: replace Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq, or Fields with FieldSeq.\n\n - stringscutprefix: replace some uses of HasPrefix followed by TrimPrefix with CutPrefix,\n added to the strings package in go1.20.\n\n - waitgroup: replace old complex usages of sync.WaitGroup by less complex WaitGroup.Go method in go1.25.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"nilfunc\"",
+ "Doc": "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"nilness\"",
+ "Doc": "check for redundant or impossible nil comparisons\n\nThe nilness checker inspects the control-flow graph of each function in\na package and reports nil pointer dereferences, degenerate nil\npointers, and panics with nil values. A degenerate comparison is of the form\nx==nil or x!=nil where x is statically known to be nil or non-nil. These are\noften a mistake, especially in control flow related to errors. Panics with nil\nvalues are checked because they are not detectable by\n\n\tif r := recover(); r != nil {\n\nThis check reports conditions such as:\n\n\tif f == nil { // impossible condition (f is a function)\n\t}\n\nand:\n\n\tp := \u0026v\n\t...\n\tif p != nil { // tautological condition\n\t}\n\nand:\n\n\tif p == nil {\n\t\tprint(*p) // nil dereference\n\t}\n\nand:\n\n\tif p == nil {\n\t\tpanic(p)\n\t}\n\nSometimes the control flow may be quite complex, making bugs hard\nto spot. In the example below, the err.Error expression is\nguaranteed to panic because, after the first return, err must be\nnil. The intervening loop is just a distraction.\n\n\t...\n\terr := g.Wait()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpartialSuccess := false\n\tfor _, err := range errs {\n\t\tif err == nil {\n\t\t\tpartialSuccess = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif partialSuccess {\n\t\treportStatus(StatusMessage{\n\t\t\tCode: code.ERROR,\n\t\t\tDetail: err.Error(), // \"nil dereference in dynamic method call\"\n\t\t})\n\t\treturn nil\n\t}\n\n...",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"nonewvars\"",
+ "Doc": "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\n\tz := 1\n\tz := 2\n\nwill turn into\n\n\tz := 1\n\tz = 2",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"noresultvalues\"",
+ "Doc": "suggested fixes for unexpected return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\" or \"too many return values\".\nFor example:\n\n\tfunc z() { return nil }\n\nwill turn into\n\n\tfunc z() { return }",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"printf\"",
+ "Doc": "check consistency of Printf format strings and arguments\n\nThe check applies to calls of the formatting functions such as\n[fmt.Printf] and [fmt.Sprintf], as well as any detected wrappers of\nthose functions such as [log.Printf]. It reports a variety of\nmistakes such as syntax errors in the format string and mismatches\n(of number and type) between the verbs and their arguments.\n\nSee the documentation of the fmt package for the complete set of\nformat operators and their operand types.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"shadow\"",
+ "Doc": "check for possible unintended shadowing of variables\n\nThis analyzer check for shadowed variables.\nA shadowed variable is a variable declared in an inner scope\nwith the same name and type as a variable in an outer scope,\nand where the outer variable is mentioned after the inner one\nis declared.\n\n(This definition can be refined; the module generates too many\nfalse positives and is not yet enabled by default.)\n\nFor example:\n\n\tfunc BadRead(f *os.File, buf []byte) error {\n\t\tvar err error\n\t\tfor {\n\t\t\tn, err := f.Read(buf) // shadows the function variable 'err'\n\t\t\tif err != nil {\n\t\t\t\tbreak // causes return of wrong value\n\t\t\t}\n\t\t\tfoo(buf)\n\t\t}\n\t\treturn err\n\t}",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"shift\"",
+ "Doc": "check for shifts that equal or exceed the width of the integer",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"sigchanyzer\"",
+ "Doc": "check for unbuffered channel of os.Signal\n\nThis checker reports call expression of the form\n\n\tsignal.Notify(c \u003c-chan os.Signal, sig ...os.Signal),\n\nwhere c is an unbuffered channel, which can be at risk of missing the signal.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"simplifycompositelit\"",
+ "Doc": "check for composite literal simplifications\n\nAn array, slice, or map composite literal of the form:\n\n\t[]T{T{}, T{}}\n\nwill be simplified to:\n\n\t[]T{{}, {}}\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"simplifyrange\"",
+ "Doc": "check for range statement simplifications\n\nA range of the form:\n\n\tfor x, _ = range v {...}\n\nwill be simplified to:\n\n\tfor x = range v {...}\n\nA range of the form:\n\n\tfor _ = range v {...}\n\nwill be simplified to:\n\n\tfor range v {...}\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"simplifyslice\"",
+ "Doc": "check for slice simplifications\n\nA slice expression of the form:\n\n\ts[a:len(s)]\n\nwill be simplified to:\n\n\ts[a:]\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"slog\"",
+ "Doc": "check for invalid structured logging calls\n\nThe slog checker looks for calls to functions from the log/slog\npackage that take alternating key-value pairs. It reports calls\nwhere an argument in a key position is neither a string nor a\nslog.Attr, and where a final key is missing its value.\nFor example,it would report\n\n\tslog.Warn(\"message\", 11, \"k\") // slog.Warn arg \"11\" should be a string or a slog.Attr\n\nand\n\n\tslog.Info(\"message\", \"k1\", v1, \"k2\") // call to slog.Info missing a final value",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"sortslice\"",
+ "Doc": "check the argument type of sort.Slice\n\nsort.Slice requires an argument of a slice type. Check that\nthe interface{} value passed to sort.Slice is actually a slice.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"stdmethods\"",
+ "Doc": "check signature of methods of well-known interfaces\n\nSometimes a type may be intended to satisfy an interface but may fail to\ndo so because of a mistake in its method signature.\nFor example, the result of this WriteTo method should be (int64, error),\nnot error, to satisfy io.WriterTo:\n\n\ttype myWriterTo struct{...}\n\tfunc (myWriterTo) WriteTo(w io.Writer) error { ... }\n\nThis check ensures that each method whose name matches one of several\nwell-known interface methods from the standard library has the correct\nsignature for that interface.\n\nChecked method names include:\n\n\tFormat GobEncode GobDecode MarshalJSON MarshalXML\n\tPeek ReadByte ReadFrom ReadRune Scan Seek\n\tUnmarshalJSON UnreadByte UnreadRune WriteByte\n\tWriteTo",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"stdversion\"",
+ "Doc": "report uses of too-new standard library symbols\n\nThe stdversion analyzer reports references to symbols in the standard\nlibrary that were introduced by a Go release higher than the one in\nforce in the referring file. (Recall that the file's Go version is\ndefined by the 'go' directive its module's go.mod file, or by a\n\"//go:build go1.X\" build tag at the top of the file.)\n\nThe analyzer does not report a diagnostic for a reference to a \"too\nnew\" field or method of a type that is itself \"too new\", as this may\nhave false positives, for example if fields or methods are accessed\nthrough a type alias that is guarded by a Go version constraint.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"stringintconv\"",
+ "Doc": "check for string(int) conversions\n\nThis checker flags conversions of the form string(x) where x is an integer\n(but not byte or rune) type. Such conversions are discouraged because they\nreturn the UTF-8 representation of the Unicode code point x, and not a decimal\nstring representation of x as one might expect. Furthermore, if x denotes an\ninvalid code point, the conversion cannot be statically rejected.\n\nFor conversions that intend on using the code point, consider replacing them\nwith string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the\nstring representation of the value in the desired base.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"structtag\"",
+ "Doc": "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"testinggoroutine\"",
+ "Doc": "report calls to (*testing.T).Fatal from goroutines started by a test\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t go func() {\n\t t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t }()\n\t}",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"tests\"",
+ "Doc": "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark, Fuzzing and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"timeformat\"",
+ "Doc": "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"unmarshal\"",
+ "Doc": "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"unreachable\"",
+ "Doc": "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by a return statement, a call to panic, an\ninfinite loop, or similar constructs.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"unsafeptr\"",
+ "Doc": "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"unusedfunc\"",
+ "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report false positives in some situations, for\nexample:\n\n - For a declaration of an unexported function that is referenced\n from another package using the go:linkname mechanism, if the\n declaration's doc comment does not also have a go:linkname\n comment.\n\n (Such code is in any case strongly discouraged: linkname\n annotations, if they must be used at all, should be used on both\n the declaration and the alias.)\n\n - For compiler intrinsics in the \"runtime\" package that, though\n never referenced, are known to the compiler and are called\n indirectly by compiled object code.\n\n - For functions called only from assembly.\n\n - For functions called only from files whose build tags are not\n selected in the current build configuration.\n\nSee https://github.com/golang/go/issues/71686 for discussion of\nthese limitations.\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"unusedparams\"",
+ "Doc": "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.\n\nThis analyzer ignores generated code.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"unusedresult\"",
+ "Doc": "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side\neffects, so it is always a mistake to discard the result. Other\nfunctions may return an error that must not be ignored, or a cleanup\noperation that must be called. This analyzer reports calls to\nfunctions like these when the result of the call is ignored.\n\nThe set of functions may be controlled using flags.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"unusedvariable\"",
+ "Doc": "check for unused variables and suggest fixes",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"unusedwrite\"",
+ "Doc": "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"waitgroup\"",
+ "Doc": "check for misuses of sync.WaitGroup\n\nThis analyzer detects mistaken calls to the (*sync.WaitGroup).Add\nmethod from inside a new goroutine, causing Add to race with Wait:\n\n\t// WRONG\n\tvar wg sync.WaitGroup\n\tgo func() {\n\t wg.Add(1) // \"WaitGroup.Add called from inside new goroutine\"\n\t defer wg.Done()\n\t ...\n\t}()\n\twg.Wait() // (may return prematurely before new goroutine starts)\n\nThe correct code calls Add before starting the goroutine:\n\n\t// RIGHT\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t...\n\t}()\n\twg.Wait()",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"yield\"",
+ "Doc": "report calls to yield where the result is ignored\n\nAfter a yield function returns false, the caller should not call\nthe yield function again; generally the iterator should return\npromptly.\n\nThis example fails to check the result of the call to yield,\ncausing this analyzer to report a diagnostic:\n\n\tyield(1) // yield may be called again (on L2) after returning false\n\tyield(2)\n\nThe corrected code is either this:\n\n\tif yield(1) { yield(2) }\n\nor simply:\n\n\t_ = yield(1) \u0026\u0026 yield(2)\n\nIt is not always a mistake to ignore the result of yield.\nFor example, this is a valid single-element iterator:\n\n\tyield(1) // ok to ignore result\n\treturn\n\nIt is only a mistake when the yield call that returned false may be\nfollowed by another call.",
+ "Default": "true",
+ "Status": ""
+ }
+ ]
+ },
+ "EnumValues": null,
+ "Default": "{}",
+ "Status": "",
+ "Hierarchy": "ui.diagnostic",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "staticcheck",
+ "Type": "bool",
+ "Doc": "staticcheck configures the default set of analyses staticcheck.io.\nThese analyses are documented on\n[Staticcheck's website](https://staticcheck.io/docs/checks/).\n\nThe \"staticcheck\" option has three values:\n- false: disable all staticcheck analyzers\n- true: enable all staticcheck analyzers\n- unset: enable a subset of staticcheck analyzers\n selected by gopls maintainers for runtime efficiency\n and analytic precision.\n\nRegardless of this setting, individual analyzers can be\nselectively enabled or disabled using the `analyses` setting.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "false",
+ "Status": "experimental",
+ "Hierarchy": "ui.diagnostic",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "staticcheckProvided",
+ "Type": "bool",
+ "Doc": "",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "false",
+ "Status": "experimental",
+ "Hierarchy": "ui.diagnostic",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "annotations",
+ "Type": "map[enum]bool",
+ "Doc": "annotations specifies the various kinds of compiler\noptimization details that should be reported as diagnostics\nwhen enabled for a package by the \"Toggle compiler\noptimization details\" (`gopls.gc_details`) command.\n\n(Some users care only about one kind of annotation in their\nprofiling efforts. More importantly, in large packages, the\nnumber of annotations can sometimes overwhelm the user\ninterface and exceed the per-file diagnostic limit.)\n\nTODO(adonovan): rename this field to CompilerOptDetail.\n",
+ "EnumKeys": {
+ "ValueType": "bool",
+ "Keys": [
+ {
+ "Name": "\"bounds\"",
+ "Doc": "`\"bounds\"` controls bounds checking diagnostics.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"escape\"",
+ "Doc": "`\"escape\"` controls diagnostics about escape choices.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"inline\"",
+ "Doc": "`\"inline\"` controls diagnostics about inlining choices.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"nil\"",
+ "Doc": "`\"nil\"` controls nil checks.\n",
+ "Default": "true",
+ "Status": ""
+ }
+ ]
+ },
+ "EnumValues": null,
+ "Default": "{\"bounds\":true,\"escape\":true,\"inline\":true,\"nil\":true}",
+ "Status": "",
+ "Hierarchy": "ui.diagnostic",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "vulncheck",
+ "Type": "enum",
+ "Doc": "vulncheck enables vulnerability scanning.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": [
+ {
+ "Value": "\"Imports\"",
+ "Doc": "`\"Imports\"`: In Imports mode, `gopls` will report vulnerabilities that affect packages\ndirectly and indirectly used by the analyzed main module.\n",
+ "Status": ""
+ },
+ {
+ "Value": "\"Off\"",
+ "Doc": "`\"Off\"`: Disable vulnerability analysis.\n",
+ "Status": ""
+ }
+ ],
+ "Default": "\"Off\"",
+ "Status": "experimental",
+ "Hierarchy": "ui.diagnostic",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "diagnosticsDelay",
+ "Type": "time.Duration",
+ "Doc": "diagnosticsDelay controls the amount of time that gopls waits\nafter the most recent file modification before computing deep diagnostics.\nSimple diagnostics (parsing and type-checking) are always run immediately\non recently modified packages.\n\nThis option must be set to a valid duration string, for example `\"250ms\"`.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "\"1s\"",
+ "Status": "advanced",
+ "Hierarchy": "ui.diagnostic",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "diagnosticsTrigger",
+ "Type": "enum",
+ "Doc": "diagnosticsTrigger controls when to run diagnostics.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": [
+ {
+ "Value": "\"Edit\"",
+ "Doc": "`\"Edit\"`: Trigger diagnostics on file edit and save. (default)\n",
+ "Status": ""
+ },
+ {
+ "Value": "\"Save\"",
+ "Doc": "`\"Save\"`: Trigger diagnostics only on file save. Events like initial workspace load\nor configuration change will still trigger diagnostics.\n",
+ "Status": ""
+ }
+ ],
+ "Default": "\"Edit\"",
+ "Status": "experimental",
+ "Hierarchy": "ui.diagnostic",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "analysisProgressReporting",
+ "Type": "bool",
+ "Doc": "analysisProgressReporting controls whether gopls sends progress\nnotifications when construction of its index of analysis facts is taking a\nlong time. Cancelling these notifications will cancel the indexing task,\nthough it will restart after the next change in the workspace.\n\nWhen a package is opened for the first time and heavyweight analyses such as\nstaticcheck are enabled, it can take a while to construct the index of\nanalysis facts for all its dependencies. The index is cached in the\nfilesystem, so subsequent analysis should be faster.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "true",
+ "Status": "",
+ "Hierarchy": "ui.diagnostic",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "hints",
+ "Type": "map[enum]bool",
+ "Doc": "hints specify inlay hints that users want to see. A full list of hints\nthat gopls uses can be found in\n[inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n",
+ "EnumKeys": {
+ "ValueType": "bool",
+ "Keys": [
+ {
+ "Name": "\"assignVariableTypes\"",
+ "Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"compositeLiteralFields\"",
+ "Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"compositeLiteralTypes\"",
+ "Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"constantValues\"",
+ "Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"functionTypeParameters\"",
+ "Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"parameterNames\"",
+ "Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"rangeVariableTypes\"",
+ "Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n",
+ "Default": "false",
+ "Status": ""
+ }
+ ]
+ },
+ "EnumValues": null,
+ "Default": "{}",
+ "Status": "experimental",
+ "Hierarchy": "ui.inlayhint",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "codelenses",
+ "Type": "map[enum]bool",
+ "Doc": "codelenses overrides the enabled/disabled state of each of gopls'\nsources of [Code Lenses](codelenses.md).\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"codelenses\": {\n \"generate\": false, // Don't show the `go generate` lens.\n }\n...\n}\n```\n",
+ "EnumKeys": {
+ "ValueType": "bool",
+ "Keys": [
+ {
+ "Name": "\"generate\"",
+ "Doc": "`\"generate\"`: Run `go generate`\n\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"regenerate_cgo\"",
+ "Doc": "`\"regenerate_cgo\"`: Re-generate cgo declarations\n\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"run_govulncheck\"",
+ "Doc": "`\"run_govulncheck\"`: Run govulncheck (legacy)\n\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run Govulncheck asynchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
+ "Default": "false",
+ "Status": "experimental"
+ },
+ {
+ "Name": "\"test\"",
+ "Doc": "`\"test\"`: Run tests and benchmarks\n\nThis codelens source annotates each `Test` and `Benchmark`\nfunction in a `*_test.go` file with a command to run it.\n\nThis source is off by default because VS Code has\na client-side custom UI for testing, and because progress\nnotifications are not a great UX for streamed test output.\nSee:\n- golang/go#67400 for a discussion of this feature.\n- https://github.com/joaotavora/eglot/discussions/1402\n for an alternative approach.\n",
+ "Default": "false",
+ "Status": ""
+ },
+ {
+ "Name": "\"tidy\"",
+ "Doc": "`\"tidy\"`: Tidy go.mod file\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\ntidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures\nthat the go.mod file matches the source code in the module.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"upgrade_dependency\"",
+ "Doc": "`\"upgrade_dependency\"`: Update dependencies\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with commands to:\n\n- check for available upgrades,\n- upgrade direct dependencies, and\n- upgrade all dependencies transitively.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"vendor\"",
+ "Doc": "`\"vendor\"`: Update vendor directory\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n",
+ "Default": "true",
+ "Status": ""
+ },
+ {
+ "Name": "\"vulncheck\"",
+ "Doc": "`\"vulncheck\"`: Run govulncheck\n\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run govulncheck synchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
+ "Default": "false",
+ "Status": "experimental"
+ }
+ ]
+ },
+ "EnumValues": null,
+ "Default": "{\"generate\":true,\"regenerate_cgo\":true,\"run_govulncheck\":false,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}",
+ "Status": "",
+ "Hierarchy": "ui",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "semanticTokens",
+ "Type": "bool",
+ "Doc": "semanticTokens controls whether the LSP server will send\nsemantic tokens to the client.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "false",
+ "Status": "experimental",
+ "Hierarchy": "ui",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "noSemanticString",
+ "Type": "bool",
+ "Doc": "noSemanticString turns off the sending of the semantic token 'string'\n\nDeprecated: Use SemanticTokenTypes[\"string\"] = false instead. See\ngolang/vscode-go#3632\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "false",
+ "Status": "experimental",
+ "Hierarchy": "ui",
+ "DeprecationMessage": "use SemanticTokenTypes[\"string\"] = false instead. See\ngolang/vscode-go#3632\n"
+ },
+ {
+ "Name": "noSemanticNumber",
+ "Type": "bool",
+ "Doc": "noSemanticNumber turns off the sending of the semantic token 'number'\n\nDeprecated: Use SemanticTokenTypes[\"number\"] = false instead. See\ngolang/vscode-go#3632.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "false",
+ "Status": "experimental",
+ "Hierarchy": "ui",
+ "DeprecationMessage": "use SemanticTokenTypes[\"number\"] = false instead. See\ngolang/vscode-go#3632.\n"
+ },
+ {
+ "Name": "semanticTokenTypes",
+ "Type": "map[string]bool",
+ "Doc": "semanticTokenTypes configures the semantic token types. It allows\ndisabling types by setting each value to false.\nBy default, all types are enabled.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "{}",
+ "Status": "experimental",
+ "Hierarchy": "ui",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "semanticTokenModifiers",
+ "Type": "map[string]bool",
+ "Doc": "semanticTokenModifiers configures the semantic token modifiers. It allows\ndisabling modifiers by setting each value to false.\nBy default, all modifiers are enabled.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "{}",
+ "Status": "experimental",
+ "Hierarchy": "ui",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "local",
+ "Type": "string",
+ "Doc": "local is the equivalent of the `goimports -local` flag, which puts\nimports beginning with this string after third-party packages. It should\nbe the prefix of the import path whose imports should be grouped\nseparately.\n\nIt is used when tidying imports (during an LSP Organize\nImports request) or when inserting new ones (for example,\nduring completion); an LSP Formatting request merely sorts the\nexisting imports.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "\"\"",
+ "Status": "",
+ "Hierarchy": "formatting",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "gofumpt",
+ "Type": "bool",
+ "Doc": "gofumpt indicates if we should run gofumpt formatting.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "false",
+ "Status": "",
+ "Hierarchy": "formatting",
+ "DeprecationMessage": ""
+ },
+ {
+ "Name": "verboseOutput",
+ "Type": "bool",
+ "Doc": "verboseOutput enables additional debug logging.\n",
+ "EnumKeys": {
+ "ValueType": "",
+ "Keys": null
+ },
+ "EnumValues": null,
+ "Default": "false",
+ "Status": "debug",
+ "Hierarchy": "",
+ "DeprecationMessage": ""
+ }
+ ]
+ },
+ "Lenses": [
+ {
+ "FileType": "Go",
+ "Lens": "generate",
+ "Title": "Run `go generate`",
+ "Doc": "\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n",
+ "Default": true,
+ "Status": ""
+ },
+ {
+ "FileType": "Go",
+ "Lens": "regenerate_cgo",
+ "Title": "Re-generate cgo declarations",
+ "Doc": "\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n",
+ "Default": true,
+ "Status": ""
+ },
+ {
+ "FileType": "Go",
+ "Lens": "test",
+ "Title": "Run tests and benchmarks",
+ "Doc": "\nThis codelens source annotates each `Test` and `Benchmark`\nfunction in a `*_test.go` file with a command to run it.\n\nThis source is off by default because VS Code has\na client-side custom UI for testing, and because progress\nnotifications are not a great UX for streamed test output.\nSee:\n- golang/go#67400 for a discussion of this feature.\n- https://github.com/joaotavora/eglot/discussions/1402\n for an alternative approach.\n",
+ "Default": false,
+ "Status": ""
+ },
+ {
+ "FileType": "go.mod",
+ "Lens": "run_govulncheck",
+ "Title": "Run govulncheck (legacy)",
+ "Doc": "\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run Govulncheck asynchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
+ "Default": false,
+ "Status": "experimental"
+ },
+ {
+ "FileType": "go.mod",
+ "Lens": "tidy",
+ "Title": "Tidy go.mod file",
+ "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\ntidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures\nthat the go.mod file matches the source code in the module.\n",
+ "Default": true,
+ "Status": ""
+ },
+ {
+ "FileType": "go.mod",
+ "Lens": "upgrade_dependency",
+ "Title": "Update dependencies",
+ "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with commands to:\n\n- check for available upgrades,\n- upgrade direct dependencies, and\n- upgrade all dependencies transitively.\n",
+ "Default": true,
+ "Status": ""
+ },
+ {
+ "FileType": "go.mod",
+ "Lens": "vendor",
+ "Title": "Update vendor directory",
+ "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n",
+ "Default": true,
+ "Status": ""
+ },
+ {
+ "FileType": "go.mod",
+ "Lens": "vulncheck",
+ "Title": "Run govulncheck",
+ "Doc": "\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run govulncheck synchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
+ "Default": false,
+ "Status": "experimental"
+ }
+ ],
+ "Analyzers": [
+ {
+ "Name": "QF1001",
+ "Doc": "Apply De Morgan's law\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1001",
+ "Default": false
+ },
+ {
+ "Name": "QF1002",
+ "Doc": "Convert untagged switch to tagged switch\n\nAn untagged switch that compares a single variable against a series of\nvalues can be replaced with a tagged switch.\n\nBefore:\n\n switch {\n case x == 1 || x == 2, x == 3:\n ...\n case x == 4:\n ...\n default:\n ...\n }\n\nAfter:\n\n switch x {\n case 1, 2, 3:\n ...\n case 4:\n ...\n default:\n ...\n }\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1002",
+ "Default": true
+ },
+ {
+ "Name": "QF1003",
+ "Doc": "Convert if/else-if chain to tagged switch\n\nA series of if/else-if checks comparing the same variable against\nvalues can be replaced with a tagged switch.\n\nBefore:\n\n if x == 1 || x == 2 {\n ...\n } else if x == 3 {\n ...\n } else {\n ...\n }\n\nAfter:\n\n switch x {\n case 1, 2:\n ...\n case 3:\n ...\n default:\n ...\n }\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1003",
+ "Default": true
+ },
+ {
+ "Name": "QF1004",
+ "Doc": "Use strings.ReplaceAll instead of strings.Replace with n == -1\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1004",
+ "Default": true
+ },
+ {
+ "Name": "QF1005",
+ "Doc": "Expand call to math.Pow\n\nSome uses of math.Pow can be simplified to basic multiplication.\n\nBefore:\n\n math.Pow(x, 2)\n\nAfter:\n\n x * x\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1005",
+ "Default": false
+ },
+ {
+ "Name": "QF1006",
+ "Doc": "Lift if+break into loop condition\n\nBefore:\n\n for {\n if done {\n break\n }\n ...\n }\n\nAfter:\n\n for !done {\n ...\n }\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1006",
+ "Default": false
+ },
+ {
+ "Name": "QF1007",
+ "Doc": "Merge conditional assignment into variable declaration\n\nBefore:\n\n x := false\n if someCondition {\n x = true\n }\n\nAfter:\n\n x := someCondition\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1007",
+ "Default": false
+ },
+ {
+ "Name": "QF1008",
+ "Doc": "Omit embedded fields from selector expression\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1008",
+ "Default": false
+ },
+ {
+ "Name": "QF1009",
+ "Doc": "Use time.Time.Equal instead of == operator\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1009",
+ "Default": true
+ },
+ {
+ "Name": "QF1010",
+ "Doc": "Convert slice of bytes to string when printing it\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1010",
+ "Default": true
+ },
+ {
+ "Name": "QF1011",
+ "Doc": "Omit redundant type from variable declaration\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#",
+ "Default": false
+ },
+ {
+ "Name": "QF1012",
+ "Doc": "Use fmt.Fprintf(x, ...) instead of x.Write(fmt.Sprintf(...))\n\nAvailable since\n 2022.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#QF1012",
+ "Default": true
+ },
+ {
+ "Name": "S1000",
+ "Doc": "Use plain channel send or receive instead of single-case select\n\nSelect statements with a single case can be replaced with a simple\nsend or receive.\n\nBefore:\n\n select {\n case x := \u003c-ch:\n fmt.Println(x)\n }\n\nAfter:\n\n x := \u003c-ch\n fmt.Println(x)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1000",
+ "Default": true
+ },
+ {
+ "Name": "S1001",
+ "Doc": "Replace for loop with call to copy\n\nUse copy() for copying elements from one slice to another. For\narrays of identical size, you can use simple assignment.\n\nBefore:\n\n for i, x := range src {\n dst[i] = x\n }\n\nAfter:\n\n copy(dst, src)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1001",
+ "Default": true
+ },
+ {
+ "Name": "S1002",
+ "Doc": "Omit comparison with boolean constant\n\nBefore:\n\n if x == true {}\n\nAfter:\n\n if x {}\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1002",
+ "Default": false
+ },
+ {
+ "Name": "S1003",
+ "Doc": "Replace call to strings.Index with strings.Contains\n\nBefore:\n\n if strings.Index(x, y) != -1 {}\n\nAfter:\n\n if strings.Contains(x, y) {}\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1003",
+ "Default": true
+ },
+ {
+ "Name": "S1004",
+ "Doc": "Replace call to bytes.Compare with bytes.Equal\n\nBefore:\n\n if bytes.Compare(x, y) == 0 {}\n\nAfter:\n\n if bytes.Equal(x, y) {}\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1004",
+ "Default": true
+ },
+ {
+ "Name": "S1005",
+ "Doc": "Drop unnecessary use of the blank identifier\n\nIn many cases, assigning to the blank identifier is unnecessary.\n\nBefore:\n\n for _ = range s {}\n x, _ = someMap[key]\n _ = \u003c-ch\n\nAfter:\n\n for range s{}\n x = someMap[key]\n \u003c-ch\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1005",
+ "Default": false
+ },
+ {
+ "Name": "S1006",
+ "Doc": "Use 'for { ... }' for infinite loops\n\nFor infinite loops, using for { ... } is the most idiomatic choice.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1006",
+ "Default": false
+ },
+ {
+ "Name": "S1007",
+ "Doc": "Simplify regular expression by using raw string literal\n\nRaw string literals use backticks instead of quotation marks and do not support\nany escape sequences. This means that the backslash can be used\nfreely, without the need of escaping.\n\nSince regular expressions have their own escape sequences, raw strings\ncan improve their readability.\n\nBefore:\n\n regexp.Compile(\"\\\\A(\\\\w+) profile: total \\\\d+\\\\n\\\\z\")\n\nAfter:\n\n regexp.Compile(`\\A(\\w+) profile: total \\d+\\n\\z`)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1007",
+ "Default": true
+ },
+ {
+ "Name": "S1008",
+ "Doc": "Simplify returning boolean expression\n\nBefore:\n\n if \u003cexpr\u003e {\n return true\n }\n return false\n\nAfter:\n\n return \u003cexpr\u003e\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1008",
+ "Default": false
+ },
+ {
+ "Name": "S1009",
+ "Doc": "Omit redundant nil check on slices, maps, and channels\n\nThe len function is defined for all slices, maps, and\nchannels, even nil ones, which have a length of zero. It is not necessary to\ncheck for nil before checking that their length is not zero.\n\nBefore:\n\n if x != nil \u0026\u0026 len(x) != 0 {}\n\nAfter:\n\n if len(x) != 0 {}\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1009",
+ "Default": true
+ },
+ {
+ "Name": "S1010",
+ "Doc": "Omit default slice index\n\nWhen slicing, the second index defaults to the length of the value,\nmaking s[n:len(s)] and s[n:] equivalent.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1010",
+ "Default": true
+ },
+ {
+ "Name": "S1011",
+ "Doc": "Use a single append to concatenate two slices\n\nBefore:\n\n for _, e := range y {\n x = append(x, e)\n }\n \n for i := range y {\n x = append(x, y[i])\n }\n \n for i := range y {\n v := y[i]\n x = append(x, v)\n }\n\nAfter:\n\n x = append(x, y...)\n x = append(x, y...)\n x = append(x, y...)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1011",
+ "Default": false
+ },
+ {
+ "Name": "S1012",
+ "Doc": "Replace time.Now().Sub(x) with time.Since(x)\n\nThe time.Since helper has the same effect as using time.Now().Sub(x)\nbut is easier to read.\n\nBefore:\n\n time.Now().Sub(x)\n\nAfter:\n\n time.Since(x)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1012",
+ "Default": true
+ },
+ {
+ "Name": "S1016",
+ "Doc": "Use a type conversion instead of manually copying struct fields\n\nTwo struct types with identical fields can be converted between each\nother. In older versions of Go, the fields had to have identical\nstruct tags. Since Go 1.8, however, struct tags are ignored during\nconversions. It is thus not necessary to manually copy every field\nindividually.\n\nBefore:\n\n var x T1\n y := T2{\n Field1: x.Field1,\n Field2: x.Field2,\n }\n\nAfter:\n\n var x T1\n y := T2(x)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1016",
+ "Default": false
+ },
+ {
+ "Name": "S1017",
+ "Doc": "Replace manual trimming with strings.TrimPrefix\n\nInstead of using strings.HasPrefix and manual slicing, use the\nstrings.TrimPrefix function. If the string doesn't start with the\nprefix, the original string will be returned. Using strings.TrimPrefix\nreduces complexity, and avoids common bugs, such as off-by-one\nmistakes.\n\nBefore:\n\n if strings.HasPrefix(str, prefix) {\n str = str[len(prefix):]\n }\n\nAfter:\n\n str = strings.TrimPrefix(str, prefix)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1017",
+ "Default": true
+ },
+ {
+ "Name": "S1018",
+ "Doc": "Use 'copy' for sliding elements\n\ncopy() permits using the same source and destination slice, even with\noverlapping ranges. This makes it ideal for sliding elements in a\nslice.\n\nBefore:\n\n for i := 0; i \u003c n; i++ {\n bs[i] = bs[offset+i]\n }\n\nAfter:\n\n copy(bs[:n], bs[offset:])\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1018",
+ "Default": true
+ },
+ {
+ "Name": "S1019",
+ "Doc": "Simplify 'make' call by omitting redundant arguments\n\nThe 'make' function has default values for the length and capacity\narguments. For channels, the length defaults to zero, and for slices,\nthe capacity defaults to the length.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1019",
+ "Default": true
+ },
+ {
+ "Name": "S1020",
+ "Doc": "Omit redundant nil check in type assertion\n\nBefore:\n\n if _, ok := i.(T); ok \u0026\u0026 i != nil {}\n\nAfter:\n\n if _, ok := i.(T); ok {}\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1020",
+ "Default": true
+ },
+ {
+ "Name": "S1021",
+ "Doc": "Merge variable declaration and assignment\n\nBefore:\n\n var x uint\n x = 1\n\nAfter:\n\n var x uint = 1\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1021",
+ "Default": false
+ },
+ {
+ "Name": "S1023",
+ "Doc": "Omit redundant control flow\n\nFunctions that have no return value do not need a return statement as\nthe final statement of the function.\n\nSwitches in Go do not have automatic fallthrough, unlike languages\nlike C. It is not necessary to have a break statement as the final\nstatement in a case block.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1023",
+ "Default": true
+ },
+ {
+ "Name": "S1024",
+ "Doc": "Replace x.Sub(time.Now()) with time.Until(x)\n\nThe time.Until helper has the same effect as using x.Sub(time.Now())\nbut is easier to read.\n\nBefore:\n\n x.Sub(time.Now())\n\nAfter:\n\n time.Until(x)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1024",
+ "Default": true
+ },
+ {
+ "Name": "S1025",
+ "Doc": "Don't use fmt.Sprintf(\"%s\", x) unnecessarily\n\nIn many instances, there are easier and more efficient ways of getting\na value's string representation. Whenever a value's underlying type is\na string already, or the type has a String method, they should be used\ndirectly.\n\nGiven the following shared definitions\n\n type T1 string\n type T2 int\n\n func (T2) String() string { return \"Hello, world\" }\n\n var x string\n var y T1\n var z T2\n\nwe can simplify\n\n fmt.Sprintf(\"%s\", x)\n fmt.Sprintf(\"%s\", y)\n fmt.Sprintf(\"%s\", z)\n\nto\n\n x\n string(y)\n z.String()\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1025",
+ "Default": false
+ },
+ {
+ "Name": "S1028",
+ "Doc": "Simplify error construction with fmt.Errorf\n\nBefore:\n\n errors.New(fmt.Sprintf(...))\n\nAfter:\n\n fmt.Errorf(...)\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1028",
+ "Default": true
+ },
+ {
+ "Name": "S1029",
+ "Doc": "Range over the string directly\n\nRanging over a string will yield byte offsets and runes. If the offset\nisn't used, this is functionally equivalent to converting the string\nto a slice of runes and ranging over that. Ranging directly over the\nstring will be more performant, however, as it avoids allocating a new\nslice, the size of which depends on the length of the string.\n\nBefore:\n\n for _, r := range []rune(s) {}\n\nAfter:\n\n for _, r := range s {}\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1029",
+ "Default": false
+ },
+ {
+ "Name": "S1030",
+ "Doc": "Use bytes.Buffer.String or bytes.Buffer.Bytes\n\nbytes.Buffer has both a String and a Bytes method. It is almost never\nnecessary to use string(buf.Bytes()) or []byte(buf.String()) – simply\nuse the other method.\n\nThe only exception to this are map lookups. Due to a compiler optimization,\nm[string(buf.Bytes())] is more efficient than m[buf.String()].\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1030",
+ "Default": true
+ },
+ {
+ "Name": "S1031",
+ "Doc": "Omit redundant nil check around loop\n\nYou can use range on nil slices and maps, the loop will simply never\nexecute. This makes an additional nil check around the loop\nunnecessary.\n\nBefore:\n\n if s != nil {\n for _, x := range s {\n ...\n }\n }\n\nAfter:\n\n for _, x := range s {\n ...\n }\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1031",
+ "Default": true
+ },
+ {
+ "Name": "S1032",
+ "Doc": "Use sort.Ints(x), sort.Float64s(x), and sort.Strings(x)\n\nThe sort.Ints, sort.Float64s and sort.Strings functions are easier to\nread than sort.Sort(sort.IntSlice(x)), sort.Sort(sort.Float64Slice(x))\nand sort.Sort(sort.StringSlice(x)).\n\nBefore:\n\n sort.Sort(sort.StringSlice(x))\n\nAfter:\n\n sort.Strings(x)\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1032",
+ "Default": true
+ },
+ {
+ "Name": "S1033",
+ "Doc": "Unnecessary guard around call to 'delete'\n\nCalling delete on a nil map is a no-op.\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1033",
+ "Default": true
+ },
+ {
+ "Name": "S1034",
+ "Doc": "Use result of type assertion to simplify cases\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1034",
+ "Default": true
+ },
+ {
+ "Name": "S1035",
+ "Doc": "Redundant call to net/http.CanonicalHeaderKey in method call on net/http.Header\n\nThe methods on net/http.Header, namely Add, Del, Get\nand Set, already canonicalize the given header name.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1035",
+ "Default": true
+ },
+ {
+ "Name": "S1036",
+ "Doc": "Unnecessary guard around map access\n\nWhen accessing a map key that doesn't exist yet, one receives a zero\nvalue. Often, the zero value is a suitable value, for example when\nusing append or doing integer math.\n\nThe following\n\n if _, ok := m[\"foo\"]; ok {\n m[\"foo\"] = append(m[\"foo\"], \"bar\")\n } else {\n m[\"foo\"] = []string{\"bar\"}\n }\n\ncan be simplified to\n\n m[\"foo\"] = append(m[\"foo\"], \"bar\")\n\nand\n\n if _, ok := m2[\"k\"]; ok {\n m2[\"k\"] += 4\n } else {\n m2[\"k\"] = 4\n }\n\ncan be simplified to\n\n m[\"k\"] += 4\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1036",
+ "Default": true
+ },
+ {
+ "Name": "S1037",
+ "Doc": "Elaborate way of sleeping\n\nUsing a select statement with a single case receiving\nfrom the result of time.After is a very elaborate way of sleeping that\ncan much simpler be expressed with a simple call to time.Sleep.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1037",
+ "Default": true
+ },
+ {
+ "Name": "S1038",
+ "Doc": "Unnecessarily complex way of printing formatted string\n\nInstead of using fmt.Print(fmt.Sprintf(...)), one can use fmt.Printf(...).\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1038",
+ "Default": true
+ },
+ {
+ "Name": "S1039",
+ "Doc": "Unnecessary use of fmt.Sprint\n\nCalling fmt.Sprint with a single string argument is unnecessary\nand identical to using the string directly.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1039",
+ "Default": true
+ },
+ {
+ "Name": "S1040",
+ "Doc": "Type assertion to current type\n\nThe type assertion x.(SomeInterface), when x already has type\nSomeInterface, can only fail if x is nil. Usually, this is\nleft-over code from when x had a different type and you can safely\ndelete the type assertion. If you want to check that x is not nil,\nconsider being explicit and using an actual if x == nil comparison\ninstead of relying on the type assertion panicking.\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#S1040",
+ "Default": true
+ },
+ {
+ "Name": "SA1000",
+ "Doc": "Invalid regular expression\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1000",
+ "Default": false
+ },
+ {
+ "Name": "SA1001",
+ "Doc": "Invalid template\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1001",
+ "Default": true
+ },
+ {
+ "Name": "SA1002",
+ "Doc": "Invalid format in time.Parse\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1002",
+ "Default": false
+ },
+ {
+ "Name": "SA1003",
+ "Doc": "Unsupported argument to functions in encoding/binary\n\nThe encoding/binary package can only serialize types with known sizes.\nThis precludes the use of the int and uint types, as their sizes\ndiffer on different architectures. Furthermore, it doesn't support\nserializing maps, channels, strings, or functions.\n\nBefore Go 1.8, bool wasn't supported, either.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1003",
+ "Default": false
+ },
+ {
+ "Name": "SA1004",
+ "Doc": "Suspiciously small untyped constant in time.Sleep\n\nThe time.Sleep function takes a time.Duration as its only argument.\nDurations are expressed in nanoseconds. Thus, calling time.Sleep(1)\nwill sleep for 1 nanosecond. This is a common source of bugs, as sleep\nfunctions in other languages often accept seconds or milliseconds.\n\nThe time package provides constants such as time.Second to express\nlarge durations. These can be combined with arithmetic to express\narbitrary durations, for example 5 * time.Second for 5 seconds.\n\nIf you truly meant to sleep for a tiny amount of time, use\nn * time.Nanosecond to signal to Staticcheck that you did mean to sleep\nfor some amount of nanoseconds.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1004",
+ "Default": true
+ },
+ {
+ "Name": "SA1005",
+ "Doc": "Invalid first argument to exec.Command\n\nos/exec runs programs directly (using variants of the fork and exec\nsystem calls on Unix systems). This shouldn't be confused with running\na command in a shell. The shell will allow for features such as input\nredirection, pipes, and general scripting. The shell is also\nresponsible for splitting the user's input into a program name and its\narguments. For example, the equivalent to\n\n ls / /tmp\n\nwould be\n\n exec.Command(\"ls\", \"/\", \"/tmp\")\n\nIf you want to run a command in a shell, consider using something like\nthe following – but be aware that not all systems, particularly\nWindows, will have a /bin/sh program:\n\n exec.Command(\"/bin/sh\", \"-c\", \"ls | grep Awesome\")\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1005",
+ "Default": true
+ },
+ {
+ "Name": "SA1007",
+ "Doc": "Invalid URL in net/url.Parse\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1007",
+ "Default": false
+ },
+ {
+ "Name": "SA1008",
+ "Doc": "Non-canonical key in http.Header map\n\nKeys in http.Header maps are canonical, meaning they follow a specific\ncombination of uppercase and lowercase letters. Methods such as\nhttp.Header.Add and http.Header.Del convert inputs into this canonical\nform before manipulating the map.\n\nWhen manipulating http.Header maps directly, as opposed to using the\nprovided methods, care should be taken to stick to canonical form in\norder to avoid inconsistencies. The following piece of code\ndemonstrates one such inconsistency:\n\n h := http.Header{}\n h[\"etag\"] = []string{\"1234\"}\n h.Add(\"etag\", \"5678\")\n fmt.Println(h)\n\n // Output:\n // map[Etag:[5678] etag:[1234]]\n\nThe easiest way of obtaining the canonical form of a key is to use\nhttp.CanonicalHeaderKey.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1008",
+ "Default": true
+ },
+ {
+ "Name": "SA1010",
+ "Doc": "(*regexp.Regexp).FindAll called with n == 0, which will always return zero results\n\nIf n \u003e= 0, the function returns at most n matches/submatches. To\nreturn all results, specify a negative number.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1010",
+ "Default": false
+ },
+ {
+ "Name": "SA1011",
+ "Doc": "Various methods in the 'strings' package expect valid UTF-8, but invalid input is provided\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1011",
+ "Default": false
+ },
+ {
+ "Name": "SA1012",
+ "Doc": "A nil context.Context is being passed to a function, consider using context.TODO instead\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1012",
+ "Default": true
+ },
+ {
+ "Name": "SA1013",
+ "Doc": "io.Seeker.Seek is being called with the whence constant as the first argument, but it should be the second\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1013",
+ "Default": true
+ },
+ {
+ "Name": "SA1014",
+ "Doc": "Non-pointer value passed to Unmarshal or Decode\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1014",
+ "Default": false
+ },
+ {
+ "Name": "SA1015",
+ "Doc": "Using time.Tick in a way that will leak. Consider using time.NewTicker, and only use time.Tick in tests, commands and endless functions\n\nBefore Go 1.23, time.Tickers had to be closed to be able to be garbage\ncollected. Since time.Tick doesn't make it possible to close the underlying\nticker, using it repeatedly would leak memory.\n\nGo 1.23 fixes this by allowing tickers to be collected even if they weren't closed.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1015",
+ "Default": false
+ },
+ {
+ "Name": "SA1016",
+ "Doc": "Trapping a signal that cannot be trapped\n\nNot all signals can be intercepted by a process. Specifically, on\nUNIX-like systems, the syscall.SIGKILL and syscall.SIGSTOP signals are\nnever passed to the process, but instead handled directly by the\nkernel. It is therefore pointless to try and handle these signals.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1016",
+ "Default": true
+ },
+ {
+ "Name": "SA1017",
+ "Doc": "Channels used with os/signal.Notify should be buffered\n\nThe os/signal package uses non-blocking channel sends when delivering\nsignals. If the receiving end of the channel isn't ready and the\nchannel is either unbuffered or full, the signal will be dropped. To\navoid missing signals, the channel should be buffered and of the\nappropriate size. For a channel used for notification of just one\nsignal value, a buffer of size 1 is sufficient.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1017",
+ "Default": false
+ },
+ {
+ "Name": "SA1018",
+ "Doc": "strings.Replace called with n == 0, which does nothing\n\nWith n == 0, zero instances will be replaced. To replace all\ninstances, use a negative number, or use strings.ReplaceAll.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1018",
+ "Default": false
+ },
+ {
+ "Name": "SA1020",
+ "Doc": "Using an invalid host:port pair with a net.Listen-related function\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1020",
+ "Default": false
+ },
+ {
+ "Name": "SA1021",
+ "Doc": "Using bytes.Equal to compare two net.IP\n\nA net.IP stores an IPv4 or IPv6 address as a slice of bytes. The\nlength of the slice for an IPv4 address, however, can be either 4 or\n16 bytes long, using different ways of representing IPv4 addresses. In\norder to correctly compare two net.IPs, the net.IP.Equal method should\nbe used, as it takes both representations into account.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1021",
+ "Default": false
+ },
+ {
+ "Name": "SA1023",
+ "Doc": "Modifying the buffer in an io.Writer implementation\n\nWrite must not modify the slice data, even temporarily.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1023",
+ "Default": false
+ },
+ {
+ "Name": "SA1024",
+ "Doc": "A string cutset contains duplicate characters\n\nThe strings.TrimLeft and strings.TrimRight functions take cutsets, not\nprefixes. A cutset is treated as a set of characters to remove from a\nstring. For example,\n\n strings.TrimLeft(\"42133word\", \"1234\")\n\nwill result in the string \"word\" – any characters that are 1, 2, 3 or\n4 are cut from the left of the string.\n\nIn order to remove one string from another, use strings.TrimPrefix instead.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1024",
+ "Default": false
+ },
+ {
+ "Name": "SA1025",
+ "Doc": "It is not possible to use (*time.Timer).Reset's return value correctly\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1025",
+ "Default": false
+ },
+ {
+ "Name": "SA1026",
+ "Doc": "Cannot marshal channels or functions\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1026",
+ "Default": false
+ },
+ {
+ "Name": "SA1027",
+ "Doc": "Atomic access to 64-bit variable must be 64-bit aligned\n\nOn ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to\narrange for 64-bit alignment of 64-bit words accessed atomically. The\nfirst word in a variable or in an allocated struct, array, or slice\ncan be relied upon to be 64-bit aligned.\n\nYou can use the structlayout tool to inspect the alignment of fields\nin a struct.\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1027",
+ "Default": false
+ },
+ {
+ "Name": "SA1028",
+ "Doc": "sort.Slice can only be used on slices\n\nThe first argument of sort.Slice must be a slice.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1028",
+ "Default": false
+ },
+ {
+ "Name": "SA1029",
+ "Doc": "Inappropriate key in call to context.WithValue\n\nThe provided key must be comparable and should not be\nof type string or any other built-in type to avoid collisions between\npackages using context. Users of WithValue should define their own\ntypes for keys.\n\nTo avoid allocating when assigning to an interface{},\ncontext keys often have concrete type struct{}. Alternatively,\nexported context key variables' static type should be a pointer or\ninterface.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1029",
+ "Default": false
+ },
+ {
+ "Name": "SA1030",
+ "Doc": "Invalid argument in call to a strconv function\n\nThis check validates the format, number base and bit size arguments of\nthe various parsing and formatting functions in strconv.\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1030",
+ "Default": false
+ },
+ {
+ "Name": "SA1031",
+ "Doc": "Overlapping byte slices passed to an encoder\n\nIn an encoding function of the form Encode(dst, src), dst and\nsrc were found to reference the same memory. This can result in\nsrc bytes being overwritten before they are read, when the encoder\nwrites more than one byte per src byte.\n\nAvailable since\n 2024.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1031",
+ "Default": false
+ },
+ {
+ "Name": "SA1032",
+ "Doc": "Wrong order of arguments to errors.Is\n\nThe first argument of the function errors.Is is the error\nthat we have and the second argument is the error we're trying to match against.\nFor example:\n\n\tif errors.Is(err, io.EOF) { ... }\n\nThis check detects some cases where the two arguments have been swapped. It\nflags any calls where the first argument is referring to a package-level error\nvariable, such as\n\n\tif errors.Is(io.EOF, err) { /* this is wrong */ }\n\nAvailable since\n 2024.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA1032",
+ "Default": false
+ },
{
- "FileType": "Go",
- "Lens": "generate",
- "Title": "Run `go generate`",
- "Doc": "\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n",
+ "Name": "SA2001",
+ "Doc": "Empty critical section, did you mean to defer the unlock?\n\nEmpty critical sections of the kind\n\n mu.Lock()\n mu.Unlock()\n\nare very often a typo, and the following was intended instead:\n\n mu.Lock()\n defer mu.Unlock()\n\nDo note that sometimes empty critical sections can be useful, as a\nform of signaling to wait on another goroutine. Many times, there are\nsimpler ways of achieving the same effect. When that isn't the case,\nthe code should be amply commented to avoid confusion. Combining such\ncomments with a //lint:ignore directive can be used to suppress this\nrare false positive.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA2001",
"Default": true
},
{
- "FileType": "Go",
- "Lens": "regenerate_cgo",
- "Title": "Re-generate cgo declarations",
- "Doc": "\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n",
+ "Name": "SA2002",
+ "Doc": "Called testing.T.FailNow or SkipNow in a goroutine, which isn't allowed\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA2002",
+ "Default": false
+ },
+ {
+ "Name": "SA2003",
+ "Doc": "Deferred Lock right after locking, likely meant to defer Unlock instead\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA2003",
+ "Default": false
+ },
+ {
+ "Name": "SA3000",
+ "Doc": "TestMain doesn't call os.Exit, hiding test failures\n\nTest executables (and in turn 'go test') exit with a non-zero status\ncode if any tests failed. When specifying your own TestMain function,\nit is your responsibility to arrange for this, by calling os.Exit with\nthe correct code. The correct code is returned by (*testing.M).Run, so\nthe usual way of implementing TestMain is to end it with\nos.Exit(m.Run()).\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA3000",
"Default": true
},
{
- "FileType": "Go",
- "Lens": "test",
- "Title": "Run tests and benchmarks",
- "Doc": "\nThis codelens source annotates each `Test` and `Benchmark`\nfunction in a `*_test.go` file with a command to run it.\n\nThis source is off by default because VS Code has\na client-side custom UI for testing, and because progress\nnotifications are not a great UX for streamed test output.\nSee:\n- golang/go#67400 for a discussion of this feature.\n- https://github.com/joaotavora/eglot/discussions/1402\n for an alternative approach.\n",
+ "Name": "SA3001",
+ "Doc": "Assigning to b.N in benchmarks distorts the results\n\nThe testing package dynamically sets b.N to improve the reliability of\nbenchmarks and uses it in computations to determine the duration of a\nsingle operation. Benchmark code must not alter b.N as this would\nfalsify results.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA3001",
+ "Default": true
+ },
+ {
+ "Name": "SA4000",
+ "Doc": "Binary operator has identical expressions on both sides\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4000",
+ "Default": true
+ },
+ {
+ "Name": "SA4001",
+ "Doc": "\u0026*x gets simplified to x, it does not copy x\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4001",
+ "Default": true
+ },
+ {
+ "Name": "SA4003",
+ "Doc": "Comparing unsigned values against negative values is pointless\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4003",
+ "Default": true
+ },
+ {
+ "Name": "SA4004",
+ "Doc": "The loop exits unconditionally after one iteration\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4004",
+ "Default": true
+ },
+ {
+ "Name": "SA4005",
+ "Doc": "Field assignment that will never be observed. Did you mean to use a pointer receiver?\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4005",
"Default": false
},
{
- "FileType": "go.mod",
- "Lens": "run_govulncheck",
- "Title": "Run govulncheck (legacy)",
- "Doc": "\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run Govulncheck asynchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
+ "Name": "SA4006",
+ "Doc": "A value assigned to a variable is never read before being overwritten. Forgotten error check or dead code?\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4006",
"Default": false
},
{
- "FileType": "go.mod",
- "Lens": "tidy",
- "Title": "Tidy go.mod file",
- "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\ntidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures\nthat the go.mod file matches the source code in the module.\n",
+ "Name": "SA4008",
+ "Doc": "The variable in the loop condition never changes, are you incrementing the wrong variable?\n\nFor example:\n\n\tfor i := 0; i \u003c 10; j++ { ... }\n\nThis may also occur when a loop can only execute once because of unconditional\ncontrol flow that terminates the loop. For example, when a loop body contains an\nunconditional break, return, or panic:\n\n\tfunc f() {\n\t\tpanic(\"oops\")\n\t}\n\tfunc g() {\n\t\tfor i := 0; i \u003c 10; i++ {\n\t\t\t// f unconditionally calls panic, which means \"i\" is\n\t\t\t// never incremented.\n\t\t\tf()\n\t\t}\n\t}\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4008",
+ "Default": false
+ },
+ {
+ "Name": "SA4009",
+ "Doc": "A function argument is overwritten before its first use\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4009",
+ "Default": false
+ },
+ {
+ "Name": "SA4010",
+ "Doc": "The result of append will never be observed anywhere\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4010",
+ "Default": false
+ },
+ {
+ "Name": "SA4011",
+ "Doc": "Break statement with no effect. Did you mean to break out of an outer loop?\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4011",
"Default": true
},
{
- "FileType": "go.mod",
- "Lens": "upgrade_dependency",
- "Title": "Update dependencies",
- "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with commands to:\n\n- check for available upgrades,\n- upgrade direct dependencies, and\n- upgrade all dependencies transitively.\n",
+ "Name": "SA4012",
+ "Doc": "Comparing a value against NaN even though no value is equal to NaN\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4012",
+ "Default": false
+ },
+ {
+ "Name": "SA4013",
+ "Doc": "Negating a boolean twice (!!b) is the same as writing b. This is either redundant, or a typo.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4013",
"Default": true
},
{
- "FileType": "go.mod",
- "Lens": "vendor",
- "Title": "Update vendor directory",
- "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n",
+ "Name": "SA4014",
+ "Doc": "An if/else if chain has repeated conditions and no side-effects; if the condition didn't match the first time, it won't match the second time, either\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4014",
"Default": true
},
{
- "FileType": "go.mod",
- "Lens": "vulncheck",
- "Title": "Run govulncheck",
- "Doc": "\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run govulncheck synchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
+ "Name": "SA4015",
+ "Doc": "Calling functions like math.Ceil on floats converted from integers doesn't do anything useful\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4015",
"Default": false
- }
- ],
- "Analyzers": [
+ },
+ {
+ "Name": "SA4016",
+ "Doc": "Certain bitwise operations, such as x ^ 0, do not do anything useful\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4016",
+ "Default": true
+ },
+ {
+ "Name": "SA4017",
+ "Doc": "Discarding the return values of a function without side effects, making the call pointless\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4017",
+ "Default": false
+ },
+ {
+ "Name": "SA4018",
+ "Doc": "Self-assignment of variables\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4018",
+ "Default": false
+ },
+ {
+ "Name": "SA4019",
+ "Doc": "Multiple, identical build constraints in the same file\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4019",
+ "Default": true
+ },
+ {
+ "Name": "SA4020",
+ "Doc": "Unreachable case clause in a type switch\n\nIn a type switch like the following\n\n type T struct{}\n func (T) Read(b []byte) (int, error) { return 0, nil }\n\n var v interface{} = T{}\n\n switch v.(type) {\n case io.Reader:\n // ...\n case T:\n // unreachable\n }\n\nthe second case clause can never be reached because T implements\nio.Reader and case clauses are evaluated in source order.\n\nAnother example:\n\n type T struct{}\n func (T) Read(b []byte) (int, error) { return 0, nil }\n func (T) Close() error { return nil }\n\n var v interface{} = T{}\n\n switch v.(type) {\n case io.Reader:\n // ...\n case io.ReadCloser:\n // unreachable\n }\n\nEven though T has a Close method and thus implements io.ReadCloser,\nio.Reader will always match first. The method set of io.Reader is a\nsubset of io.ReadCloser. Thus it is impossible to match the second\ncase without matching the first case.\n\n\nStructurally equivalent interfaces\n\nA special case of the previous example are structurally identical\ninterfaces. Given these declarations\n\n type T error\n type V error\n\n func doSomething() error {\n err, ok := doAnotherThing()\n if ok {\n return T(err)\n }\n\n return U(err)\n }\n\nthe following type switch will have an unreachable case clause:\n\n switch doSomething().(type) {\n case T:\n // ...\n case V:\n // unreachable\n }\n\nT will always match before V because they are structurally equivalent\nand therefore doSomething()'s return value implements both.\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4020",
+ "Default": true
+ },
+ {
+ "Name": "SA4022",
+ "Doc": "Comparing the address of a variable against nil\n\nCode such as 'if \u0026x == nil' is meaningless, because taking the address of a variable always yields a non-nil pointer.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4022",
+ "Default": true
+ },
+ {
+ "Name": "SA4023",
+ "Doc": "Impossible comparison of interface value with untyped nil\n\nUnder the covers, interfaces are implemented as two elements, a\ntype T and a value V. V is a concrete value such as an int,\nstruct or pointer, never an interface itself, and has type T. For\ninstance, if we store the int value 3 in an interface, the\nresulting interface value has, schematically, (T=int, V=3). The\nvalue V is also known as the interface's dynamic value, since a\ngiven interface variable might hold different values V (and\ncorresponding types T) during the execution of the program.\n\nAn interface value is nil only if the V and T are both\nunset, (T=nil, V is not set), In particular, a nil interface will\nalways hold a nil type. If we store a nil pointer of type *int\ninside an interface value, the inner type will be *int regardless\nof the value of the pointer: (T=*int, V=nil). Such an interface\nvalue will therefore be non-nil even when the pointer value V\ninside is nil.\n\nThis situation can be confusing, and arises when a nil value is\nstored inside an interface value such as an error return:\n\n func returnsError() error {\n var p *MyError = nil\n if bad() {\n p = ErrBad\n }\n return p // Will always return a non-nil error.\n }\n\nIf all goes well, the function returns a nil p, so the return\nvalue is an error interface value holding (T=*MyError, V=nil).\nThis means that if the caller compares the returned error to nil,\nit will always look as if there was an error even if nothing bad\nhappened. To return a proper nil error to the caller, the\nfunction must return an explicit nil:\n\n func returnsError() error {\n if bad() {\n return ErrBad\n }\n return nil\n }\n\nIt's a good idea for functions that return errors always to use\nthe error type in their signature (as we did above) rather than a\nconcrete type such as *MyError, to help guarantee the error is\ncreated correctly. As an example, os.Open returns an error even\nthough, if not nil, it's always of concrete type *os.PathError.\n\nSimilar situations to those described here can arise whenever\ninterfaces are used. Just keep in mind that if any concrete value\nhas been stored in the interface, the interface will not be nil.\nFor more information, see The Laws of\nReflection at https://golang.org/doc/articles/laws_of_reflection.html.\n\nThis text has been copied from\nhttps://golang.org/doc/faq#nil_error, licensed under the Creative\nCommons Attribution 3.0 License.\n\nAvailable since\n 2020.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4023",
+ "Default": false
+ },
+ {
+ "Name": "SA4024",
+ "Doc": "Checking for impossible return value from a builtin function\n\nReturn values of the len and cap builtins cannot be negative.\n\nSee https://golang.org/pkg/builtin/#len and https://golang.org/pkg/builtin/#cap.\n\nExample:\n\n if len(slice) \u003c 0 {\n fmt.Println(\"unreachable code\")\n }\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4024",
+ "Default": true
+ },
+ {
+ "Name": "SA4025",
+ "Doc": "Integer division of literals that results in zero\n\nWhen dividing two integer constants, the result will\nalso be an integer. Thus, a division such as 2 / 3 results in 0.\nThis is true for all of the following examples:\n\n\t_ = 2 / 3\n\tconst _ = 2 / 3\n\tconst _ float64 = 2 / 3\n\t_ = float64(2 / 3)\n\nStaticcheck will flag such divisions if both sides of the division are\ninteger literals, as it is highly unlikely that the division was\nintended to truncate to zero. Staticcheck will not flag integer\ndivision involving named constants, to avoid noisy positives.\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4025",
+ "Default": true
+ },
+ {
+ "Name": "SA4026",
+ "Doc": "Go constants cannot express negative zero\n\nIn IEEE 754 floating point math, zero has a sign and can be positive\nor negative. This can be useful in certain numerical code.\n\nGo constants, however, cannot express negative zero. This means that\nthe literals -0.0 and 0.0 have the same ideal value (zero) and\nwill both represent positive zero at runtime.\n\nTo explicitly and reliably create a negative zero, you can use the\nmath.Copysign function: math.Copysign(0, -1).\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4026",
+ "Default": true
+ },
+ {
+ "Name": "SA4027",
+ "Doc": "(*net/url.URL).Query returns a copy, modifying it doesn't change the URL\n\n(*net/url.URL).Query parses the current value of net/url.URL.RawQuery\nand returns it as a map of type net/url.Values. Subsequent changes to\nthis map will not affect the URL unless the map gets encoded and\nassigned to the URL's RawQuery.\n\nAs a consequence, the following code pattern is an expensive no-op:\nu.Query().Add(key, value).\n\nAvailable since\n 2021.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4027",
+ "Default": true
+ },
+ {
+ "Name": "SA4028",
+ "Doc": "x % 1 is always zero\n\nAvailable since\n 2022.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4028",
+ "Default": true
+ },
+ {
+ "Name": "SA4029",
+ "Doc": "Ineffective attempt at sorting slice\n\nsort.Float64Slice, sort.IntSlice, and sort.StringSlice are\ntypes, not functions. Doing x = sort.StringSlice(x) does nothing,\nespecially not sort any values. The correct usage is\nsort.Sort(sort.StringSlice(x)) or sort.StringSlice(x).Sort(),\nbut there are more convenient helpers, namely sort.Float64s,\nsort.Ints, and sort.Strings.\n\nAvailable since\n 2022.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4029",
+ "Default": true
+ },
+ {
+ "Name": "SA4030",
+ "Doc": "Ineffective attempt at generating random number\n\nFunctions in the math/rand package that accept upper limits, such\nas Intn, generate random numbers in the half-open interval [0,n). In\nother words, the generated numbers will be \u003e= 0 and \u003c n – they\ndon't include n. rand.Intn(1) therefore doesn't generate 0\nor 1, it always generates 0.\n\nAvailable since\n 2022.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4030",
+ "Default": true
+ },
+ {
+ "Name": "SA4031",
+ "Doc": "Checking never-nil value against nil\n\nAvailable since\n 2022.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4031",
+ "Default": false
+ },
+ {
+ "Name": "SA4032",
+ "Doc": "Comparing runtime.GOOS or runtime.GOARCH against impossible value\n\nAvailable since\n 2024.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA4032",
+ "Default": true
+ },
+ {
+ "Name": "SA5000",
+ "Doc": "Assignment to nil map\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5000",
+ "Default": false
+ },
+ {
+ "Name": "SA5001",
+ "Doc": "Deferring Close before checking for a possible error\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5001",
+ "Default": true
+ },
+ {
+ "Name": "SA5002",
+ "Doc": "The empty for loop ('for {}') spins and can block the scheduler\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5002",
+ "Default": false
+ },
+ {
+ "Name": "SA5003",
+ "Doc": "Defers in infinite loops will never execute\n\nDefers are scoped to the surrounding function, not the surrounding\nblock. In a function that never returns, i.e. one containing an\ninfinite loop, defers will never execute.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5003",
+ "Default": true
+ },
+ {
+ "Name": "SA5004",
+ "Doc": "'for { select { ...' with an empty default branch spins\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5004",
+ "Default": true
+ },
+ {
+ "Name": "SA5005",
+ "Doc": "The finalizer references the finalized object, preventing garbage collection\n\nA finalizer is a function associated with an object that runs when the\ngarbage collector is ready to collect said object, that is when the\nobject is no longer referenced by anything.\n\nIf the finalizer references the object, however, it will always remain\nas the final reference to that object, preventing the garbage\ncollector from collecting the object. The finalizer will never run,\nand the object will never be collected, leading to a memory leak. That\nis why the finalizer should instead use its first argument to operate\non the object. That way, the number of references can temporarily go\nto zero before the object is being passed to the finalizer.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5005",
+ "Default": false
+ },
+ {
+ "Name": "SA5007",
+ "Doc": "Infinite recursive call\n\nA function that calls itself recursively needs to have an exit\ncondition. Otherwise it will recurse forever, until the system runs\nout of memory.\n\nThis issue can be caused by simple bugs such as forgetting to add an\nexit condition. It can also happen \"on purpose\". Some languages have\ntail call optimization which makes certain infinite recursive calls\nsafe to use. Go, however, does not implement TCO, and as such a loop\nshould be used instead.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5007",
+ "Default": false
+ },
+ {
+ "Name": "SA5008",
+ "Doc": "Invalid struct tag\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5008",
+ "Default": true
+ },
+ {
+ "Name": "SA5010",
+ "Doc": "Impossible type assertion\n\nSome type assertions can be statically proven to be\nimpossible. This is the case when the method sets of both\narguments of the type assertion conflict with each other, for\nexample by containing the same method with different\nsignatures.\n\nThe Go compiler already applies this check when asserting from an\ninterface value to a concrete type. If the concrete type misses\nmethods from the interface, or if function signatures don't match,\nthen the type assertion can never succeed.\n\nThis check applies the same logic when asserting from one interface to\nanother. If both interface types contain the same method but with\ndifferent signatures, then the type assertion can never succeed,\neither.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5010",
+ "Default": false
+ },
+ {
+ "Name": "SA5011",
+ "Doc": "Possible nil pointer dereference\n\nA pointer is being dereferenced unconditionally, while\nalso being checked against nil in another place. This suggests that\nthe pointer may be nil and dereferencing it may panic. This is\ncommonly a result of improperly ordered code or missing return\nstatements. Consider the following examples:\n\n func fn(x *int) {\n fmt.Println(*x)\n\n // This nil check is equally important for the previous dereference\n if x != nil {\n foo(*x)\n }\n }\n\n func TestFoo(t *testing.T) {\n x := compute()\n if x == nil {\n t.Errorf(\"nil pointer received\")\n }\n\n // t.Errorf does not abort the test, so if x is nil, the next line will panic.\n foo(*x)\n }\n\nStaticcheck tries to deduce which functions abort control flow.\nFor example, it is aware that a function will not continue\nexecution after a call to panic or log.Fatal. However, sometimes\nthis detection fails, in particular in the presence of\nconditionals. Consider the following example:\n\n func Log(msg string, level int) {\n fmt.Println(msg)\n if level == levelFatal {\n os.Exit(1)\n }\n }\n\n func Fatal(msg string) {\n Log(msg, levelFatal)\n }\n\n func fn(x *int) {\n if x == nil {\n Fatal(\"unexpected nil pointer\")\n }\n fmt.Println(*x)\n }\n\nStaticcheck will flag the dereference of x, even though it is perfectly\nsafe. Staticcheck is not able to deduce that a call to\nFatal will exit the program. For the time being, the easiest\nworkaround is to modify the definition of Fatal like so:\n\n func Fatal(msg string) {\n Log(msg, levelFatal)\n panic(\"unreachable\")\n }\n\nWe also hard-code functions from common logging packages such as\nlogrus. Please file an issue if we're missing support for a\npopular package.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5011",
+ "Default": false
+ },
+ {
+ "Name": "SA5012",
+ "Doc": "Passing odd-sized slice to function expecting even size\n\nSome functions that take slices as parameters expect the slices to have an even number of elements. \nOften, these functions treat elements in a slice as pairs. \nFor example, strings.NewReplacer takes pairs of old and new strings, \nand calling it with an odd number of elements would be an error.\n\nAvailable since\n 2020.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA5012",
+ "Default": false
+ },
+ {
+ "Name": "SA6000",
+ "Doc": "Using regexp.Match or related in a loop, should use regexp.Compile\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA6000",
+ "Default": false
+ },
+ {
+ "Name": "SA6001",
+ "Doc": "Missing an optimization opportunity when indexing maps by byte slices\n\nMap keys must be comparable, which precludes the use of byte slices.\nThis usually leads to using string keys and converting byte slices to\nstrings.\n\nNormally, a conversion of a byte slice to a string needs to copy the data and\ncauses allocations. The compiler, however, recognizes m[string(b)] and\nuses the data of b directly, without copying it, because it knows that\nthe data can't change during the map lookup. This leads to the\ncounter-intuitive situation that\n\n k := string(b)\n println(m[k])\n println(m[k])\n\nwill be less efficient than\n\n println(m[string(b)])\n println(m[string(b)])\n\nbecause the first version needs to copy and allocate, while the second\none does not.\n\nFor some history on this optimization, check out commit\nf5f5a8b6209f84961687d993b93ea0d397f5d5bf in the Go repository.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA6001",
+ "Default": false
+ },
+ {
+ "Name": "SA6002",
+ "Doc": "Storing non-pointer values in sync.Pool allocates memory\n\nA sync.Pool is used to avoid unnecessary allocations and reduce the\namount of work the garbage collector has to do.\n\nWhen passing a value that is not a pointer to a function that accepts\nan interface, the value needs to be placed on the heap, which means an\nadditional allocation. Slices are a common thing to put in sync.Pools,\nand they're structs with 3 fields (length, capacity, and a pointer to\nan array). In order to avoid the extra allocation, one should store a\npointer to the slice instead.\n\nSee the comments on https://go-review.googlesource.com/c/go/+/24371\nthat discuss this problem.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA6002",
+ "Default": false
+ },
+ {
+ "Name": "SA6003",
+ "Doc": "Converting a string to a slice of runes before ranging over it\n\nYou may want to loop over the runes in a string. Instead of converting\nthe string to a slice of runes and looping over that, you can loop\nover the string itself. That is,\n\n for _, r := range s {}\n\nand\n\n for _, r := range []rune(s) {}\n\nwill yield the same values. The first version, however, will be faster\nand avoid unnecessary memory allocations.\n\nDo note that if you are interested in the indices, ranging over a\nstring and over a slice of runes will yield different indices. The\nfirst one yields byte offsets, while the second one yields indices in\nthe slice of runes.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA6003",
+ "Default": false
+ },
+ {
+ "Name": "SA6005",
+ "Doc": "Inefficient string comparison with strings.ToLower or strings.ToUpper\n\nConverting two strings to the same case and comparing them like so\n\n if strings.ToLower(s1) == strings.ToLower(s2) {\n ...\n }\n\nis significantly more expensive than comparing them with\nstrings.EqualFold(s1, s2). This is due to memory usage as well as\ncomputational complexity.\n\nstrings.ToLower will have to allocate memory for the new strings, as\nwell as convert both strings fully, even if they differ on the very\nfirst byte. strings.EqualFold, on the other hand, compares the strings\none character at a time. It doesn't need to create two intermediate\nstrings and can return as soon as the first non-matching character has\nbeen found.\n\nFor a more in-depth explanation of this issue, see\nhttps://blog.digitalocean.com/how-to-efficiently-compare-strings-in-go/\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA6005",
+ "Default": true
+ },
+ {
+ "Name": "SA6006",
+ "Doc": "Using io.WriteString to write []byte\n\nUsing io.WriteString to write a slice of bytes, as in\n\n io.WriteString(w, string(b))\n\nis both unnecessary and inefficient. Converting from []byte to string\nhas to allocate and copy the data, and we could simply use w.Write(b)\ninstead.\n\nAvailable since\n 2024.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA6006",
+ "Default": true
+ },
+ {
+ "Name": "SA9001",
+ "Doc": "Defers in range loops may not run when you expect them to\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9001",
+ "Default": false
+ },
+ {
+ "Name": "SA9002",
+ "Doc": "Using a non-octal os.FileMode that looks like it was meant to be in octal.\n\nAvailable since\n 2017.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9002",
+ "Default": true
+ },
+ {
+ "Name": "SA9003",
+ "Doc": "Empty body in an if or else branch\n\nAvailable since\n 2017.1, non-default\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9003",
+ "Default": false
+ },
+ {
+ "Name": "SA9004",
+ "Doc": "Only the first constant has an explicit type\n\nIn a constant declaration such as the following:\n\n const (\n First byte = 1\n Second = 2\n )\n\nthe constant Second does not have the same type as the constant First.\nThis construct shouldn't be confused with\n\n const (\n First byte = iota\n Second\n )\n\nwhere First and Second do indeed have the same type. The type is only\npassed on when no explicit value is assigned to the constant.\n\nWhen declaring enumerations with explicit values it is therefore\nimportant not to write\n\n const (\n EnumFirst EnumType = 1\n EnumSecond = 2\n EnumThird = 3\n )\n\nThis discrepancy in types can cause various confusing behaviors and\nbugs.\n\n\nWrong type in variable declarations\n\nThe most obvious issue with such incorrect enumerations expresses\nitself as a compile error:\n\n package pkg\n\n const (\n EnumFirst uint8 = 1\n EnumSecond = 2\n )\n\n func fn(useFirst bool) {\n x := EnumSecond\n if useFirst {\n x = EnumFirst\n }\n }\n\nfails to compile with\n\n ./const.go:11:5: cannot use EnumFirst (type uint8) as type int in assignment\n\n\nLosing method sets\n\nA more subtle issue occurs with types that have methods and optional\ninterfaces. Consider the following:\n\n package main\n\n import \"fmt\"\n\n type Enum int\n\n func (e Enum) String() string {\n return \"an enum\"\n }\n\n const (\n EnumFirst Enum = 1\n EnumSecond = 2\n )\n\n func main() {\n fmt.Println(EnumFirst)\n fmt.Println(EnumSecond)\n }\n\nThis code will output\n\n an enum\n 2\n\nas EnumSecond has no explicit type, and thus defaults to int.\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9004",
+ "Default": true
+ },
+ {
+ "Name": "SA9005",
+ "Doc": "Trying to marshal a struct with no public fields nor custom marshaling\n\nThe encoding/json and encoding/xml packages only operate on exported\nfields in structs, not unexported ones. It is usually an error to try\nto (un)marshal structs that only consist of unexported fields.\n\nThis check will not flag calls involving types that define custom\nmarshaling behavior, e.g. via MarshalJSON methods. It will also not\nflag empty structs.\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9005",
+ "Default": false
+ },
+ {
+ "Name": "SA9006",
+ "Doc": "Dubious bit shifting of a fixed size integer value\n\nBit shifting a value past its size will always clear the value.\n\nFor instance:\n\n v := int8(42)\n v \u003e\u003e= 8\n\nwill always result in 0.\n\nThis check flags bit shifting operations on fixed size integer values only.\nThat is, int, uint and uintptr are never flagged to avoid potential false\npositives in somewhat exotic but valid bit twiddling tricks:\n\n // Clear any value above 32 bits if integers are more than 32 bits.\n func f(i int) int {\n v := i \u003e\u003e 32\n v = v \u003c\u003c 32\n return i-v\n }\n\nAvailable since\n 2020.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9006",
+ "Default": true
+ },
+ {
+ "Name": "SA9007",
+ "Doc": "Deleting a directory that shouldn't be deleted\n\nIt is virtually never correct to delete system directories such as\n/tmp or the user's home directory. However, it can be fairly easy to\ndo by mistake, for example by mistakenly using os.TempDir instead\nof ioutil.TempDir, or by forgetting to add a suffix to the result\nof os.UserHomeDir.\n\nWriting\n\n d := os.TempDir()\n defer os.RemoveAll(d)\n\nin your unit tests will have a devastating effect on the stability of your system.\n\nThis check flags attempts at deleting the following directories:\n\n- os.TempDir\n- os.UserCacheDir\n- os.UserConfigDir\n- os.UserHomeDir\n\nAvailable since\n 2022.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9007",
+ "Default": false
+ },
+ {
+ "Name": "SA9008",
+ "Doc": "else branch of a type assertion is probably not reading the right value\n\nWhen declaring variables as part of an if statement (like in 'if\nfoo := ...; foo {'), the same variables will also be in the scope of\nthe else branch. This means that in the following example\n\n if x, ok := x.(int); ok {\n // ...\n } else {\n fmt.Printf(\"unexpected type %T\", x)\n }\n\nx in the else branch will refer to the x from x, ok\n:=; it will not refer to the x that is being type-asserted. The\nresult of a failed type assertion is the zero value of the type that\nis being asserted to, so x in the else branch will always have the\nvalue 0 and the type int.\n\nAvailable since\n 2022.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9008",
+ "Default": false
+ },
+ {
+ "Name": "SA9009",
+ "Doc": "Ineffectual Go compiler directive\n\nA potential Go compiler directive was found, but is ineffectual as it begins\nwith whitespace.\n\nAvailable since\n 2024.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#SA9009",
+ "Default": true
+ },
+ {
+ "Name": "ST1000",
+ "Doc": "Incorrect or missing package comment\n\nPackages must have a package comment that is formatted according to\nthe guidelines laid out in\nhttps://go.dev/wiki/CodeReviewComments#package-comments.\n\nAvailable since\n 2019.1, non-default\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1000",
+ "Default": false
+ },
+ {
+ "Name": "ST1001",
+ "Doc": "Dot imports are discouraged\n\nDot imports that aren't in external test packages are discouraged.\n\nThe dot_import_whitelist option can be used to whitelist certain\nimports.\n\nQuoting Go Code Review Comments:\n\n\u003e The import . form can be useful in tests that, due to circular\n\u003e dependencies, cannot be made part of the package being tested:\n\u003e \n\u003e package foo_test\n\u003e \n\u003e import (\n\u003e \"bar/testutil\" // also imports \"foo\"\n\u003e . \"foo\"\n\u003e )\n\u003e \n\u003e In this case, the test file cannot be in package foo because it\n\u003e uses bar/testutil, which imports foo. So we use the import .\n\u003e form to let the file pretend to be part of package foo even though\n\u003e it is not. Except for this one case, do not use import . in your\n\u003e programs. It makes the programs much harder to read because it is\n\u003e unclear whether a name like Quux is a top-level identifier in the\n\u003e current package or in an imported package.\n\nAvailable since\n 2019.1\n\nOptions\n dot_import_whitelist\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1001",
+ "Default": false
+ },
+ {
+ "Name": "ST1003",
+ "Doc": "Poorly chosen identifier\n\nIdentifiers, such as variable and package names, follow certain rules.\n\nSee the following links for details:\n\n- https://go.dev/doc/effective_go#package-names\n- https://go.dev/doc/effective_go#mixed-caps\n- https://go.dev/wiki/CodeReviewComments#initialisms\n- https://go.dev/wiki/CodeReviewComments#variable-names\n\nAvailable since\n 2019.1, non-default\n\nOptions\n initialisms\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1003",
+ "Default": false
+ },
+ {
+ "Name": "ST1005",
+ "Doc": "Incorrectly formatted error string\n\nError strings follow a set of guidelines to ensure uniformity and good\ncomposability.\n\nQuoting Go Code Review Comments:\n\n\u003e Error strings should not be capitalized (unless beginning with\n\u003e proper nouns or acronyms) or end with punctuation, since they are\n\u003e usually printed following other context. That is, use\n\u003e fmt.Errorf(\"something bad\") not fmt.Errorf(\"Something bad\"), so\n\u003e that log.Printf(\"Reading %s: %v\", filename, err) formats without a\n\u003e spurious capital letter mid-message.\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1005",
+ "Default": false
+ },
+ {
+ "Name": "ST1006",
+ "Doc": "Poorly chosen receiver name\n\nQuoting Go Code Review Comments:\n\n\u003e The name of a method's receiver should be a reflection of its\n\u003e identity; often a one or two letter abbreviation of its type\n\u003e suffices (such as \"c\" or \"cl\" for \"Client\"). Don't use generic\n\u003e names such as \"me\", \"this\" or \"self\", identifiers typical of\n\u003e object-oriented languages that place more emphasis on methods as\n\u003e opposed to functions. The name need not be as descriptive as that\n\u003e of a method argument, as its role is obvious and serves no\n\u003e documentary purpose. It can be very short as it will appear on\n\u003e almost every line of every method of the type; familiarity admits\n\u003e brevity. Be consistent, too: if you call the receiver \"c\" in one\n\u003e method, don't call it \"cl\" in another.\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1006",
+ "Default": false
+ },
+ {
+ "Name": "ST1008",
+ "Doc": "A function's error value should be its last return value\n\nA function's error value should be its last return value.\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1008",
+ "Default": false
+ },
+ {
+ "Name": "ST1011",
+ "Doc": "Poorly chosen name for variable of type time.Duration\n\ntime.Duration values represent an amount of time, which is represented\nas a count of nanoseconds. An expression like 5 * time.Microsecond\nyields the value 5000. It is therefore not appropriate to suffix a\nvariable of type time.Duration with any time unit, such as Msec or\nMilli.\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1011",
+ "Default": false
+ },
+ {
+ "Name": "ST1012",
+ "Doc": "Poorly chosen name for error variable\n\nError variables that are part of an API should be called errFoo or\nErrFoo.\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1012",
+ "Default": false
+ },
+ {
+ "Name": "ST1013",
+ "Doc": "Should use constants for HTTP error codes, not magic numbers\n\nHTTP has a tremendous number of status codes. While some of those are\nwell known (200, 400, 404, 500), most of them are not. The net/http\npackage provides constants for all status codes that are part of the\nvarious specifications. It is recommended to use these constants\ninstead of hard-coding magic numbers, to vastly improve the\nreadability of your code.\n\nAvailable since\n 2019.1\n\nOptions\n http_status_code_whitelist\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1013",
+ "Default": false
+ },
+ {
+ "Name": "ST1015",
+ "Doc": "A switch's default case should be the first or last case\n\nAvailable since\n 2019.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1015",
+ "Default": false
+ },
+ {
+ "Name": "ST1016",
+ "Doc": "Use consistent method receiver names\n\nAvailable since\n 2019.1, non-default\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1016",
+ "Default": false
+ },
+ {
+ "Name": "ST1017",
+ "Doc": "Don't use Yoda conditions\n\nYoda conditions are conditions of the kind 'if 42 == x', where the\nliteral is on the left side of the comparison. These are a common\nidiom in languages in which assignment is an expression, to avoid bugs\nof the kind 'if (x = 42)'. In Go, which doesn't allow for this kind of\nbug, we prefer the more idiomatic 'if x == 42'.\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1017",
+ "Default": false
+ },
+ {
+ "Name": "ST1018",
+ "Doc": "Avoid zero-width and control characters in string literals\n\nAvailable since\n 2019.2\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1018",
+ "Default": false
+ },
+ {
+ "Name": "ST1019",
+ "Doc": "Importing the same package multiple times\n\nGo allows importing the same package multiple times, as long as\ndifferent import aliases are being used. That is, the following\nbit of code is valid:\n\n import (\n \"fmt\"\n fumpt \"fmt\"\n format \"fmt\"\n _ \"fmt\"\n )\n\nHowever, this is very rarely done on purpose. Usually, it is a\nsign of code that got refactored, accidentally adding duplicate\nimport statements. It is also a rarely known feature, which may\ncontribute to confusion.\n\nDo note that sometimes, this feature may be used\nintentionally (see for example\nhttps://github.com/golang/go/commit/3409ce39bfd7584523b7a8c150a310cea92d879d)\n– if you want to allow this pattern in your code base, you're\nadvised to disable this check.\n\nAvailable since\n 2020.1\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1019",
+ "Default": false
+ },
+ {
+ "Name": "ST1020",
+ "Doc": "The documentation of an exported function should start with the function's name\n\nDoc comments work best as complete sentences, which\nallow a wide variety of automated presentations. The first sentence\nshould be a one-sentence summary that starts with the name being\ndeclared.\n\nIf every doc comment begins with the name of the item it describes,\nyou can use the doc subcommand of the go tool and run the output\nthrough grep.\n\nSee https://go.dev/doc/effective_go#commentary for more\ninformation on how to write good documentation.\n\nAvailable since\n 2020.1, non-default\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1020",
+ "Default": false
+ },
+ {
+ "Name": "ST1021",
+ "Doc": "The documentation of an exported type should start with type's name\n\nDoc comments work best as complete sentences, which\nallow a wide variety of automated presentations. The first sentence\nshould be a one-sentence summary that starts with the name being\ndeclared.\n\nIf every doc comment begins with the name of the item it describes,\nyou can use the doc subcommand of the go tool and run the output\nthrough grep.\n\nSee https://go.dev/doc/effective_go#commentary for more\ninformation on how to write good documentation.\n\nAvailable since\n 2020.1, non-default\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1021",
+ "Default": false
+ },
+ {
+ "Name": "ST1022",
+ "Doc": "The documentation of an exported variable or constant should start with variable's name\n\nDoc comments work best as complete sentences, which\nallow a wide variety of automated presentations. The first sentence\nshould be a one-sentence summary that starts with the name being\ndeclared.\n\nIf every doc comment begins with the name of the item it describes,\nyou can use the doc subcommand of the go tool and run the output\nthrough grep.\n\nSee https://go.dev/doc/effective_go#commentary for more\ninformation on how to write good documentation.\n\nAvailable since\n 2020.1, non-default\n",
+ "URL": "https://staticcheck.dev/docs/checks/#ST1022",
+ "Default": false
+ },
+ {
+ "Name": "ST1023",
+ "Doc": "Redundant type in variable declaration\n\nAvailable since\n 2021.1, non-default\n",
+ "URL": "https://staticcheck.dev/docs/checks/#",
+ "Default": false
+ },
{
"Name": "appends",
"Doc": "check for missing values after append\n\nThis checker reports calls to append that pass\nno values to be appended to the slice.\n\n\ts := []string{\"a\", \"b\", \"c\"}\n\t_ = append(s)\n\nSuch calls are always no-ops and often indicate an\nunderlying mistake.",
@@ -1148,13 +3171,13 @@
{
"Name": "gofix",
"Doc": "apply fixes based on go:fix comment directives\n\nThe gofix analyzer inlines functions and constants that are marked for inlining.",
- "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix",
+ "URL": "https://pkg.go.dev/golang.org/x/tools/internal/gofix",
"Default": true
},
{
"Name": "hostport",
"Doc": "check format of addresses passed to net.Dial\n\nThis analyzer flags code that produce network address strings using\nfmt.Sprintf, as in this example:\n\n addr := fmt.Sprintf(\"%s:%d\", host, 12345) // \"will not work with IPv6\"\n ...\n conn, err := net.Dial(\"tcp\", addr) // \"when passed to dial here\"\n\nThe analyzer suggests a fix to use the correct approach, a call to\nnet.JoinHostPort:\n\n addr := net.JoinHostPort(host, \"12345\")\n ...\n conn, err := net.Dial(\"tcp\", addr)\n\nA similar diagnostic and fix are produced for a format string of \"%s:%s\".\n",
- "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/hostport",
+ "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/hostport",
"Default": true
},
{
@@ -1189,7 +3212,7 @@
},
{
"Name": "modernize",
- "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;",
+ "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go and its standard\nlibrary.\n\nEach diagnostic provides a fix. Our intent is that these fixes may\nbe safely applied en masse without changing the behavior of your\nprogram. In some cases the suggested fixes are imperfect and may\nlead to (for example) unused imports or unused local variables,\ncausing build breakage. However, these problems are generally\ntrivial to fix. We regard any modernizer whose fix changes program\nbehavior to have a serious bug and will endeavor to fix it.\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...\n\n(Do not use \"go get -tool\" to add gopls as a dependency of your\nmodule; gopls commands must be built from their release branch.)\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.\n\nChanges produced by this tool should be reviewed as usual before\nbeing merged. In some cases, a loop may be replaced by a simple\nfunction call, causing comments within the loop to be discarded.\nHuman judgment may be required to avoid losing comments of value.\n\nEach diagnostic reported by modernize has a specific category. (The\ncategories are listed below.) Diagnostics in some categories, such\nas \"efaceany\" (which replaces \"interface{}\" with \"any\" where it is\nsafe to do so) are particularly numerous. It may ease the burden of\ncode review to apply fixes in two passes, the first change\nconsisting only of fixes of category \"efaceany\", the second\nconsisting of all others. This can be achieved using the -category flag:\n\n\t$ modernize -category=efaceany -fix -test ./...\n\t$ modernize -category=-efaceany -fix -test ./...\n\nCategories of modernize diagnostic:\n\n - forvar: remove x := x variable declarations made unnecessary by the new semantics of loops in go1.22.\n\n - slicescontains: replace 'for i, elem := range s { if elem == needle { ...; break }'\n by a call to slices.Contains, added in go1.21.\n\n - minmax: replace an if/else conditional assignment by a call to\n the built-in min or max functions added in go1.21.\n\n - sortslice: replace sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21.\n\n - efaceany: replace interface{} by the 'any' type added in go1.18.\n\n - slicesclone: replace append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21.\n\n - mapsloop: replace a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions from\n the maps package, added in go1.21.\n\n - fmtappendf: replace []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19.\n\n - testingcontext: replace uses of context.WithCancel in tests\n with t.Context, added in go1.24.\n\n - omitzero: replace omitempty by omitzero on structs, added in go1.24.\n\n - bloop: replace \"for i := range b.N\" or \"for range b.N\" in a\n benchmark with \"for b.Loop()\", and remove any preceding calls\n to b.StopTimer, b.StartTimer, and b.ResetTimer.\n\n - slicesdelete: replace append(s[:i], s[i+1]...) by\n slices.Delete(s, i, i+1), added in go1.21.\n\n - rangeint: replace a 3-clause \"for i := 0; i \u003c n; i++\" loop by\n \"for i := range n\", added in go1.22.\n\n - stringsseq: replace Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq, or Fields with FieldSeq.\n\n - stringscutprefix: replace some uses of HasPrefix followed by TrimPrefix with CutPrefix,\n added to the strings package in go1.20.\n\n - waitgroup: replace old complex usages of sync.WaitGroup by less complex WaitGroup.Go method in go1.25.",
"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize",
"Default": true
},
@@ -1333,7 +3356,7 @@
},
{
"Name": "unusedfunc",
- "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report a false positive for a declaration of an\nunexported function that is referenced from another package using\nthe go:linkname mechanism, if the declaration's doc comment does\nnot also have a go:linkname comment. (Such code is in any case\nstrongly discouraged: linkname annotations, if they must be used at\nall, should be used on both the declaration and the alias.)\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.",
+ "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report false positives in some situations, for\nexample:\n\n - For a declaration of an unexported function that is referenced\n from another package using the go:linkname mechanism, if the\n declaration's doc comment does not also have a go:linkname\n comment.\n\n (Such code is in any case strongly discouraged: linkname\n annotations, if they must be used at all, should be used on both\n the declaration and the alias.)\n\n - For compiler intrinsics in the \"runtime\" package that, though\n never referenced, are known to the compiler and are called\n indirectly by compiled object code.\n\n - For functions called only from assembly.\n\n - For functions called only from files whose build tags are not\n selected in the current build configuration.\n\nSee https://github.com/golang/go/issues/71686 for discussion of\nthese limitations.\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.",
"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedfunc",
"Default": true
},
@@ -1378,37 +3401,44 @@
{
"Name": "assignVariableTypes",
"Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```\n",
- "Default": false
+ "Default": false,
+ "Status": ""
},
{
"Name": "compositeLiteralFields",
"Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```\n",
- "Default": false
+ "Default": false,
+ "Status": ""
},
{
"Name": "compositeLiteralTypes",
"Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n",
- "Default": false
+ "Default": false,
+ "Status": ""
},
{
"Name": "constantValues",
"Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```\n",
- "Default": false
+ "Default": false,
+ "Status": ""
},
{
"Name": "functionTypeParameters",
"Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```\n",
- "Default": false
+ "Default": false,
+ "Status": ""
},
{
"Name": "parameterNames",
"Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n",
- "Default": false
+ "Default": false,
+ "Status": ""
},
{
"Name": "rangeVariableTypes",
"Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n",
- "Default": false
+ "Default": false,
+ "Status": ""
}
]
}
\ No newline at end of file
diff --git a/gopls/doc/generate/generate.go b/gopls/internal/doc/generate/generate.go
similarity index 93%
rename from gopls/doc/generate/generate.go
rename to gopls/internal/doc/generate/generate.go
index b0d3e8c49f6..d470fb71333 100644
--- a/gopls/doc/generate/generate.go
+++ b/gopls/internal/doc/generate/generate.go
@@ -11,9 +11,7 @@
//
// Run it with this command:
//
-// $ cd gopls/internal/doc && go generate
-//
-// TODO(adonovan): move this package to gopls/internal/doc/generate.
+// $ cd gopls/internal/doc/generate && go generate
package main
import (
@@ -138,7 +136,7 @@ func loadAPI() (*doc.API, error) {
defaults := settings.DefaultOptions()
api := &doc.API{
Options: map[string][]*doc.Option{},
- Analyzers: loadAnalyzers(settings.DefaultAnalyzers), // no staticcheck analyzers
+ Analyzers: loadAnalyzers(settings.AllAnalyzers, defaults),
}
api.Lenses, err = loadLenses(settingsPkg, defaults.Codelenses)
@@ -319,9 +317,17 @@ func loadEnums(pkg *packages.Package) (map[types.Type][]doc.EnumValue, error) {
spec := path[1].(*ast.ValueSpec)
value := cnst.Val().ExactString()
docstring := valueDoc(cnst.Name(), value, spec.Doc.Text())
+ var status string
+ for _, d := range internalastutil.Directives(spec.Doc) {
+ if d.Tool == "gopls" && d.Name == "status" {
+ status = d.Args
+ break
+ }
+ }
v := doc.EnumValue{
- Value: value,
- Doc: docstring,
+ Value: value,
+ Doc: docstring,
+ Status: status,
}
enums[obj.Type()] = append(enums[obj.Type()], v)
}
@@ -356,6 +362,7 @@ func collectEnumKeys(m *types.Map, reflectField reflect.Value, enumValues []doc.
keys = append(keys, doc.EnumKey{
Name: v.Value,
Doc: v.Doc,
+ Status: v.Status,
Default: def,
})
}
@@ -438,6 +445,7 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou
// Find the CodeLensSource enums among the files of the protocol package.
// Map each enum value to its doc comment.
enumDoc := make(map[string]string)
+ enumStatus := make(map[string]string)
for _, f := range settingsPkg.Syntax {
for _, decl := range f.Decls {
if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.CONST {
@@ -457,6 +465,12 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou
return nil, fmt.Errorf("%s: %s lacks doc comment", posn, spec.Names[0].Name)
}
enumDoc[value] = spec.Doc.Text()
+ for _, d := range internalastutil.Directives(spec.Doc) {
+ if d.Tool == "gopls" && d.Name == "status" {
+ enumStatus[value] = d.Args
+ break
+ }
+ }
}
}
}
@@ -481,6 +495,7 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou
Title: title,
Doc: docText,
Default: defaults[source],
+ Status: enumStatus[string(source)],
})
}
return nil
@@ -490,20 +505,17 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou
return lenses, nil
}
-func loadAnalyzers(m map[string]*settings.Analyzer) []*doc.Analyzer {
- var sorted []string
- for _, a := range m {
- sorted = append(sorted, a.Analyzer().Name)
- }
- sort.Strings(sorted)
+func loadAnalyzers(analyzers []*settings.Analyzer, defaults *settings.Options) []*doc.Analyzer {
+ slices.SortFunc(analyzers, func(x, y *settings.Analyzer) int {
+ return strings.Compare(x.Analyzer().Name, y.Analyzer().Name)
+ })
var json []*doc.Analyzer
- for _, name := range sorted {
- a := m[name]
+ for _, a := range analyzers {
json = append(json, &doc.Analyzer{
Name: a.Analyzer().Name,
Doc: a.Analyzer().Doc,
URL: a.Analyzer().URL,
- Default: a.EnabledByDefault(),
+ Default: a.Enabled(defaults),
})
}
return json
@@ -520,8 +532,9 @@ func loadHints(settingsPkg *packages.Package) ([]*doc.Hint, error) {
for _, enumVal := range enums[inlayHint] {
name, _ := strconv.Unquote(enumVal.Value)
hints = append(hints, &doc.Hint{
- Name: name,
- Doc: enumVal.Doc,
+ Name: name,
+ Doc: enumVal.Doc,
+ Status: enumVal.Status,
})
}
return hints, nil
@@ -602,17 +615,7 @@ func rewriteSettings(prevContent []byte, api *doc.API) ([]byte, error) {
fmt.Fprintf(&buf, "### `%s %s`\n\n", opt.Name, opt.Type)
// status
- switch opt.Status {
- case "":
- case "advanced":
- fmt.Fprint(&buf, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n")
- case "debug":
- fmt.Fprint(&buf, "**This setting is for debugging purposes only.**\n\n")
- case "experimental":
- fmt.Fprint(&buf, "**This setting is experimental and may be deleted.**\n\n")
- default:
- fmt.Fprintf(&buf, "**Status: %s.**\n\n", opt.Status)
- }
+ writeStatus(&buf, opt.Status)
// doc comment
buf.WriteString(opt.Doc)
@@ -653,6 +656,22 @@ func rewriteSettings(prevContent []byte, api *doc.API) ([]byte, error) {
return content, nil
}
+// writeStatus emits a Markdown paragraph to buf about the status of a feature,
+// if nonempty.
+func writeStatus(buf *bytes.Buffer, status string) {
+ switch status {
+ case "":
+ case "advanced":
+ fmt.Fprint(buf, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n")
+ case "debug":
+ fmt.Fprint(buf, "**This setting is for debugging purposes only.**\n\n")
+ case "experimental":
+ fmt.Fprint(buf, "**This setting is experimental and may be deleted.**\n\n")
+ default:
+ fmt.Fprintf(buf, "**Status: %s.**\n\n", status)
+ }
+}
+
var parBreakRE = regexp.MustCompile("\n{2,}")
func shouldShowEnumKeysInSettings(name string) bool {
@@ -724,6 +743,7 @@ func rewriteCodeLenses(prevContent []byte, api *doc.API) ([]byte, error) {
var buf bytes.Buffer
for _, lens := range api.Lenses {
fmt.Fprintf(&buf, "## `%s`: %s\n\n", lens.Lens, lens.Title)
+ writeStatus(&buf, lens.Status)
fmt.Fprintf(&buf, "%s\n\n", lens.Doc)
fmt.Fprintf(&buf, "Default: %v\n\n", onOff(lens.Default))
fmt.Fprintf(&buf, "File type: %s\n\n", lens.FileType)
@@ -782,7 +802,7 @@ func replaceSection(content []byte, sectionName string, replacement []byte) ([]b
if idx == nil {
return nil, fmt.Errorf("could not find section %q", sectionName)
}
- result := append([]byte(nil), content[:idx[2]]...)
+ result := slices.Clone(content[:idx[2]])
result = append(result, replacement...)
result = append(result, content[idx[3]:]...)
return result, nil
diff --git a/gopls/doc/generate/generate_test.go b/gopls/internal/doc/generate/generate_test.go
similarity index 100%
rename from gopls/doc/generate/generate_test.go
rename to gopls/internal/doc/generate/generate_test.go
diff --git a/gopls/internal/file/file.go b/gopls/internal/file/file.go
index 5f8be06cf60..b817306aa07 100644
--- a/gopls/internal/file/file.go
+++ b/gopls/internal/file/file.go
@@ -49,6 +49,8 @@ type Handle interface {
// Content returns the contents of a file.
// If the file is not available, returns a nil slice and an error.
Content() ([]byte, error)
+ // String returns the file's path.
+ String() string
}
// A Source maps URIs to Handles.
diff --git a/gopls/internal/file/kind.go b/gopls/internal/file/kind.go
index 087a57f32d0..6a0ed009ed5 100644
--- a/gopls/internal/file/kind.go
+++ b/gopls/internal/file/kind.go
@@ -28,6 +28,8 @@ const (
Tmpl
// Work is a go.work file.
Work
+ // Asm is a Go assembly (.s) file.
+ Asm
)
func (k Kind) String() string {
@@ -42,13 +44,15 @@ func (k Kind) String() string {
return "tmpl"
case Work:
return "go.work"
+ case Asm:
+ return "Go assembly"
default:
return fmt.Sprintf("internal error: unknown file kind %d", k)
}
}
// KindForLang returns the gopls file [Kind] associated with the given LSP
-// LanguageKind string from protocol.TextDocumentItem.LanguageID,
+// LanguageKind string from the LanguageID field of [protocol.TextDocumentItem],
// or UnknownKind if the language is not one recognized by gopls.
func KindForLang(langID protocol.LanguageKind) Kind {
switch langID {
@@ -62,6 +66,8 @@ func KindForLang(langID protocol.LanguageKind) Kind {
return Tmpl
case "go.work":
return Work
+ case "go.s":
+ return Asm
default:
return UnknownKind
}
diff --git a/gopls/internal/filecache/filecache_test.go b/gopls/internal/filecache/filecache_test.go
index 3419db4b513..4dbc04490f5 100644
--- a/gopls/internal/filecache/filecache_test.go
+++ b/gopls/internal/filecache/filecache_test.go
@@ -100,7 +100,6 @@ func TestConcurrency(t *testing.T) {
// there is no third possibility.
var group errgroup.Group
for i := range values {
- i := i
group.Go(func() error { return filecache.Set(kind, key, values[i][:]) })
group.Go(func() error { return get(false) })
}
@@ -217,12 +216,12 @@ func BenchmarkUncontendedGet(b *testing.B) {
if err := filecache.Set(kind, key, value[:]); err != nil {
b.Fatal(err)
}
- b.ResetTimer()
+
b.SetBytes(int64(len(value)))
var group errgroup.Group
group.SetLimit(50)
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
group.Go(func() error {
_, err := filecache.Get(kind, key)
return err
@@ -246,7 +245,7 @@ func BenchmarkUncontendedSet(b *testing.B) {
const P = 1000 // parallelism
b.SetBytes(P * int64(len(value)))
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
// Perform P concurrent calls to Set. All must succeed.
var group errgroup.Group
for range [P]bool{} {
diff --git a/gopls/internal/fuzzy/input.go b/gopls/internal/fuzzy/input.go
index c1038163f1a..fd8575f6382 100644
--- a/gopls/internal/fuzzy/input.go
+++ b/gopls/internal/fuzzy/input.go
@@ -36,7 +36,7 @@ func RuneRoles(candidate []byte, reuse []RuneRole) []RuneRole {
}
prev, prev2 := rtNone, rtNone
- for i := 0; i < len(candidate); i++ {
+ for i := range candidate {
r := rune(candidate[i])
role := RNone
@@ -122,7 +122,7 @@ func LastSegment(input string, roles []RuneRole) string {
func fromChunks(chunks []string, buffer []byte) []byte {
ii := 0
for _, chunk := range chunks {
- for i := 0; i < len(chunk); i++ {
+ for i := range len(chunk) {
if ii >= cap(buffer) {
break
}
@@ -143,7 +143,7 @@ func toLower(input []byte, reuse []byte) []byte {
output = make([]byte, len(input))
}
- for i := 0; i < len(input); i++ {
+ for i := range input {
r := rune(input[i])
if input[i] <= unicode.MaxASCII {
if 'A' <= r && r <= 'Z' {
diff --git a/gopls/internal/fuzzy/input_test.go b/gopls/internal/fuzzy/input_test.go
index ffe147241b6..dd751b8f0c2 100644
--- a/gopls/internal/fuzzy/input_test.go
+++ b/gopls/internal/fuzzy/input_test.go
@@ -127,7 +127,7 @@ func BenchmarkRoles(b *testing.B) {
str := "AbstractSWTFactory"
out := make([]fuzzy.RuneRole, len(str))
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
fuzzy.RuneRoles([]byte(str), out)
}
b.SetBytes(int64(len(str)))
diff --git a/gopls/internal/fuzzy/matcher.go b/gopls/internal/fuzzy/matcher.go
index 29d1b36501e..eff86efac34 100644
--- a/gopls/internal/fuzzy/matcher.go
+++ b/gopls/internal/fuzzy/matcher.go
@@ -134,10 +134,7 @@ func (m *Matcher) ScoreChunks(chunks []string) float32 {
if sc < 0 {
sc = 0
}
- normalizedScore := float32(sc) * m.scoreScale
- if normalizedScore > 1 {
- normalizedScore = 1
- }
+ normalizedScore := min(float32(sc)*m.scoreScale, 1)
return normalizedScore
}
@@ -177,7 +174,7 @@ func (m *Matcher) MatchedRanges() []int {
i--
}
// Reverse slice.
- for i := 0; i < len(ret)/2; i++ {
+ for i := range len(ret) / 2 {
ret[i], ret[len(ret)-1-i] = ret[len(ret)-1-i], ret[i]
}
return ret
@@ -211,7 +208,7 @@ func (m *Matcher) computeScore(candidate []byte, candidateLower []byte) int {
m.scores[0][0][0] = score(0, 0) // Start with 0.
segmentsLeft, lastSegStart := 1, 0
- for i := 0; i < candLen; i++ {
+ for i := range candLen {
if m.roles[i] == RSep {
segmentsLeft++
lastSegStart = i + 1
@@ -304,7 +301,7 @@ func (m *Matcher) computeScore(candidate []byte, candidateLower []byte) int {
// Third dimension encodes whether there is a gap between the previous match and the current
// one.
- for k := 0; k < 2; k++ {
+ for k := range 2 {
sc := m.scores[i-1][j-1][k].val() + charScore
isConsecutive := k == 1 || i-1 == 0 || i-1 == lastSegStart
@@ -342,7 +339,7 @@ func (m *Matcher) ScoreTable(candidate string) string {
var line1, line2, separator bytes.Buffer
line1.WriteString("\t")
line2.WriteString("\t")
- for j := 0; j < len(m.pattern); j++ {
+ for j := range len(m.pattern) {
line1.WriteString(fmt.Sprintf("%c\t\t", m.pattern[j]))
separator.WriteString("----------------")
}
diff --git a/gopls/internal/fuzzy/matcher_test.go b/gopls/internal/fuzzy/matcher_test.go
index 056da25d675..f743be0c5ef 100644
--- a/gopls/internal/fuzzy/matcher_test.go
+++ b/gopls/internal/fuzzy/matcher_test.go
@@ -293,8 +293,7 @@ func BenchmarkMatcher(b *testing.B) {
matcher := fuzzy.NewMatcher(pattern)
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
for _, c := range candidates {
matcher.Score(c)
}
diff --git a/gopls/internal/fuzzy/self_test.go b/gopls/internal/fuzzy/self_test.go
index 1c64f1953df..7cdb4fdef96 100644
--- a/gopls/internal/fuzzy/self_test.go
+++ b/gopls/internal/fuzzy/self_test.go
@@ -14,7 +14,7 @@ func BenchmarkSelf_Matcher(b *testing.B) {
idents := collectIdentifiers(b)
patterns := generatePatterns()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
for _, pattern := range patterns {
sm := NewMatcher(pattern)
for _, ident := range idents {
@@ -28,7 +28,7 @@ func BenchmarkSelf_SymbolMatcher(b *testing.B) {
idents := collectIdentifiers(b)
patterns := generatePatterns()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
for _, pattern := range patterns {
sm := NewSymbolMatcher(pattern)
for _, ident := range idents {
diff --git a/gopls/internal/goasm/definition.go b/gopls/internal/goasm/definition.go
new file mode 100644
index 00000000000..903916d265d
--- /dev/null
+++ b/gopls/internal/goasm/definition.go
@@ -0,0 +1,136 @@
+// Copyright 2025 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 goasm provides language-server features for files in Go
+// assembly language (https://go.dev/doc/asm).
+package goasm
+
+import (
+ "context"
+ "fmt"
+ "go/token"
+
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/util/asm"
+ "golang.org/x/tools/gopls/internal/util/morestrings"
+ "golang.org/x/tools/internal/event"
+)
+
+// Definition handles the textDocument/definition request for Go assembly files.
+func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) {
+ ctx, done := event.Start(ctx, "goasm.Definition")
+ defer done()
+
+ mp, err := snapshot.NarrowestMetadataForFile(ctx, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+
+ // Read the file.
+ content, err := fh.Content()
+ if err != nil {
+ return nil, err
+ }
+ mapper := protocol.NewMapper(fh.URI(), content)
+ offset, err := mapper.PositionOffset(position)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse the assembly.
+ //
+ // TODO(adonovan): make this just another
+ // attribute of the type-checked cache.Package.
+ file := asm.Parse(content)
+
+ // Figure out the selected symbol.
+ // For now, just find the identifier around the cursor.
+ var found *asm.Ident
+ for _, id := range file.Idents {
+ if id.Offset <= offset && offset <= id.End() {
+ found = &id
+ break
+ }
+ }
+ if found == nil {
+ return nil, fmt.Errorf("not an identifier")
+ }
+
+ // Resolve a symbol with a "." prefix to the current package.
+ sym := found.Name
+ if sym != "" && sym[0] == '.' {
+ sym = string(mp.PkgPath) + sym
+ }
+
+ // package-qualified symbol?
+ if pkgpath, name, ok := morestrings.CutLast(sym, "."); ok {
+ // Find declaring package among dependencies.
+ //
+ // TODO(adonovan): assembly may legally reference
+ // non-dependencies. For example, sync/atomic calls
+ // internal/runtime/atomic. Perhaps we should search
+ // the entire metadata graph, but that's path-dependent.
+ var declaring *metadata.Package
+ for pkg := range snapshot.MetadataGraph().ForwardReflexiveTransitiveClosure(mp.ID) {
+ if pkg.PkgPath == metadata.PackagePath(pkgpath) {
+ declaring = pkg
+ break
+ }
+ }
+ if declaring == nil {
+ return nil, fmt.Errorf("package %q is not a dependency", pkgpath)
+ }
+
+ pkgs, err := snapshot.TypeCheck(ctx, declaring.ID)
+ if err != nil {
+ return nil, err
+ }
+ pkg := pkgs[0]
+ def := pkg.Types().Scope().Lookup(name)
+ if def == nil {
+ return nil, fmt.Errorf("no symbol %q in package %q", name, pkgpath)
+ }
+ loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, def.Pos(), def.Pos())
+ if err == nil {
+ return []protocol.Location{loc}, nil
+ }
+
+ } else {
+ // local symbols (funcs, vars, labels)
+ for _, id := range file.Idents {
+ if id.Name == found.Name &&
+ (id.Kind == asm.Text || id.Kind == asm.Global || id.Kind == asm.Label) {
+
+ loc, err := mapper.OffsetLocation(id.Offset, id.End())
+ if err != nil {
+ return nil, err
+ }
+ return []protocol.Location{loc}, nil
+ }
+ }
+ }
+
+ return nil, nil
+}
+
+// TODO(rfindley): avoid the duplicate column mapping here, by associating a
+// column mapper with each file handle.
+// TODO(adonovan): plundered from ../golang; factor.
+func mapPosition(ctx context.Context, fset *token.FileSet, s file.Source, start, end token.Pos) (protocol.Location, error) {
+ file := fset.File(start)
+ uri := protocol.URIFromPath(file.Name())
+ fh, err := s.ReadFile(ctx, uri)
+ if err != nil {
+ return protocol.Location{}, err
+ }
+ content, err := fh.Content()
+ if err != nil {
+ return protocol.Location{}, err
+ }
+ m := protocol.NewMapper(fh.URI(), content)
+ return m.PosLocation(file, start, end)
+}
diff --git a/gopls/internal/golang/addtest.go b/gopls/internal/golang/addtest.go
index 4a43a82ffee..3a5b1e03308 100644
--- a/gopls/internal/golang/addtest.go
+++ b/gopls/internal/golang/addtest.go
@@ -319,7 +319,7 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol.
// package decl based on the originating file.
// Search for something that looks like a copyright header, to replicate
// in the new file.
- if c := copyrightComment(pgf.File); c != nil {
+ if c := CopyrightComment(pgf.File); c != nil {
start, end, err := pgf.NodeOffsets(c)
if err != nil {
return nil, err
@@ -480,8 +480,6 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol.
},
}
- errorType := types.Universe.Lookup("error").Type()
-
var isContextType = func(t types.Type) bool {
named, ok := t.(*types.Named)
if !ok {
diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go
index 3b778a54697..12244a58c59 100644
--- a/gopls/internal/golang/assembly.go
+++ b/gopls/internal/golang/assembly.go
@@ -10,12 +10,17 @@ package golang
// - ./codeaction.go - computes the symbol and offers the CodeAction command.
// - ../server/command.go - handles the command by opening a web page.
// - ../server/server.go - handles the HTTP request and calls this function.
+//
+// For language-server behavior in Go assembly language files,
+// see [golang.org/x/tools/gopls/internal/goasm].
import (
"bytes"
"context"
"fmt"
"html"
+ "io"
+ "net/http"
"regexp"
"strconv"
"strings"
@@ -26,39 +31,35 @@ import (
// AssemblyHTML returns an HTML document containing an assembly listing of the selected function.
//
-// TODO(adonovan):
-// - display a "Compiling..." message as a cold build can be slow.
-// - cross-link jumps and block labels, like github.com/aclements/objbrowse.
-func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string, web Web) ([]byte, error) {
- // Compile the package with -S, and capture its stderr stream.
+// TODO(adonovan): cross-link jumps and block labels, like github.com/aclements/objbrowse.
+//
+// See gopls/internal/test/integration/misc/webserver_test.go for tests.
+func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, w http.ResponseWriter, pkg *cache.Package, symbol string, web Web) {
+ // Prepare to compile the package with -S, and capture its stderr stream.
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NoNetwork, pkg.Metadata().CompiledGoFiles[0].DirPath(), "build", []string{"-gcflags=-S", "."})
if err != nil {
- return nil, err // e.g. failed to write overlays (rare)
+ // e.g. failed to write overlays (rare)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
defer cleanupInvocation()
- _, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv)
- if err != nil {
- return nil, err // e.g. won't compile
- }
- content := stderr.String()
escape := html.EscapeString
- // Produce the report.
+ // Emit the start of the report.
title := fmt.Sprintf("%s assembly for %s",
escape(snapshot.View().GOARCH()),
escape(symbol))
- var buf bytes.Buffer
- buf.WriteString(`
+ io.WriteString(w, `
- Codestin Search App
+ Codestin Search App
-
@@ -69,11 +70,38 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
Click on a source line marker L1234 to navigate your editor there.
(VS Code users: please upvote #208093)
-
- Reload the page to recompile.
-
+
Compiling...
`)
+ if flusher, ok := w.(http.Flusher); ok {
+ flusher.Flush()
+ }
+
+ // At this point errors must be reported by writing HTML.
+ // To do this, set "status" return early.
+
+ var buf bytes.Buffer
+ status := "Reload the page to recompile."
+ defer func() {
+ // Update the "Compiling..." message.
+ fmt.Fprintf(&buf, `
+
+
+`, status)
+ w.Write(buf.Bytes())
+ }()
+
+ // Compile the package.
+ _, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv)
+ if err != nil {
+ status = fmt.Sprintf("compilation failed: %v", err)
+ return
+ }
+
+ // Write the rest of the report.
+ content := stderr.String()
// insnRx matches an assembly instruction line.
// Submatch groups are: (offset-hex-dec, file-line-column, instruction).
@@ -88,7 +116,7 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
//
// Allow matches of symbol, symbol.func1, symbol.deferwrap, etc.
on := false
- for _, line := range strings.Split(content, "\n") {
+ for line := range strings.SplitSeq(content, "\n") {
// start of function symbol?
if strings.Contains(line, " STEXT ") {
on = strings.HasPrefix(line, symbol) &&
@@ -116,5 +144,4 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
}
buf.WriteByte('\n')
}
- return buf.Bytes(), nil
}
diff --git a/gopls/internal/golang/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go
index 04dc9deeb5d..b9f21cd18d7 100644
--- a/gopls/internal/golang/call_hierarchy.go
+++ b/gopls/internal/golang/call_hierarchy.go
@@ -14,13 +14,16 @@ import (
"path/filepath"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
+ "golang.org/x/tools/gopls/internal/util/moremaps"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/typesinternal"
)
// PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file.
@@ -99,7 +102,7 @@ func IncomingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle
// Flatten the map of pointers into a slice of values.
incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls))
- for _, callItem := range incomingCalls {
+ for _, callItem := range moremaps.SortedFunc(incomingCalls, protocol.CompareLocation) {
incomingCallItems = append(incomingCallItems, *callItem)
}
return incomingCallItems, nil
@@ -247,30 +250,21 @@ func OutgoingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle
type callRange struct {
start, end token.Pos
}
- callRanges := []callRange{}
- ast.Inspect(declNode, func(n ast.Node) bool {
- if call, ok := n.(*ast.CallExpr); ok {
- var start, end token.Pos
- switch n := call.Fun.(type) {
- case *ast.SelectorExpr:
- start, end = n.Sel.NamePos, call.Lparen
- case *ast.Ident:
- start, end = n.NamePos, call.Lparen
- case *ast.FuncLit:
- // while we don't add the function literal as an 'outgoing' call
- // we still want to traverse into it
- return true
- default:
- // ignore any other kind of call expressions
- // for ex: direct function literal calls since that's not an 'outgoing' call
- return false
- }
- callRanges = append(callRanges, callRange{start: start, end: end})
+
+ // Find calls to known functions/methods, including interface methods.
+ var callRanges []callRange
+ for n := range ast.Preorder(declNode) {
+ if call, ok := n.(*ast.CallExpr); ok &&
+ is[*types.Func](typeutil.Callee(pkg.TypesInfo(), call)) {
+ id := typesinternal.UsedIdent(pkg.TypesInfo(), call.Fun)
+ callRanges = append(callRanges, callRange{
+ start: id.NamePos,
+ end: call.Lparen,
+ })
}
- return true
- })
+ }
- outgoingCalls := map[token.Pos]*protocol.CallHierarchyOutgoingCall{}
+ outgoingCalls := make(map[protocol.Location]*protocol.CallHierarchyOutgoingCall)
for _, callRange := range callRanges {
_, obj, _ := referencedObject(declPkg, declPGF, callRange.start)
if obj == nil {
@@ -280,12 +274,13 @@ func OutgoingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle
continue // built-ins have no position
}
- outgoingCall, ok := outgoingCalls[obj.Pos()]
+ loc, err := mapPosition(ctx, declPkg.FileSet(), snapshot, obj.Pos(), obj.Pos()+token.Pos(len(obj.Name())))
+ if err != nil {
+ return nil, err
+ }
+
+ outgoingCall, ok := outgoingCalls[loc]
if !ok {
- loc, err := mapPosition(ctx, declPkg.FileSet(), snapshot, obj.Pos(), obj.Pos()+token.Pos(len(obj.Name())))
- if err != nil {
- return nil, err
- }
outgoingCall = &protocol.CallHierarchyOutgoingCall{
To: protocol.CallHierarchyItem{
Name: obj.Name(),
@@ -297,7 +292,7 @@ func OutgoingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle
SelectionRange: loc.Range,
},
}
- outgoingCalls[obj.Pos()] = outgoingCall
+ outgoingCalls[loc] = outgoingCall
}
rng, err := declPGF.PosRange(callRange.start, callRange.end)
@@ -308,7 +303,7 @@ func OutgoingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle
}
outgoingCallItems := make([]protocol.CallHierarchyOutgoingCall, 0, len(outgoingCalls))
- for _, callItem := range outgoingCalls {
+ for _, callItem := range moremaps.SortedFunc(outgoingCalls, protocol.CompareLocation) {
outgoingCallItems = append(outgoingCallItems, *callItem)
}
return outgoingCallItems, nil
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index 34ac7426019..a7917fbbda4 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -14,7 +14,6 @@ import (
"path/filepath"
"reflect"
"slices"
- "sort"
"strings"
"golang.org/x/tools/go/ast/astutil"
@@ -28,6 +27,7 @@ import (
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/settings"
+ "golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/typesinternal"
@@ -106,16 +106,17 @@ func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle,
req.pkg = nil
}
if err := p.fn(ctx, req); err != nil {
- // TODO(adonovan): most errors in code action providers should
- // not block other providers; see https://go.dev/issue/71275.
- return nil, err
+ // An error in one code action producer
+ // should not affect the others.
+ if ctx.Err() != nil {
+ return nil, err
+ }
+ event.Error(ctx, fmt.Sprintf("CodeAction producer %s failed", p.kind), err)
+ continue
}
}
- sort.Slice(actions, func(i, j int) bool {
- return actions[i].Kind < actions[j].Kind
- })
-
+ // Return code actions in the order their providers are listed.
return actions, nil
}
@@ -233,6 +234,8 @@ type codeActionProducer struct {
needPkg bool // fn needs type information (req.pkg)
}
+// Code Actions are returned in the order their producers are listed below.
+// Depending on the client, this may influence the order they appear in the UI.
var codeActionProducers = [...]codeActionProducer{
{kind: protocol.QuickFix, fn: quickFix, needPkg: true},
{kind: protocol.SourceOrganizeImports, fn: sourceOrganizeImports},
@@ -240,9 +243,8 @@ var codeActionProducers = [...]codeActionProducer{
{kind: settings.GoAssembly, fn: goAssembly, needPkg: true},
{kind: settings.GoDoc, fn: goDoc, needPkg: true},
{kind: settings.GoFreeSymbols, fn: goFreeSymbols},
- {kind: settings.GoTest, fn: goTest},
+ {kind: settings.GoTest, fn: goTest, needPkg: true},
{kind: settings.GoToggleCompilerOptDetails, fn: toggleCompilerOptDetails},
- {kind: settings.GoplsDocFeatures, fn: goplsDocFeatures},
{kind: settings.RefactorExtractFunction, fn: refactorExtractFunction},
{kind: settings.RefactorExtractMethod, fn: refactorExtractMethod},
{kind: settings.RefactorExtractToNewFile, fn: refactorExtractToNewFile},
@@ -260,6 +262,10 @@ var codeActionProducers = [...]codeActionProducer{
{kind: settings.RefactorRewriteMoveParamLeft, fn: refactorRewriteMoveParamLeft, needPkg: true},
{kind: settings.RefactorRewriteMoveParamRight, fn: refactorRewriteMoveParamRight, needPkg: true},
{kind: settings.RefactorRewriteSplitLines, fn: refactorRewriteSplitLines, needPkg: true},
+ {kind: settings.RefactorRewriteEliminateDotImport, fn: refactorRewriteEliminateDotImport, needPkg: true},
+ {kind: settings.RefactorRewriteAddTags, fn: refactorRewriteAddStructTags, needPkg: true},
+ {kind: settings.RefactorRewriteRemoveTags, fn: refactorRewriteRemoveStructTags, needPkg: true},
+ {kind: settings.GoplsDocFeatures, fn: goplsDocFeatures}, // offer this one last (#72742)
// Note: don't forget to update the allow-list in Server.CodeAction
// when adding new query operations like GoTest and GoDoc that
@@ -309,7 +315,7 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
for _, typeError := range req.pkg.TypeErrors() {
// Does type error overlap with CodeAction range?
start, end := typeError.Pos, typeError.Pos
- if _, _, endPos, ok := typesinternal.ReadGo116ErrorData(typeError); ok {
+ if _, _, endPos, ok := typesinternal.ErrorCodeStartEnd(typeError); ok {
end = endPos
}
typeErrorRange, err := req.pgf.PosRange(start, end)
@@ -325,8 +331,7 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
case strings.Contains(msg, "missing method"),
strings.HasPrefix(msg, "cannot convert"),
strings.Contains(msg, "not implement"):
- path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)
- si := stubmethods.GetIfaceStubInfo(req.pkg.FileSet(), info, path, start)
+ si := stubmethods.GetIfaceStubInfo(req.pkg.FileSet(), info, req.pgf, start, end)
if si != nil {
qual := typesinternal.FileQualifier(req.pgf.File, si.Concrete.Obj().Pkg())
iface := types.TypeString(si.Interface.Type(), qual)
@@ -338,8 +343,7 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
// Offer a "Declare missing method T.f" code action.
// See [stubMissingCalledFunctionFixer] for command implementation.
case strings.Contains(msg, "has no field or method"):
- path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)
- si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, path, start)
+ si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, req.pgf, start, end)
if si != nil {
msg := fmt.Sprintf("Declare missing method %s.%s", si.Receiver.Obj().Name(), si.MethodName)
req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc)
@@ -462,7 +466,7 @@ func goDoc(ctx context.Context, req *codeActionsRequest) error {
// refactorExtractFunction produces "Extract function" code actions.
// See [extractFunction] for command implementation.
func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error {
- if _, ok, _, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok {
+ if _, ok, _, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.Cursor); ok {
req.addApplyFixAction("Extract function", fixExtractFunction, req.loc)
}
return nil
@@ -471,7 +475,7 @@ func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error
// refactorExtractMethod produces "Extract method" code actions.
// See [extractMethod] for command implementation.
func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error {
- if _, ok, methodOK, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok && methodOK {
+ if _, ok, methodOK, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.Cursor); ok && methodOK {
req.addApplyFixAction("Extract method", fixExtractMethod, req.loc)
}
return nil
@@ -481,7 +485,7 @@ func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error {
// See [extractVariable] for command implementation.
func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error {
info := req.pkg.TypesInfo()
- if exprs, err := canExtractVariable(info, req.pgf.File, req.start, req.end, false); err == nil {
+ if exprs, err := canExtractVariable(info, req.pgf.Cursor, req.start, req.end, false); err == nil {
// Offer one of refactor.extract.{constant,variable}
// based on the constness of the expression; this is a
// limitation of the codeActionProducers mechanism.
@@ -507,7 +511,7 @@ func refactorExtractVariableAll(ctx context.Context, req *codeActionsRequest) er
info := req.pkg.TypesInfo()
// Don't suggest if only one expr is found,
// otherwise it will duplicate with [refactorExtractVariable]
- if exprs, err := canExtractVariable(info, req.pgf.File, req.start, req.end, true); err == nil && len(exprs) > 1 {
+ if exprs, err := canExtractVariable(info, req.pgf.Cursor, req.start, req.end, true); err == nil && len(exprs) > 1 {
start, end, err := req.pgf.NodeOffsets(exprs[0])
if err != nil {
return err
@@ -664,7 +668,7 @@ func refactorRewriteChangeQuote(ctx context.Context, req *codeActionsRequest) er
// refactorRewriteInvertIf produces "Invert 'if' condition" code actions.
// See [invertIfCondition] for command implementation.
func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error {
- if _, ok, _ := canInvertIfCondition(req.pgf.File, req.start, req.end); ok {
+ if _, ok, _ := canInvertIfCondition(req.pgf.Cursor, req.start, req.end); ok {
req.addApplyFixAction("Invert 'if' condition", fixInvertIfCondition, req.loc)
}
return nil
@@ -674,17 +678,105 @@ func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error
// See [splitLines] for command implementation.
func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) error {
// TODO(adonovan): opt: don't set needPkg just for FileSet.
- if msg, ok, _ := canSplitLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok {
+ if msg, ok, _ := canSplitLines(req.pgf.Cursor, req.pkg.FileSet(), req.start, req.end); ok {
req.addApplyFixAction(msg, fixSplitLines, req.loc)
}
return nil
}
+func refactorRewriteEliminateDotImport(ctx context.Context, req *codeActionsRequest) error {
+ // Figure out if the request is placed over a dot import.
+ var importSpec *ast.ImportSpec
+ for _, imp := range req.pgf.File.Imports {
+ if posRangeContains(imp.Pos(), imp.End(), req.start, req.end) {
+ importSpec = imp
+ break
+ }
+ }
+ if importSpec == nil {
+ return nil
+ }
+ if importSpec.Name == nil || importSpec.Name.Name != "." {
+ return nil
+ }
+
+ // dotImported package path and its imported name after removing the dot.
+ imported := req.pkg.TypesInfo().PkgNameOf(importSpec).Imported()
+ newName := imported.Name()
+
+ rng, err := req.pgf.PosRange(importSpec.Name.Pos(), importSpec.Path.Pos())
+ if err != nil {
+ return err
+ }
+ // Delete the '.' part of the import.
+ edits := []protocol.TextEdit{{
+ Range: rng,
+ }}
+
+ fileScope, ok := req.pkg.TypesInfo().Scopes[req.pgf.File]
+ if !ok {
+ return nil
+ }
+
+ // Go through each use of the dot imported package, checking its scope for
+ // shadowing and calculating an edit to qualify the identifier.
+ for curId := range req.pgf.Cursor.Preorder((*ast.Ident)(nil)) {
+ ident := curId.Node().(*ast.Ident)
+
+ // Only keep identifiers that use a symbol from the
+ // dot imported package.
+ use := req.pkg.TypesInfo().Uses[ident]
+ if use == nil || use.Pkg() == nil {
+ continue
+ }
+ if use.Pkg() != imported {
+ continue
+ }
+
+ // Only qualify unqualified identifiers (due to dot imports).
+ // All other references to a symbol imported from another package
+ // are nested within a select expression (pkg.Foo, v.Method, v.Field).
+ if is[*ast.SelectorExpr](curId.Parent().Node()) {
+ continue
+ }
+
+ // Make sure that the package name will not be shadowed by something else in scope.
+ // If it is then we cannot offer this particular code action.
+ //
+ // TODO: If the object found in scope is the package imported without a
+ // dot, or some builtin not used in the file, the code action could be
+ // allowed to go through.
+ sc := fileScope.Innermost(ident.Pos())
+ if sc == nil {
+ continue
+ }
+ _, obj := sc.LookupParent(newName, ident.Pos())
+ if obj != nil {
+ continue
+ }
+
+ rng, err := req.pgf.PosRange(ident.Pos(), ident.Pos()) // sic, zero-width range before ident
+ if err != nil {
+ continue
+ }
+ edits = append(edits, protocol.TextEdit{
+ Range: rng,
+ NewText: newName + ".",
+ })
+ }
+
+ req.addEditAction("Eliminate dot import", nil, protocol.DocumentChangeEdit(
+ req.fh,
+ edits,
+ ))
+ return nil
+}
+
// refactorRewriteJoinLines produces "Join ITEMS into one line" code actions.
// See [joinLines] for command implementation.
func refactorRewriteJoinLines(ctx context.Context, req *codeActionsRequest) error {
// TODO(adonovan): opt: don't set needPkg just for FileSet.
- if msg, ok, _ := canJoinLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok {
+ if msg, ok, _ := canJoinLines(req.pgf.Cursor, req.pkg.FileSet(), req.start, req.end); ok {
req.addApplyFixAction(msg, fixJoinLines, req.loc)
}
return nil
@@ -721,6 +813,82 @@ func refactorRewriteFillSwitch(ctx context.Context, req *codeActionsRequest) err
return nil
}
+// selectionContainsStructField returns true if the given struct contains a
+// field between start and end pos. If needsTag is true, it only returns true if
+// the struct field found contains a struct tag.
+func selectionContainsStructField(node *ast.StructType, start, end token.Pos, needsTag bool) bool {
+ for _, field := range node.Fields.List {
+ if start <= field.End() && end >= field.Pos() {
+ if !needsTag || field.Tag != nil {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// selectionContainsStruct returns true if there exists a struct containing
+// fields within start and end positions. If removeTags is true, it means the
+// current command is for remove tags rather than add tags, so we only return
+// true if the struct field found contains a struct tag to remove.
+func selectionContainsStruct(cursor cursor.Cursor, start, end token.Pos, removeTags bool) bool {
+ cur, ok := cursor.FindByPos(start, end)
+ if !ok {
+ return false
+ }
+ if _, ok := cur.Node().(*ast.StructType); ok {
+ return true
+ }
+
+ // Handles case where selection is within struct.
+ for c := range cur.Enclosing((*ast.StructType)(nil)) {
+ if selectionContainsStructField(c.Node().(*ast.StructType), start, end, removeTags) {
+ return true
+ }
+ }
+
+ // Handles case where selection contains struct but may contain other nodes, including other structs.
+ for c := range cur.Preorder((*ast.StructType)(nil)) {
+ node := c.Node().(*ast.StructType)
+ // Check that at least one field is located within the selection. If we are removing tags, that field
+ // must also have a struct tag, otherwise we do not provide the code action.
+ if selectionContainsStructField(node, start, end, removeTags) {
+ return true
+ }
+ }
+ return false
+}
+
+// refactorRewriteAddStructTags produces "Add struct tags" code actions.
+// See [server.commandHandler.ModifyTags] for command implementation.
+func refactorRewriteAddStructTags(ctx context.Context, req *codeActionsRequest) error {
+ if selectionContainsStruct(req.pgf.Cursor, req.start, req.end, false) {
+ // TODO(mkalil): Prompt user for modification args once we have dialogue capabilities.
+ cmdAdd := command.NewModifyTagsCommand("Add struct tags", command.ModifyTagsArgs{
+ URI: req.loc.URI,
+ Range: req.loc.Range,
+ Add: "json",
+ })
+ req.addCommandAction(cmdAdd, false)
+ }
+ return nil
+}
+
+// refactorRewriteRemoveStructTags produces "Remove struct tags" code actions.
+// See [server.commandHandler.ModifyTags] for command implementation.
+func refactorRewriteRemoveStructTags(ctx context.Context, req *codeActionsRequest) error {
+ // TODO(mkalil): Prompt user for modification args once we have dialogue capabilities.
+ if selectionContainsStruct(req.pgf.Cursor, req.start, req.end, true) {
+ cmdRemove := command.NewModifyTagsCommand("Remove struct tags", command.ModifyTagsArgs{
+ URI: req.loc.URI,
+ Range: req.loc.Range,
+ Clear: true,
+ })
+ req.addCommandAction(cmdRemove, false)
+ }
+ return nil
+}
+
// removableParameter returns paramInfo about a removable parameter indicated
// by the given [start, end) range, or nil if no such removal is available.
//
@@ -847,44 +1015,66 @@ func goAssembly(ctx context.Context, req *codeActionsRequest) error {
// directly to (say) a lambda of interest.
// Perhaps we could scroll to STEXT for the innermost
// enclosing nested function?
- path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end)
- if len(path) >= 2 { // [... FuncDecl File]
- if decl, ok := path[len(path)-2].(*ast.FuncDecl); ok {
- if fn, ok := req.pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok {
- sig := fn.Signature()
-
- // Compute the linker symbol of the enclosing function.
- var sym strings.Builder
- if fn.Pkg().Name() == "main" {
- sym.WriteString("main")
- } else {
- sym.WriteString(fn.Pkg().Path())
- }
- sym.WriteString(".")
- if sig.Recv() != nil {
- if isPtr, named := typesinternal.ReceiverNamed(sig.Recv()); named != nil {
- if isPtr {
- fmt.Fprintf(&sym, "(*%s)", named.Obj().Name())
- } else {
- sym.WriteString(named.Obj().Name())
+
+ // Compute the linker symbol of the enclosing function or var initializer.
+ var sym strings.Builder
+ if pkg := req.pkg.Types(); pkg.Name() == "main" {
+ sym.WriteString("main")
+ } else {
+ sym.WriteString(pkg.Path())
+ }
+ sym.WriteString(".")
+
+ curSel, _ := req.pgf.Cursor.FindByPos(req.start, req.end)
+ for cur := range curSel.Enclosing((*ast.FuncDecl)(nil), (*ast.ValueSpec)(nil)) {
+ var name string // in command title
+ switch node := cur.Node().(type) {
+ case *ast.FuncDecl:
+ // package-level func or method
+ if fn, ok := req.pkg.TypesInfo().Defs[node.Name].(*types.Func); ok &&
+ fn.Name() != "_" { // blank functions are not compiled
+
+ // Source-level init functions are compiled (along with
+ // package-level var initializers) in into a single pkg.init
+ // function, so this falls out of the logic below.
+
+ if sig := fn.Signature(); sig.TypeParams() == nil && sig.RecvTypeParams() == nil { // generic => no assembly
+ if sig.Recv() != nil {
+ if isPtr, named := typesinternal.ReceiverNamed(sig.Recv()); named != nil {
+ if isPtr {
+ fmt.Fprintf(&sym, "(*%s)", named.Obj().Name())
+ } else {
+ sym.WriteString(named.Obj().Name())
+ }
+ sym.WriteByte('.')
}
- sym.WriteByte('.')
}
+ sym.WriteString(fn.Name())
+
+ name = node.Name.Name // success
}
- sym.WriteString(fn.Name())
-
- if fn.Name() != "_" && // blank functions are not compiled
- (fn.Name() != "init" || sig.Recv() != nil) && // init functions aren't linker functions
- sig.TypeParams() == nil && sig.RecvTypeParams() == nil { // generic => no assembly
- cmd := command.NewAssemblyCommand(
- fmt.Sprintf("Browse %s assembly for %s", view.GOARCH(), decl.Name),
- view.ID(),
- string(req.pkg.Metadata().ID),
- sym.String())
- req.addCommandAction(cmd, false)
+ }
+
+ case *ast.ValueSpec:
+ // package-level var initializer?
+ if len(node.Names) > 0 && len(node.Values) > 0 {
+ v := req.pkg.TypesInfo().Defs[node.Names[0]]
+ if v != nil && typesinternal.IsPackageLevel(v) {
+ sym.WriteString("init")
+ name = "package initializer" // success
}
}
}
+
+ if name != "" {
+ cmd := command.NewAssemblyCommand(
+ fmt.Sprintf("Browse %s assembly for %s", view.GOARCH(), name),
+ view.ID(),
+ string(req.pkg.Metadata().ID),
+ sym.String())
+ req.addCommandAction(cmd, false)
+ break
+ }
}
return nil
}
diff --git a/gopls/internal/golang/comment.go b/gopls/internal/golang/comment.go
index 9a360ce2e2b..a58045b1819 100644
--- a/gopls/internal/golang/comment.go
+++ b/gopls/internal/golang/comment.go
@@ -96,11 +96,7 @@ func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.O
// position of each doc link from the parsed result.
line := safetoken.Line(pgf.Tok, pos)
var start, end token.Pos
- if pgf.Tok.LineStart(line) > comment.Pos() {
- start = pgf.Tok.LineStart(line)
- } else {
- start = comment.Pos()
- }
+ start = max(pgf.Tok.LineStart(line), comment.Pos())
if line < pgf.Tok.LineCount() && pgf.Tok.LineStart(line+1) < comment.End() {
end = pgf.Tok.LineStart(line + 1)
} else {
diff --git a/gopls/internal/golang/compileropt.go b/gopls/internal/golang/compileropt.go
index f9f046463f6..bcce82c123f 100644
--- a/gopls/internal/golang/compileropt.go
+++ b/gopls/internal/golang/compileropt.go
@@ -16,6 +16,7 @@ import (
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/event"
)
@@ -65,7 +66,7 @@ func CompilerOptDetails(ctx context.Context, snapshot *cache.Snapshot, pkgDir pr
reports := make(map[protocol.DocumentURI][]*cache.Diagnostic)
var parseError error
for _, fn := range files {
- uri, diagnostics, err := parseDetailsFile(fn)
+ uri, diagnostics, err := parseDetailsFile(fn, snapshot.Options())
if err != nil {
// expect errors for all the files, save 1
parseError = err
@@ -87,7 +88,7 @@ func CompilerOptDetails(ctx context.Context, snapshot *cache.Snapshot, pkgDir pr
}
// parseDetailsFile parses the file written by the Go compiler which contains a JSON-encoded protocol.Diagnostic.
-func parseDetailsFile(filename string) (protocol.DocumentURI, []*cache.Diagnostic, error) {
+func parseDetailsFile(filename string, options *settings.Options) (protocol.DocumentURI, []*cache.Diagnostic, error) {
buf, err := os.ReadFile(filename)
if err != nil {
return "", nil, err
@@ -118,30 +119,14 @@ func parseDetailsFile(filename string) (protocol.DocumentURI, []*cache.Diagnosti
if err := dec.Decode(d); err != nil {
return "", nil, err
}
- if d.Source != "go compiler" {
- continue
- }
d.Tags = []protocol.DiagnosticTag{} // must be an actual slice
msg := d.Code.(string)
if msg != "" {
- // Typical message prefixes gathered by grepping the source of
- // cmd/compile for literal arguments in calls to logopt.LogOpt.
- // (It is not a well defined set.)
- //
- // - canInlineFunction
- // - cannotInlineCall
- // - cannotInlineFunction
- // - copy
- // - escape
- // - escapes
- // - isInBounds
- // - isSliceInBounds
- // - iteration-variable-to-{heap,stack}
- // - leak
- // - loop-modified-{range,for}
- // - nilcheck
msg = fmt.Sprintf("%s(%s)", msg, d.Message)
}
+ if !showDiagnostic(msg, d.Source, options) {
+ continue
+ }
// zeroIndexedRange subtracts 1 from the line and
// range, because the compiler output neglects to
@@ -186,6 +171,51 @@ func parseDetailsFile(filename string) (protocol.DocumentURI, []*cache.Diagnosti
return uri, diagnostics, nil
}
+// showDiagnostic reports whether a given diagnostic should be shown to the end
+// user, given the current options.
+func showDiagnostic(msg, source string, o *settings.Options) bool {
+ if source != "go compiler" {
+ return false
+ }
+ if o.Annotations == nil {
+ return true
+ }
+
+ // The strings below were gathered by grepping the source of
+ // cmd/compile for literal arguments in calls to logopt.LogOpt.
+ // (It is not a well defined set.)
+ //
+ // - canInlineFunction
+ // - cannotInlineCall
+ // - cannotInlineFunction
+ // - escape
+ // - escapes
+ // - isInBounds
+ // - isSliceInBounds
+ // - leak
+ // - nilcheck
+ //
+ // Additional ones not handled by logic below:
+ // - copy
+ // - iteration-variable-to-{heap,stack}
+ // - loop-modified-{range,for}
+
+ switch {
+ case strings.HasPrefix(msg, "canInline") ||
+ strings.HasPrefix(msg, "cannotInline") ||
+ strings.HasPrefix(msg, "inlineCall"):
+ return o.Annotations[settings.Inline]
+ case strings.HasPrefix(msg, "escape") || msg == "leak":
+ return o.Annotations[settings.Escape]
+ case strings.HasPrefix(msg, "nilcheck"):
+ return o.Annotations[settings.Nil]
+ case strings.HasPrefix(msg, "isInBounds") ||
+ strings.HasPrefix(msg, "isSliceInBounds"):
+ return o.Annotations[settings.Bounds]
+ }
+ return false
+}
+
func findJSONFiles(dir string) ([]string, error) {
ans := []string{}
f := func(path string, fi os.FileInfo, _ error) error {
diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go
index 4c340055233..83be9f2ed80 100644
--- a/gopls/internal/golang/completion/completion.go
+++ b/gopls/internal/golang/completion/completion.go
@@ -164,14 +164,14 @@ func (i *CompletionItem) addConversion(c *completer, conv conversionEdits) error
// Scoring constants are used for weighting the relevance of different candidates.
const (
+ // lowScore indicates an irrelevant or not useful completion item.
+ lowScore float64 = 0.01
+
// stdScore is the base score for all completion items.
stdScore float64 = 1.0
// highScore indicates a very relevant completion item.
highScore float64 = 10.0
-
- // lowScore indicates an irrelevant or not useful completion item.
- lowScore float64 = 0.01
)
// matcher matches a candidate's label against the user input. The
@@ -489,12 +489,7 @@ type candidate struct {
}
func (c candidate) hasMod(mod typeModKind) bool {
- for _, m := range c.mods {
- if m == mod {
- return true
- }
- }
- return false
+ return slices.Contains(c.mods, mod)
}
// Completion returns a list of possible candidates for completion, given a
@@ -668,7 +663,7 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
err = c.collectCompletions(ctx)
if err != nil {
- return nil, nil, err
+ return nil, nil, fmt.Errorf("failed to collect completions: %v", err)
}
// Deep search collected candidates and their members for more candidates.
@@ -688,7 +683,7 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
for _, callback := range c.completionCallbacks {
if deadline == nil || time.Now().Before(*deadline) {
if err := c.snapshot.RunProcessEnvFunc(ctx, callback); err != nil {
- return nil, nil, err
+ return nil, nil, fmt.Errorf("failed to run goimports callback: %v", err)
}
}
}
@@ -702,7 +697,7 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
// depend on other candidates having already been collected.
c.addStatementCandidates()
- c.sortItems()
+ sortItems(c.items)
return c.items, c.getSurrounding(), nil
}
@@ -792,7 +787,6 @@ func (c *completer) containingIdent(src []byte) *ast.Ident {
}
fakeIdent := &ast.Ident{Name: lit, NamePos: pos}
-
if _, isBadDecl := c.path[0].(*ast.BadDecl); isBadDecl {
// You don't get *ast.Idents at the file level, so look for bad
// decls and use the manually extracted token.
@@ -807,6 +801,18 @@ func (c *completer) containingIdent(src []byte) *ast.Ident {
// is a keyword. This improves completion after an "accidental
// keyword", e.g. completing to "variance" in "someFunc(var<>)".
return fakeIdent
+ } else if block, ok := c.path[0].(*ast.BlockStmt); ok && len(block.List) != 0 {
+ last := block.List[len(block.List)-1]
+ // Handle incomplete AssignStmt with multiple left-hand vars:
+ // var left, right int
+ // left, ri‸ -> "right"
+ if expr, ok := last.(*ast.ExprStmt); ok &&
+ (is[*ast.Ident](expr.X) ||
+ is[*ast.SelectorExpr](expr.X) ||
+ is[*ast.IndexExpr](expr.X) ||
+ is[*ast.StarExpr](expr.X)) {
+ return fakeIdent
+ }
}
return nil
@@ -830,16 +836,16 @@ func (c *completer) scanToken(contents []byte) (token.Pos, token.Token, string)
}
}
-func (c *completer) sortItems() {
- sort.SliceStable(c.items, func(i, j int) bool {
+func sortItems(items []CompletionItem) {
+ sort.SliceStable(items, func(i, j int) bool {
// Sort by score first.
- if c.items[i].Score != c.items[j].Score {
- return c.items[i].Score > c.items[j].Score
+ if items[i].Score != items[j].Score {
+ return items[i].Score > items[j].Score
}
// Then sort by label so order stays consistent. This also has the
// effect of preferring shorter candidates.
- return c.items[i].Label < c.items[j].Label
+ return items[i].Label < items[j].Label
})
}
@@ -989,7 +995,10 @@ func (c *completer) populateImportCompletions(searchImport *ast.ImportSpec) erro
}
c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error {
- return imports.GetImportPaths(ctx, searchImports, prefix, c.filename, c.pkg.Types().Name(), opts.Env)
+ if err := imports.GetImportPaths(ctx, searchImports, prefix, c.filename, c.pkg.Types().Name(), opts.Env); err != nil {
+ return fmt.Errorf("getting import paths: %v", err)
+ }
+ return nil
})
return nil
}
@@ -1174,7 +1183,10 @@ func isValidIdentifierChar(char byte) bool {
// adds struct fields, interface methods, function declaration fields to completion
func (c *completer) addFieldItems(fields *ast.FieldList) {
- if fields == nil {
+ // TODO: in golang/go#72828, we get here with a nil surrounding.
+ // This indicates a logic bug elsewhere: we should only be interrogating the
+ // surrounding if it is set.
+ if fields == nil || c.surrounding == nil {
return
}
@@ -1481,7 +1493,6 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
}
for _, uri := range mp.CompiledGoFiles {
- uri := uri
g.Go(func() error {
return quickParse(uri, mp, tooNew)
})
@@ -1529,7 +1540,10 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error {
defer cancel()
- return imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.Types().Name(), opts.Env)
+ if err := imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.Types().Name(), opts.Env); err != nil {
+ return fmt.Errorf("getting package exports: %v", err)
+ }
+ return nil
})
return nil
}
@@ -1916,7 +1930,10 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru
}
c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error {
- return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.Types().Name(), opts.Env)
+ if err := imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.Types().Name(), opts.Env); err != nil {
+ return fmt.Errorf("getting completion candidates: %v", err)
+ }
+ return nil
})
return nil
@@ -1989,6 +2006,8 @@ func (c *completer) structLiteralFieldName(ctx context.Context) error {
// enclosingCompositeLiteral returns information about the composite literal enclosing the
// position.
+// It returns nil on failure; for example, if there is no type information for a
+// node on path.
func enclosingCompositeLiteral(path []ast.Node, pos token.Pos, info *types.Info) *compLitInfo {
for _, n := range path {
switch n := n.(type) {
@@ -2553,7 +2572,7 @@ func inferExpectedResultTypes(c *completer, callNodeIdx int) []types.Type {
switch node := c.path[callNodeIdx+1].(type) {
case *ast.KeyValueExpr:
enclosingCompositeLiteral := enclosingCompositeLiteral(c.path[callNodeIdx:], callNode.Pos(), c.pkg.TypesInfo())
- if !wantStructFieldCompletions(enclosingCompositeLiteral) {
+ if enclosingCompositeLiteral != nil && !wantStructFieldCompletions(enclosingCompositeLiteral) {
expectedResults = append(expectedResults, expectedCompositeLiteralType(enclosingCompositeLiteral, callNode.Pos()))
}
case *ast.AssignStmt:
diff --git a/gopls/internal/golang/completion/deep_completion.go b/gopls/internal/golang/completion/deep_completion.go
index 053ece8219e..523c5b8652b 100644
--- a/gopls/internal/golang/completion/deep_completion.go
+++ b/gopls/internal/golang/completion/deep_completion.go
@@ -9,6 +9,8 @@ import (
"go/types"
"strings"
"time"
+
+ "golang.org/x/tools/gopls/internal/util/typesutil"
)
// MaxDeepCompletions limits deep completion results because in most cases
@@ -312,6 +314,9 @@ func deepCandName(cand *candidate) string {
for i, obj := range cand.path {
buf.WriteString(obj.Name())
+ if fn, ok := obj.(*types.Func); ok {
+ buf.WriteString(typesutil.FormatTypeParams(fn.Signature().TypeParams()))
+ }
if cand.pathInvokeMask&(1< 0 {
buf.WriteByte('(')
buf.WriteByte(')')
diff --git a/gopls/internal/golang/completion/deep_completion_test.go b/gopls/internal/golang/completion/deep_completion_test.go
index 27009af1b4f..d522b9be9a9 100644
--- a/gopls/internal/golang/completion/deep_completion_test.go
+++ b/gopls/internal/golang/completion/deep_completion_test.go
@@ -20,7 +20,7 @@ func TestDeepCompletionIsHighScore(t *testing.T) {
}
// Fill up with higher scores.
- for i := 0; i < MaxDeepCompletions; i++ {
+ for range MaxDeepCompletions {
if !s.isHighScore(10) {
t.Error("10 should be high score")
}
diff --git a/gopls/internal/golang/completion/keywords.go b/gopls/internal/golang/completion/keywords.go
index 3f2f5ac78cd..fb1fa1694ce 100644
--- a/gopls/internal/golang/completion/keywords.go
+++ b/gopls/internal/golang/completion/keywords.go
@@ -121,18 +121,69 @@ func (c *completer) addKeywordCompletions() {
c.addKeywordItems(seen, stdScore, BREAK)
}
case *ast.TypeSwitchStmt, *ast.SelectStmt, *ast.SwitchStmt:
- c.addKeywordItems(seen, stdScore, CASE, DEFAULT)
+ // if there is no default case yet, it's highly likely to add a default in switch.
+ // we don't offer 'default' anymore if user has used it already in current swtich.
+ if !hasDefaultClause(node) {
+ c.addKeywordItems(seen, highScore, CASE, DEFAULT)
+ }
case *ast.ForStmt, *ast.RangeStmt:
c.addKeywordItems(seen, stdScore, BREAK, CONTINUE)
// This is a bit weak, functions allow for many keywords
case *ast.FuncDecl:
if node.Body != nil && c.pos > node.Body.Lbrace {
- c.addKeywordItems(seen, stdScore, DEFER, RETURN, FOR, GO, SWITCH, SELECT, IF, ELSE, VAR, CONST, GOTO, TYPE)
+ // requireReturnObj checks whether user must provide some objects after return.
+ requireReturnObj := func(sig *ast.FuncType) bool {
+ results := sig.Results
+ if results == nil || results.List == nil {
+ return false // nothing to return
+ }
+ // If any result is named, allow a bare return.
+ for _, r := range results.List {
+ for _, name := range r.Names {
+ if name.Name != "_" {
+ return false
+ }
+ }
+ }
+ return true
+ }
+ ret := RETURN
+ if requireReturnObj(node.Type) {
+ // as user must return something, we offer a space after return.
+ // function literal inside a function will be affected by outer function,
+ // but 'go fmt' will help to remove the ending space.
+ // the benefit is greater than introducing an unncessary space.
+ ret += " "
+ }
+
+ c.addKeywordItems(seen, stdScore, DEFER, ret, FOR, GO, SWITCH, SELECT, IF, ELSE, VAR, CONST, GOTO, TYPE)
}
}
}
}
+// hasDefaultClause reports whether the given node contains a direct default case.
+// It does not traverse child nodes to look for nested default clauses,
+// and returns false if the node is not a switch statement.
+func hasDefaultClause(node ast.Node) bool {
+ var cases []ast.Stmt
+ switch node := node.(type) {
+ case *ast.TypeSwitchStmt:
+ cases = node.Body.List
+ case *ast.SelectStmt:
+ cases = node.Body.List
+ case *ast.SwitchStmt:
+ cases = node.Body.List
+ }
+ for _, c := range cases {
+ if clause, ok := c.(*ast.CaseClause); ok &&
+ clause.List == nil { // default case
+ return true
+ }
+ }
+ return false
+}
+
// addKeywordItems dedupes and adds completion items for the specified
// keywords with the specified score.
func (c *completer) addKeywordItems(seen map[string]bool, score float64, kws ...string) {
diff --git a/gopls/internal/golang/completion/labels.go b/gopls/internal/golang/completion/labels.go
index f0e5f42a67a..52afafebf25 100644
--- a/gopls/internal/golang/completion/labels.go
+++ b/gopls/internal/golang/completion/labels.go
@@ -8,6 +8,7 @@ import (
"go/ast"
"go/token"
"math"
+ "slices"
)
type labelType int
@@ -96,12 +97,7 @@ func (c *completer) labels(lt labelType) {
// Only search into block-like nodes enclosing our "goto".
// This prevents us from finding labels in nested blocks.
case *ast.BlockStmt, *ast.CommClause, *ast.CaseClause:
- for _, p := range c.path {
- if n == p {
- return true
- }
- }
- return false
+ return slices.Contains(c.path, n)
case *ast.LabeledStmt:
addLabel(highScore, n)
}
diff --git a/gopls/internal/golang/completion/newfile.go b/gopls/internal/golang/completion/newfile.go
new file mode 100644
index 00000000000..d9869a2f050
--- /dev/null
+++ b/gopls/internal/golang/completion/newfile.go
@@ -0,0 +1,65 @@
+// Copyright 2025 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 completion
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
+)
+
+// NewFile returns a document change to complete an empty go file.
+func NewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) (*protocol.DocumentChange, error) {
+ if bs, err := fh.Content(); err != nil || len(bs) != 0 {
+ return nil, err
+ }
+ meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+ var buf bytes.Buffer
+ // Copy the copyright header from the first existing file that has one.
+ for _, fileURI := range meta.GoFiles {
+ if fileURI == fh.URI() {
+ continue
+ }
+ fh, err := snapshot.ReadFile(ctx, fileURI)
+ if err != nil {
+ continue
+ }
+ pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header)
+ if err != nil {
+ continue
+ }
+ if group := golang.CopyrightComment(pgf.File); group != nil {
+ start, end, err := pgf.NodeOffsets(group)
+ if err != nil {
+ continue
+ }
+ buf.Write(pgf.Src[start:end])
+ buf.WriteString("\n\n")
+ break
+ }
+ }
+
+ pkgName, err := bestPackage(ctx, snapshot, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+
+ fmt.Fprintf(&buf, "package %s\n", pkgName)
+ change := protocol.DocumentChangeEdit(fh, []protocol.TextEdit{{
+ Range: protocol.Range{}, // insert at start of file
+ NewText: buf.String(),
+ }})
+
+ return &change, nil
+}
diff --git a/gopls/internal/golang/completion/package.go b/gopls/internal/golang/completion/package.go
index 5fd6c04144d..d1698ee6580 100644
--- a/gopls/internal/golang/completion/package.go
+++ b/gopls/internal/golang/completion/package.go
@@ -15,6 +15,7 @@ import (
"go/token"
"go/types"
"path/filepath"
+ "sort"
"strings"
"unicode"
@@ -27,6 +28,24 @@ import (
"golang.org/x/tools/gopls/internal/util/safetoken"
)
+// bestPackage offers the best package name for a package declaration when
+// one is not present in the given file.
+func bestPackage(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (string, error) {
+ suggestions, err := packageSuggestions(ctx, snapshot, uri, "")
+ if err != nil {
+ return "", err
+ }
+ // sort with the same way of sortItems.
+ sort.SliceStable(suggestions, func(i, j int) bool {
+ if suggestions[i].score != suggestions[j].score {
+ return suggestions[i].score > suggestions[j].score
+ }
+ return suggestions[i].name < suggestions[j].name
+ })
+
+ return suggestions[0].name, nil
+}
+
// packageClauseCompletions offers completions for a package declaration when
// one is not present in the given file.
func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]CompletionItem, *Selection, error) {
@@ -62,7 +81,7 @@ func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh
Score: pkg.score,
})
}
-
+ sortItems(items)
return items, surrounding, nil
}
@@ -197,11 +216,20 @@ func packageSuggestions(ctx context.Context, snapshot *cache.Snapshot, fileURI p
}
matcher := fuzzy.NewMatcher(prefix)
+ var currentPackageName string
+ if variants, err := snapshot.MetadataForFile(ctx, fileURI); err == nil &&
+ len(variants) != 0 {
+ currentPackageName = string(variants[0].Name)
+ }
// Always try to suggest a main package
defer func() {
+ mainScore := lowScore
+ if currentPackageName == "main" {
+ mainScore = highScore
+ }
if score := float64(matcher.Score("main")); score > 0 {
- packages = append(packages, toCandidate("main", score*lowScore))
+ packages = append(packages, toCandidate("main", score*mainScore))
}
}()
@@ -254,15 +282,20 @@ func packageSuggestions(ctx context.Context, snapshot *cache.Snapshot, fileURI p
seenPkgs[testPkgName] = struct{}{}
}
- // Add current directory name as a low relevance suggestion.
if _, ok := seenPkgs[pkgName]; !ok {
+ // Add current directory name as a low relevance suggestion.
+ dirNameScore := lowScore
+ // if current package name is empty, the dir name is the best choice.
+ if currentPackageName == "" {
+ dirNameScore = highScore
+ }
if score := float64(matcher.Score(string(pkgName))); score > 0 {
- packages = append(packages, toCandidate(string(pkgName), score*lowScore))
+ packages = append(packages, toCandidate(string(pkgName), score*dirNameScore))
}
testPkgName := pkgName + "_test"
if score := float64(matcher.Score(string(testPkgName))); score > 0 {
- packages = append(packages, toCandidate(string(testPkgName), score*lowScore))
+ packages = append(packages, toCandidate(string(testPkgName), score*dirNameScore))
}
}
diff --git a/gopls/internal/golang/completion/postfix_snippets.go b/gopls/internal/golang/completion/postfix_snippets.go
index 1bafe848490..1d306e3518d 100644
--- a/gopls/internal/golang/completion/postfix_snippets.go
+++ b/gopls/internal/golang/completion/postfix_snippets.go
@@ -334,7 +334,29 @@ if {{$errName | $a.SpecifiedPlaceholder 1}} != nil {
{{end}}
}
{{end}}`,
-}}
+},
+ {
+ label: "tostring",
+ details: "[]byte to string",
+ body: `{{if (eq (.TypeName .Type) "[]byte") -}}
+ string({{.X}})
+ {{- end}}`,
+ },
+ {
+ label: "tostring",
+ details: "int to string",
+ body: `{{if (eq (.TypeName .Type) "int") -}}
+ {{.Import "strconv"}}.Itoa({{.X}})
+ {{- end}}`,
+ },
+ {
+ label: "tobytes",
+ details: "string to []byte",
+ body: `{{if (eq (.TypeName .Type) "string") -}}
+ []byte({{.X}})
+ {{- end}}`,
+ },
+}
// Cursor indicates where the client's cursor should end up after the
// snippet is done.
diff --git a/gopls/internal/golang/completion/unify.go b/gopls/internal/golang/completion/unify.go
index 8f4a1d3cbe0..f28ad49cd52 100644
--- a/gopls/internal/golang/completion/unify.go
+++ b/gopls/internal/golang/completion/unify.go
@@ -189,29 +189,6 @@ func (u *unifier) set(x *types.TypeParam, t types.Type) {
*u.handles[x] = t
}
-// unknowns returns the number of type parameters for which no type has been set yet.
-func (u *unifier) unknowns() int {
- n := 0
- for _, h := range u.handles {
- if *h == nil {
- n++
- }
- }
- return n
-}
-
-// inferred returns the list of inferred types for the given type parameter list.
-// The result is never nil and has the same length as tparams; result types that
-// could not be inferred are nil. Corresponding type parameters and result types
-// have identical indices.
-func (u *unifier) inferred(tparams []*types.TypeParam) []types.Type {
- list := make([]types.Type, len(tparams))
- for i, x := range tparams {
- list[i] = u.at(x)
- }
- return list
-}
-
// asInterface returns the underlying type of x as an interface if
// it is a non-type parameter interface. Otherwise it returns nil.
func asInterface(x types.Type) (i *types.Interface) {
@@ -245,30 +222,6 @@ func identicalOrigin(x, y *types.Named) bool {
return x.Origin().Obj() == y.Origin().Obj()
}
-func match(x, y types.Type) types.Type {
- // Common case: we don't have channels.
- if types.Identical(x, y) {
- return x
- }
-
- // We may have channels that differ in direction only.
- if x, _ := x.(*types.Chan); x != nil {
- if y, _ := y.(*types.Chan); y != nil && types.Identical(x.Elem(), y.Elem()) {
- // We have channels that differ in direction only.
- // If there's an unrestricted channel, select the restricted one.
- switch {
- case x.Dir() == types.SendRecv:
- return y
- case y.Dir() == types.SendRecv:
- return x
- }
- }
- }
-
- // types are different
- return nil
-}
-
func coreType(t types.Type) types.Type {
t = types.Unalias(t)
tpar, _ := t.(*types.TypeParam)
diff --git a/gopls/internal/golang/completion/util.go b/gopls/internal/golang/completion/util.go
index 7a4729413ae..306078296c1 100644
--- a/gopls/internal/golang/completion/util.go
+++ b/gopls/internal/golang/completion/util.go
@@ -171,7 +171,7 @@ func deslice(T types.Type) types.Type {
return nil
}
-// isSelector returns the enclosing *ast.SelectorExpr when pos is in the
+// enclosingSelector returns the enclosing *ast.SelectorExpr when pos is in the
// selector.
func enclosingSelector(path []ast.Node, pos token.Pos) *ast.SelectorExpr {
if len(path) == 0 {
diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go
index 2ce89795a06..5e82e430225 100644
--- a/gopls/internal/golang/extract.go
+++ b/gopls/internal/golang/extract.go
@@ -15,26 +15,30 @@ import (
"go/types"
"slices"
"sort"
+ "strconv"
"strings"
"text/scanner"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
goplsastutil "golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/typesinternal"
)
// extractVariable implements the refactor.extract.{variable,constant} CodeAction command.
-func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- return extractExprs(fset, start, end, src, file, info, false)
+func extractVariable(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ return extractExprs(pkg, pgf, start, end, false)
}
// extractVariableAll implements the refactor.extract.{variable,constant}-all CodeAction command.
-func extractVariableAll(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- return extractExprs(fset, start, end, src, file, info, true)
+func extractVariableAll(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ return extractExprs(pkg, pgf, start, end, true)
}
// extractExprs replaces occurrence(s) of a specified expression within the same function
@@ -43,9 +47,15 @@ func extractVariableAll(fset *token.FileSet, start, end token.Pos, src []byte, f
//
// The new variable/constant is declared as close as possible to the first found expression
// within the deepest common scope accessible to all candidate occurrences.
-func extractExprs(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, info *types.Info, all bool) (*token.FileSet, *analysis.SuggestedFix, error) {
+func extractExprs(pkg *cache.Package, pgf *parsego.File, start, end token.Pos, all bool) (*token.FileSet, *analysis.SuggestedFix, error) {
+ var (
+ fset = pkg.FileSet()
+ info = pkg.TypesInfo()
+ file = pgf.File
+ )
+ // TODO(adonovan): simplify, using Cursor.
tokFile := fset.File(file.FileStart)
- exprs, err := canExtractVariable(info, file, start, end, all)
+ exprs, err := canExtractVariable(info, pgf.Cursor, start, end, all)
if err != nil {
return nil, nil, fmt.Errorf("cannot extract: %v", err)
}
@@ -154,7 +164,7 @@ Outer:
return nil, nil, fmt.Errorf("cannot find location to insert extraction: %v", err)
}
// Within function: compute appropriate statement indentation.
- indent, err := calculateIndentation(src, tokFile, before)
+ indent, err := calculateIndentation(pgf.Src, tokFile, before)
if err != nil {
return nil, nil, err
}
@@ -375,16 +385,18 @@ func stmtToInsertVarBefore(path []ast.Node, variables []*variable) (ast.Stmt, er
}
return parent, nil
}
- return enclosingStmt.(ast.Stmt), nil
+ return enclosingStmt, nil
}
// canExtractVariable reports whether the code in the given range can be
// extracted to a variable (or constant). It returns the selected expression or, if 'all',
// all structurally equivalent expressions within the same function body, in lexical order.
-func canExtractVariable(info *types.Info, file *ast.File, start, end token.Pos, all bool) ([]ast.Expr, error) {
+func canExtractVariable(info *types.Info, curFile cursor.Cursor, start, end token.Pos, all bool) ([]ast.Expr, error) {
if start == end {
return nil, fmt.Errorf("empty selection")
}
+ file := curFile.Node().(*ast.File)
+ // TODO(adonovan): simplify, using Cursor.
path, exact := astutil.PathEnclosingInterval(file, start, end)
if !exact {
return nil, fmt.Errorf("selection is not an expression")
@@ -476,10 +488,8 @@ func canExtractVariable(info *types.Info, file *ast.File, start, end token.Pos,
path, _ := astutil.PathEnclosingInterval(file, e.Pos(), e.End())
for _, n := range path {
if assignment, ok := n.(*ast.AssignStmt); ok {
- for _, lhs := range assignment.Lhs {
- if lhs == e {
- return nil, fmt.Errorf("node %T is in LHS of an AssignStmt", expr)
- }
+ if slices.Contains(assignment.Lhs, e) {
+ return nil, fmt.Errorf("node %T is in LHS of an AssignStmt", expr)
}
break
}
@@ -571,13 +581,13 @@ type returnVariable struct {
}
// extractMethod refactors the selected block of code into a new method.
-func extractMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- return extractFunctionMethod(fset, start, end, src, file, pkg, info, true)
+func extractMethod(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ return extractFunctionMethod(pkg, pgf, start, end, true)
}
// extractFunction refactors the selected block of code into a new function.
-func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- return extractFunctionMethod(fset, start, end, src, file, pkg, info, false)
+func extractFunction(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ return extractFunctionMethod(pkg, pgf, start, end, false)
}
// extractFunctionMethod refactors the selected block of code into a new function/method.
@@ -588,17 +598,26 @@ func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, file
// and return values of the extracted function/method. Lastly, we construct the call
// of the function/method and insert this call as well as the extracted function/method into
// their proper locations.
-func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info, isMethod bool) (*token.FileSet, *analysis.SuggestedFix, error) {
+func extractFunctionMethod(cpkg *cache.Package, pgf *parsego.File, start, end token.Pos, isMethod bool) (*token.FileSet, *analysis.SuggestedFix, error) {
+ var (
+ fset = cpkg.FileSet()
+ pkg = cpkg.Types()
+ info = cpkg.TypesInfo()
+ src = pgf.Src
+ )
+
errorPrefix := "extractFunction"
if isMethod {
errorPrefix = "extractMethod"
}
+ file := pgf.Cursor.Node().(*ast.File)
+ // TODO(adonovan): simplify, using Cursor.
tok := fset.File(file.FileStart)
if tok == nil {
return nil, nil, bug.Errorf("no file for position")
}
- p, ok, methodOk, err := canExtractFunction(tok, start, end, src, file)
+ p, ok, methodOk, err := canExtractFunction(tok, start, end, src, pgf.Cursor)
if (!ok && !isMethod) || (!methodOk && isMethod) {
return nil, nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix,
safetoken.StartPosition(fset, start), err)
@@ -894,6 +913,123 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte
}
}
+ // Determine if the extracted block contains any free branch statements, for
+ // example: "continue label" where "label" is declared outside of the
+ // extracted block, or continue inside a "for" statement where the for
+ // statement is declared outside of the extracted block.
+
+ // If the extracted block contains free branch statements, we add another
+ // return value "ctrl" to the extracted function that will be used to
+ // determine the control flow. See the following example, where === denotes
+ // the range to be extracted.
+ //
+ // Before:
+ // func f(cond bool) {
+ // for range "abc" {
+ // ==============
+ // if cond {
+ // continue
+ // }
+ // ==============
+ // println(0)
+ // }
+ // }
+
+ // After:
+ // func f(cond bool) {
+ // for range "abc" {
+ // ctrl := newFunction(cond)
+ // switch ctrl {
+ // case 1:
+ // continue
+ // }
+ // println(0)
+ // }
+ // }
+ //
+ // func newFunction(cond bool) int {
+ // if cond {
+ // return 1
+ // }
+ // return 0
+ // }
+ //
+
+ curSel, _ := pgf.Cursor.FindByPos(start, end) // since canExtractFunction succeeded, this will always return a valid cursor
+ freeBranches := freeBranches(info, curSel, start, end)
+
+ // Generate an unused identifier for the control value.
+ ctrlVar, _ := freshName(info, file, start, "ctrl", 0)
+ if len(freeBranches) > 0 {
+
+ zeroValExpr := &ast.BasicLit{
+ Kind: token.INT,
+ Value: "0",
+ }
+ var branchStmts []*ast.BranchStmt
+ var stack []ast.Node
+ // Add the zero "ctrl" value to each return statement in the extracted block.
+ ast.Inspect(extractedBlock, func(n ast.Node) bool {
+ if n != nil {
+ stack = append(stack, n)
+ } else {
+ stack = stack[:len(stack)-1]
+ }
+ switch n := n.(type) {
+ case *ast.ReturnStmt:
+ n.Results = append(n.Results, zeroValExpr)
+ case *ast.BranchStmt:
+ // Collect a list of branch statements in the extracted block to examine later.
+ if isFreeBranchStmt(stack) {
+ branchStmts = append(branchStmts, n)
+ }
+ case *ast.FuncLit:
+ // Don't descend into nested functions. When we return false
+ // here, ast.Inspect does not give us a "pop" event when leaving
+ // the subtree, so we need to pop here. (golang/go#73319)
+ stack = stack[:len(stack)-1]
+ return false
+ }
+ return true
+ })
+
+ // Construct a return statement to replace each free branch statement in the extracted block. It should have
+ // zero values for all return parameters except one, "ctrl", which dictates which continuation to follow.
+ var freeCtrlStmtReturns []ast.Expr
+ // Create "zero values" for each type.
+ for _, returnType := range returnTypes {
+ var val ast.Expr
+ var isValid bool
+ for obj, typ := range seenVars {
+ if typ == returnType.Type {
+ val, isValid = typesinternal.ZeroExpr(obj.Type(), qual)
+ break
+ }
+ }
+ if !isValid {
+ return nil, nil, fmt.Errorf("could not find matching AST expression for %T", returnType.Type)
+ }
+ freeCtrlStmtReturns = append(freeCtrlStmtReturns, val)
+ }
+ freeCtrlStmtReturns = append(freeCtrlStmtReturns, getZeroVals(retVars)...)
+
+ for i, branchStmt := range branchStmts {
+ replaceBranchStmtWithReturnStmt(extractedBlock, branchStmt, &ast.ReturnStmt{
+ Return: branchStmt.Pos(),
+ Results: append(slices.Clip(freeCtrlStmtReturns), &ast.BasicLit{
+ Kind: token.INT,
+ Value: strconv.Itoa(i + 1), // start with 1 because 0 is reserved for base case
+ }),
+ })
+
+ }
+ retVars = append(retVars, &returnVariable{
+ name: ast.NewIdent(ctrlVar),
+ decl: &ast.Field{Type: ast.NewIdent("int")},
+ zeroVal: zeroValExpr,
+ })
+ }
+
// Add a return statement to the end of the new function. This return statement must include
// the values for the types of the original extracted function signature and (if a return
// statement is present in the selection) enclosing function signature.
@@ -1022,6 +1158,22 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte
strings.ReplaceAll(ifBuf.String(), "\n", newLineIndent)
fullReplacement.WriteString(ifstatement)
}
+
+ // Add the switch statement for free branch statements after the new function call.
+ if len(freeBranches) > 0 {
+ fmt.Fprintf(&fullReplacement, "%[1]sswitch %[2]s {%[1]s", newLineIndent, ctrlVar)
+ for i, br := range freeBranches {
+ // Preserve spacing at the beginning of the line containing the branch statement.
+ startPos := tok.LineStart(safetoken.Line(tok, br.Pos()))
+ start, end, err := safetoken.Offsets(tok, startPos, br.End())
+ if err != nil {
+ return nil, nil, err
+ }
+ fmt.Fprintf(&fullReplacement, "case %d:\n%s%s", i+1, pgf.Src[start:end], newLineIndent)
+ }
+ fullReplacement.WriteString("}")
+ }
+
fullReplacement.Write(after)
fullReplacement.WriteString("\n\n") // add newlines after the enclosing function
fullReplacement.Write(newFuncBuf.Bytes()) // insert the extracted function
@@ -1086,7 +1238,10 @@ func moveParamToFrontIfFound(params []ast.Expr, paramTypes []*ast.Field, x, sel
// their cursors for whitespace. To support this use case, we must manually adjust the
// ranges to match the correct AST node. In this particular example, we would adjust
// rng.Start forward to the start of 'if' and rng.End backward to after '}'.
-func adjustRangeForCommentsAndWhiteSpace(tok *token.File, start, end token.Pos, content []byte, file *ast.File) (token.Pos, token.Pos, error) {
+func adjustRangeForCommentsAndWhiteSpace(tok *token.File, start, end token.Pos, content []byte, curFile cursor.Cursor) (token.Pos, token.Pos, error) {
+ file := curFile.Node().(*ast.File)
+ // TODO(adonovan): simplify, using Cursor.
+
// Adjust the end of the range to after leading whitespace and comments.
prevStart := token.NoPos
startComment := sort.Search(len(file.Comments), func(i int) bool {
@@ -1229,7 +1384,7 @@ func collectFreeVars(info *types.Info, file *ast.File, start, end token.Pos, nod
// return value acts as an indicator for where it was defined.
var sel func(n *ast.SelectorExpr) (types.Object, bool)
sel = func(n *ast.SelectorExpr) (types.Object, bool) {
- switch x := astutil.Unparen(n.X).(type) {
+ switch x := ast.Unparen(n.X).(type) {
case *ast.SelectorExpr:
return sel(x)
case *ast.Ident:
@@ -1248,6 +1403,9 @@ func collectFreeVars(info *types.Info, file *ast.File, start, end token.Pos, nod
var obj types.Object
var isFree, prune bool
switch n := n.(type) {
+ case *ast.BranchStmt:
+ // Avoid including labels attached to branch statements.
+ return false
case *ast.Ident:
obj, isFree = id(n)
case *ast.SelectorExpr:
@@ -1410,12 +1568,14 @@ type fnExtractParams struct {
// canExtractFunction reports whether the code in the given range can be
// extracted to a function.
-func canExtractFunction(tok *token.File, start, end token.Pos, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) {
+func canExtractFunction(tok *token.File, start, end token.Pos, src []byte, curFile cursor.Cursor) (*fnExtractParams, bool, bool, error) {
if start == end {
return nil, false, false, fmt.Errorf("start and end are equal")
}
var err error
- start, end, err = adjustRangeForCommentsAndWhiteSpace(tok, start, end, src, file)
+ file := curFile.Node().(*ast.File)
+ // TODO(adonovan): simplify, using Cursor.
+ start, end, err = adjustRangeForCommentsAndWhiteSpace(tok, start, end, src, curFile)
if err != nil {
return nil, false, false, err
}
@@ -1681,8 +1841,8 @@ func varNameForType(t types.Type) (string, bool) {
return AbbreviateVarName(typeName), true
}
-// adjustReturnStatements adds "zero values" of the given types to each return statement
-// in the given AST node.
+// adjustReturnStatements adds "zero values" of the given types to each return
+// statement in the given AST node.
func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]ast.Expr, extractedBlock *ast.BlockStmt, qual types.Qualifier) error {
var zeroVals []ast.Expr
// Create "zero values" for each type.
@@ -1690,11 +1850,10 @@ func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]
var val ast.Expr
var isValid bool
for obj, typ := range seenVars {
- if typ != returnType.Type {
- continue
+ if typ == returnType.Type {
+ val, isValid = typesinternal.ZeroExpr(obj.Type(), qual)
+ break
}
- val, isValid = typesinternal.ZeroExpr(obj.Type(), qual)
- break
}
if !isValid {
return fmt.Errorf("could not find matching AST expression for %T", returnType.Type)
@@ -1835,3 +1994,122 @@ func cond[T any](cond bool, t, f T) T {
return f
}
}
+
+// replaceBranchStmtWithReturnStmt modifies the ast node to replace the given
+// branch statement with the given return statement.
+func replaceBranchStmtWithReturnStmt(block ast.Node, br *ast.BranchStmt, ret *ast.ReturnStmt) {
+ ast.Inspect(block, func(n ast.Node) bool {
+ // Look for the branch statement within a BlockStmt or CaseClause.
+ switch n := n.(type) {
+ case *ast.BlockStmt:
+ for i, stmt := range n.List {
+ if stmt == br {
+ n.List[i] = ret
+ return false
+ }
+ }
+ case *ast.CaseClause:
+ for i, stmt := range n.Body {
+ if stmt.Pos() == br.Pos() {
+ n.Body[i] = ret
+ return false
+ }
+ }
+ }
+ return true
+ })
+}
+
+// freeBranches returns all branch statements beneath cur whose continuation
+// lies outside the (start, end) range.
+func freeBranches(info *types.Info, cur cursor.Cursor, start, end token.Pos) (free []*ast.BranchStmt) {
+nextBranch:
+ for curBr := range cur.Preorder((*ast.BranchStmt)(nil)) {
+ br := curBr.Node().(*ast.BranchStmt)
+ if br.End() < start || br.Pos() > end {
+ continue
+ }
+ label, _ := info.Uses[br.Label].(*types.Label)
+ if label != nil && !(start <= label.Pos() && label.Pos() <= end) {
+ free = append(free, br)
+ continue
+ }
+ if br.Tok == token.BREAK || br.Tok == token.CONTINUE {
+ filter := []ast.Node{
+ (*ast.ForStmt)(nil),
+ (*ast.RangeStmt)(nil),
+ (*ast.SwitchStmt)(nil),
+ (*ast.TypeSwitchStmt)(nil),
+ (*ast.SelectStmt)(nil),
+ }
+ // Find innermost relevant ancestor for break/continue.
+ for curAncestor := range curBr.Parent().Enclosing(filter...) {
+ if l, ok := curAncestor.Parent().Node().(*ast.LabeledStmt); ok &&
+ label != nil &&
+ l.Label.Name == label.Name() {
+ continue
+ }
+ switch n := curAncestor.Node().(type) {
+ case *ast.ForStmt, *ast.RangeStmt:
+ if n.Pos() < start {
+ free = append(free, br)
+ }
+ continue nextBranch
+ case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt:
+ if br.Tok == token.BREAK {
+ if n.Pos() < start {
+ free = append(free, br)
+ }
+ continue nextBranch
+ }
+ }
+ }
+ }
+ }
+ return
+}
+
+// isFreeBranchStmt returns true if the relevant ancestor for the branch
+// statement at stack[len(stack)-1] cannot be found in the stack. This is used
+// when we are examining the extracted block, since type information isn't
+// available. We need to find the location of the label without using
+// types.Info.
+func isFreeBranchStmt(stack []ast.Node) bool {
+ switch node := stack[len(stack)-1].(type) {
+ case *ast.BranchStmt:
+ isLabeled := node.Label != nil
+ switch node.Tok {
+ case token.GOTO:
+ if isLabeled {
+ return !enclosingLabel(stack, node.Label.Name)
+ }
+ case token.BREAK, token.CONTINUE:
+ // Find innermost relevant ancestor for break/continue.
+ for i := len(stack) - 2; i >= 0; i-- {
+ n := stack[i]
+ if isLabeled {
+ l, ok := n.(*ast.LabeledStmt)
+ if !(ok && l.Label.Name == node.Label.Name) {
+ continue
+ }
+ }
+ switch n.(type) {
+ case *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt:
+ return false
+ }
+ }
+ }
+ }
+ // We didn't find the relevant ancestor on the path, so this must be a free branch statement.
+ return true
+}
+
+// enclosingLabel returns true if the given label is found on the stack.
+func enclosingLabel(stack []ast.Node, label string) bool {
+ for _, n := range stack {
+ if labelStmt, ok := n.(*ast.LabeledStmt); ok && labelStmt.Label.Name == label {
+ return true
+ }
+ }
+ return false
+}
diff --git a/gopls/internal/golang/extracttofile.go b/gopls/internal/golang/extracttofile.go
index 39fb28e624b..cc833f12c42 100644
--- a/gopls/internal/golang/extracttofile.go
+++ b/gopls/internal/golang/extracttofile.go
@@ -93,6 +93,7 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
return nil, fmt.Errorf("%s: %w", errorPrefix, err)
}
+ // Expand the selection, and compute the portion to extract.
start, end, firstSymbol, ok := selectedToplevelDecls(pgf, start, end)
if !ok {
return nil, fmt.Errorf("invalid selection")
@@ -109,7 +110,20 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
spaces := len(rest) - len(bytes.TrimLeft(rest, " \t\n"))
end += token.Pos(spaces)
pgf.CheckPos(end) // #70553
- // Inv: end is valid wrt pgf.Tok.
+ if !(start <= end) {
+ bug.Reportf("start: not before end")
+ }
+ // Inv: end is valid wrt pgf.Tok; env >= start.
+ fileStart := pgf.File.FileStart
+ pgf.CheckPos(fileStart) // #70553
+ if !(0 <= start-fileStart) {
+ bug.Reportf("start: out of bounds")
+ }
+ if !(int(end-fileStart) <= len(pgf.Src)) {
+ bug.Reportf("end: out of bounds")
+ }
+ // Inv: 0 <= start-fileStart <= end-fileStart <= len(Src).
+ src := pgf.Src[start-fileStart : end-fileStart]
replaceRange, err := pgf.PosRange(start, end)
if err != nil {
@@ -138,7 +152,7 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
}
var buf bytes.Buffer
- if c := copyrightComment(pgf.File); c != nil {
+ if c := CopyrightComment(pgf.File); c != nil {
start, end, err := pgf.NodeOffsets(c)
if err != nil {
return nil, err
@@ -176,9 +190,7 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
return nil, fmt.Errorf("%s: %w", errorPrefix, err)
}
- fileStart := pgf.File.FileStart
- pgf.CheckPos(fileStart) // #70553
- buf.Write(pgf.Src[start-fileStart : end-fileStart])
+ buf.Write(src)
newFileContent, err := format.Source(buf.Bytes())
if err != nil {
diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go
index e812c677541..dbd83ef071f 100644
--- a/gopls/internal/golang/fix.go
+++ b/gopls/internal/golang/fix.go
@@ -7,9 +7,7 @@ package golang
import (
"context"
"fmt"
- "go/ast"
"go/token"
- "go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/gopls/internal/analysis/embeddirective"
@@ -41,18 +39,14 @@ import (
// A fixer may return (nil, nil) if no fix is available.
type fixer func(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error)
-// A singleFileFixer is a Fixer that inspects only a single file,
-// and does not depend on data types from the cache package.
-//
-// TODO(adonovan): move fillstruct and undeclaredname into this
-// package, so we can remove the import restriction and push
-// the singleFile wrapper down into each singleFileFixer?
-type singleFileFixer func(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error)
+// A singleFileFixer is a [fixer] that inspects only a single file.
+type singleFileFixer func(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error)
-// singleFile adapts a single-file fixer to a Fixer.
+// singleFile adapts a [singleFileFixer] to a [fixer]
+// by discarding the snapshot and the context it needs.
func singleFile(fixer1 singleFileFixer) fixer {
- return func(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
- return fixer1(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.Types(), pkg.TypesInfo())
+ return func(_ context.Context, _ *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ return fixer1(pkg, pgf, start, end)
}
}
diff --git a/gopls/internal/golang/folding_range.go b/gopls/internal/golang/folding_range.go
index 9d80cc8de29..eed31e92944 100644
--- a/gopls/internal/golang/folding_range.go
+++ b/gopls/internal/golang/folding_range.go
@@ -6,6 +6,7 @@ package golang
import (
"bytes"
+ "cmp"
"context"
"go/ast"
"go/token"
@@ -20,14 +21,8 @@ import (
"golang.org/x/tools/gopls/internal/util/safetoken"
)
-// FoldingRangeInfo holds range and kind info of folding for an ast.Node
-type FoldingRangeInfo struct {
- Range protocol.Range
- Kind protocol.FoldingRangeKind
-}
-
// FoldingRange gets all of the folding range for f.
-func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) {
+func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lineFoldingOnly bool) ([]protocol.FoldingRange, error) {
// TODO(suzmue): consider limiting the number of folding ranges returned, and
// implement a way to prioritize folding ranges in that case.
pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full)
@@ -48,86 +43,97 @@ func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle,
}
// Get folding ranges for comments separately as they are not walked by ast.Inspect.
- ranges = append(ranges, commentsFoldingRange(pgf)...)
+ ranges := commentsFoldingRange(pgf)
- visit := func(n ast.Node) bool {
- rng := foldingRangeFunc(pgf, n, lineFoldingOnly)
- if rng != nil {
- ranges = append(ranges, rng)
- }
- return true
- }
// Walk the ast and collect folding ranges.
- ast.Inspect(pgf.File, visit)
-
- slices.SortFunc(ranges, func(x, y *FoldingRangeInfo) int {
- return protocol.CompareRange(x.Range, y.Range)
- })
-
- return ranges, nil
-}
+ filter := []ast.Node{
+ (*ast.BasicLit)(nil),
+ (*ast.BlockStmt)(nil),
+ (*ast.CallExpr)(nil),
+ (*ast.CaseClause)(nil),
+ (*ast.CommClause)(nil),
+ (*ast.CompositeLit)(nil),
+ (*ast.FieldList)(nil),
+ (*ast.GenDecl)(nil),
+ }
+ for cur := range pgf.Cursor.Preorder(filter...) {
+ // TODO(suzmue): include trailing empty lines before the closing
+ // parenthesis/brace.
+ var kind protocol.FoldingRangeKind
+ // start and end define the range of content to fold away.
+ var start, end token.Pos
+ switch n := cur.Node().(type) {
+ case *ast.BlockStmt:
+ // Fold between positions of or lines between "{" and "}".
+ start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly)
+
+ case *ast.CaseClause:
+ // Fold from position of ":" to end.
+ start, end = n.Colon+1, n.End()
+
+ case *ast.CommClause:
+ // Fold from position of ":" to end.
+ start, end = n.Colon+1, n.End()
+
+ case *ast.CallExpr:
+ // Fold between positions of or lines between "(" and ")".
+ start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly)
+
+ case *ast.FieldList:
+ // Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace.
+ start, end = getLineFoldingRange(pgf, n.Opening, n.Closing, lineFoldingOnly)
+
+ case *ast.GenDecl:
+ // If this is an import declaration, set the kind to be protocol.Imports.
+ if n.Tok == token.IMPORT {
+ kind = protocol.Imports
+ }
+ // Fold between positions of or lines between "(" and ")".
+ start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly)
+
+ case *ast.BasicLit:
+ // Fold raw string literals from position of "`" to position of "`".
+ if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' {
+ start, end = n.Pos(), n.End()
+ }
+
+ case *ast.CompositeLit:
+ // Fold between positions of or lines between "{" and "}".
+ start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly)
+
+ default:
+ panic(n)
+ }
-// foldingRangeFunc calculates the line folding range for ast.Node n
-func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo {
- // TODO(suzmue): include trailing empty lines before the closing
- // parenthesis/brace.
- var kind protocol.FoldingRangeKind
- // start and end define the range of content to fold away.
- var start, end token.Pos
- switch n := n.(type) {
- case *ast.BlockStmt:
- // Fold between positions of or lines between "{" and "}".
- start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly)
- case *ast.CaseClause:
- // Fold from position of ":" to end.
- start, end = n.Colon+1, n.End()
- case *ast.CommClause:
- // Fold from position of ":" to end.
- start, end = n.Colon+1, n.End()
- case *ast.CallExpr:
- // Fold between positions of or lines between "(" and ")".
- start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly)
- case *ast.FieldList:
- // Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace.
- start, end = getLineFoldingRange(pgf, n.Opening, n.Closing, lineFoldingOnly)
- case *ast.GenDecl:
- // If this is an import declaration, set the kind to be protocol.Imports.
- if n.Tok == token.IMPORT {
- kind = protocol.Imports
+ // Check that folding positions are valid.
+ if !start.IsValid() || !end.IsValid() {
+ continue
+ }
+ if start == end {
+ // Nothing to fold.
+ continue
+ }
+ // in line folding mode, do not fold if the start and end lines are the same.
+ if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) {
+ continue
}
- // Fold between positions of or lines between "(" and ")".
- start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly)
- case *ast.BasicLit:
- // Fold raw string literals from position of "`" to position of "`".
- if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' {
- start, end = n.Pos(), n.End()
+ rng, err := pgf.PosRange(start, end)
+ if err != nil {
+ bug.Reportf("failed to create range: %s", err) // can't happen
+ continue
}
- case *ast.CompositeLit:
- // Fold between positions of or lines between "{" and "}".
- start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly)
+ ranges = append(ranges, foldingRange(kind, rng))
}
- // Check that folding positions are valid.
- if !start.IsValid() || !end.IsValid() {
- return nil
- }
- if start == end {
- // Nothing to fold.
- return nil
- }
- // in line folding mode, do not fold if the start and end lines are the same.
- if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) {
- return nil
- }
- rng, err := pgf.PosRange(start, end)
- if err != nil {
- bug.Reportf("failed to create range: %s", err) // can't happen
- return nil
- }
- return &FoldingRangeInfo{
- Range: rng,
- Kind: kind,
- }
+ // Sort by start position.
+ slices.SortFunc(ranges, func(x, y protocol.FoldingRange) int {
+ if d := cmp.Compare(x.StartLine, y.StartLine); d != 0 {
+ return d
+ }
+ return cmp.Compare(x.StartCharacter, y.StartCharacter)
+ })
+
+ return ranges, nil
}
// getLineFoldingRange returns the folding range for nodes with parentheses/braces/brackets
@@ -196,7 +202,7 @@ func getLineFoldingRange(pgf *parsego.File, open, close token.Pos, lineFoldingOn
// commentsFoldingRange returns the folding ranges for all comment blocks in file.
// The folding range starts at the end of the first line of the comment block, and ends at the end of the
// comment block and has kind protocol.Comment.
-func commentsFoldingRange(pgf *parsego.File) (comments []*FoldingRangeInfo) {
+func commentsFoldingRange(pgf *parsego.File) (comments []protocol.FoldingRange) {
tokFile := pgf.Tok
for _, commentGrp := range pgf.File.Comments {
startGrpLine, endGrpLine := safetoken.Line(tokFile, commentGrp.Pos()), safetoken.Line(tokFile, commentGrp.End())
@@ -218,11 +224,19 @@ func commentsFoldingRange(pgf *parsego.File) (comments []*FoldingRangeInfo) {
bug.Reportf("failed to create mapped range: %s", err) // can't happen
continue
}
- comments = append(comments, &FoldingRangeInfo{
- // Fold from the end of the first line comment to the end of the comment block.
- Range: rng,
- Kind: protocol.Comment,
- })
+ // Fold from the end of the first line comment to the end of the comment block.
+ comments = append(comments, foldingRange(protocol.Comment, rng))
}
return comments
}
+
+func foldingRange(kind protocol.FoldingRangeKind, rng protocol.Range) protocol.FoldingRange {
+ return protocol.FoldingRange{
+ // I have no idea why LSP doesn't use a protocol.Range here.
+ StartLine: rng.Start.Line,
+ StartCharacter: rng.Start.Character,
+ EndLine: rng.End.Line,
+ EndCharacter: rng.End.Character,
+ Kind: string(kind),
+ }
+}
diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go
index de4ec3a642c..ded00deef38 100644
--- a/gopls/internal/golang/format.go
+++ b/gopls/internal/golang/format.go
@@ -35,15 +35,16 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr
ctx, done := event.Start(ctx, "golang.Format")
defer done()
- // Generated files shouldn't be edited. So, don't format them
- if IsGenerated(ctx, snapshot, fh.URI()) {
- return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Path())
- }
-
pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full)
if err != nil {
return nil, err
}
+
+ // Generated files shouldn't be edited. So, don't format them.
+ if ast.IsGenerated(pgf.File) {
+ return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Path())
+ }
+
// Even if this file has parse errors, it might still be possible to format it.
// Using format.Node on an AST with errors may result in code being modified.
// Attempt to format the source of this file instead.
@@ -137,7 +138,13 @@ func computeImportEdits(ctx context.Context, pgf *parsego.File, snapshot *cache.
// Build up basic information about the original file.
isource, err := imports.NewProcessEnvSource(options.Env, filename, pgf.File.Name.Name)
+ if err != nil {
+ return nil, nil, err
+ }
var source imports.Source
+
+ // Keep this in sync with [cache.Session.createView] (see the TODO there: we
+ // should factor out the handling of the ImportsSource setting).
switch snapshot.Options().ImportsSource {
case settings.ImportsSourceGopls:
source = snapshot.NewGoplsSource(isource)
@@ -146,6 +153,9 @@ func computeImportEdits(ctx context.Context, pgf *parsego.File, snapshot *cache.
case settings.ImportsSourceGoimports:
source = isource
}
+ // imports require a current metadata graph
+ // TODO(rfindlay) improve the API
+ snapshot.WorkspaceMetadata(ctx)
allFixes, err := imports.FixImports(ctx, filename, pgf.Src, goroot, options.Env.Logf, source)
if err != nil {
return nil, nil, err
diff --git a/gopls/internal/golang/freesymbols.go b/gopls/internal/golang/freesymbols.go
index 2c9e25165f6..336025367f5 100644
--- a/gopls/internal/golang/freesymbols.go
+++ b/gopls/internal/golang/freesymbols.go
@@ -342,7 +342,7 @@ func freeRefs(pkg *types.Package, info *types.Info, file *ast.File, start, end t
for {
suffix = append(suffix, info.Uses[sel.Sel])
- switch x := astutil.Unparen(sel.X).(type) {
+ switch x := ast.Unparen(sel.X).(type) {
case *ast.Ident:
return id(x, suffix)
default:
diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go
index 7fc584f2c1a..93c89f3af97 100644
--- a/gopls/internal/golang/hover.go
+++ b/gopls/internal/golang/hover.go
@@ -7,6 +7,7 @@ package golang
import (
"bytes"
"context"
+ "encoding/json"
"fmt"
"go/ast"
"go/constant"
@@ -37,6 +38,7 @@ import (
gastutil "golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ internalastutil "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/tokeninternal"
@@ -48,37 +50,47 @@ import (
// It is formatted in one of several formats as determined by the
// HoverKind setting.
type hoverResult struct {
- // synopsis is a single sentence synopsis of the symbol's documentation.
+ // The fields below are exported to define the JSON hover format.
+ // TODO(golang/go#70233): (re)remove support for JSON hover.
+
+ // Synopsis is a single sentence Synopsis of the symbol's documentation.
//
- // TODO(adonovan): in what syntax? It (usually) comes from doc.synopsis,
+ // TODO(adonovan): in what syntax? It (usually) comes from doc.Synopsis,
// which produces "Text" form, but it may be fed to
// DocCommentToMarkdown, which expects doc comment syntax.
- synopsis string
+ Synopsis string `json:"synopsis"`
- // fullDocumentation is the symbol's full documentation.
- fullDocumentation string
+ // FullDocumentation is the symbol's full documentation.
+ FullDocumentation string `json:"fullDocumentation"`
- // signature is the symbol's signature.
- signature string
+ // Signature is the symbol's Signature.
+ Signature string `json:"signature"`
- // singleLine is a single line describing the symbol.
+ // SingleLine is a single line describing the symbol.
// This is recommended only for use in clients that show a single line for hover.
- singleLine string
+ SingleLine string `json:"singleLine"`
- // symbolName is the human-readable name to use for the symbol in links.
- symbolName string
+ // SymbolName is the human-readable name to use for the symbol in links.
+ SymbolName string `json:"symbolName"`
- // linkPath is the path of the package enclosing the given symbol,
+ // LinkPath is the path of the package enclosing the given symbol,
// with the module portion (if any) replaced by "module@version".
//
// For example: "github.com/google/go-github/v48@v48.1.0/github".
//
- // Use LinkTarget + "/" + linkPath + "#" + LinkAnchor to form a pkgsite URL.
- linkPath string
+ // Use LinkTarget + "/" + LinkPath + "#" + LinkAnchor to form a pkgsite URL.
+ LinkPath string `json:"linkPath"`
- // linkAnchor is the pkg.go.dev link anchor for the given symbol.
+ // LinkAnchor is the pkg.go.dev link anchor for the given symbol.
// For example, the "Node" part of "pkg.go.dev/go/ast#Node".
- linkAnchor string
+ LinkAnchor string `json:"linkAnchor"`
+
+ // New fields go below, and are unexported. The existing
+ // exported fields are underspecified and have already
+ // constrained our movements too much. A detailed JSON
+ // interface might be nice, but it needs a design and a
+ // precise specification.
+ // TODO(golang/go#70233): (re)deprecate the JSON hover output.
// typeDecl is the declaration syntax for a type,
// or "" for a non-type.
@@ -127,6 +139,28 @@ func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, positi
}, nil
}
+// findRhsTypeDecl finds an alias's rhs type and returns its declaration.
+// The rhs of an alias might be an alias as well, but we feel this is a rare case.
+// It returns an empty string if the given obj is not an alias.
+func findRhsTypeDecl(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, obj types.Object) (string, error) {
+ if alias, ok := obj.Type().(*types.Alias); ok {
+ // we choose Rhs instead of types.Unalias to make the connection between original alias
+ // and the corresponding aliased type clearer.
+ // types.Unalias brings confusion because it breaks the connection from A to C given
+ // the alias chain like 'type ( A = B; B =C ; )' except we show all transitive alias
+ // from start to the end. As it's rare, we don't do so.
+ t := alias.Rhs()
+ switch o := t.(type) {
+ case *types.Named:
+ obj = o.Obj()
+ declPGF1, declPos1, _ := parseFull(ctx, snapshot, pkg.FileSet(), obj.Pos())
+ realTypeDecl, _, err := typeDeclContent(declPGF1, declPos1, obj)
+ return realTypeDecl, err
+ }
+ }
+ return "", nil
+}
+
// hover computes hover information at the given position. If we do not support
// hovering at the position, it returns _, nil, nil: an error is only returned
// if the position is valid but we fail to compute hover information.
@@ -253,6 +287,10 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
}
}
+ // By convention, we qualify hover information relative to the package
+ // from which the request originated.
+ qual := typesinternal.FileQualifier(pgf.File, pkg.Types())
+
// Handle hover over identifier.
// The general case: compute hover information for the object referenced by
@@ -271,10 +309,6 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
hoverRange = &rng
}
- // By convention, we qualify hover information relative to the package
- // from which the request originated.
- qual := typesinternal.FileQualifier(pgf.File, pkg.Types())
-
// Handle type switch identifiers as a special case, since they don't have an
// object.
//
@@ -284,9 +318,9 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
typesinternal.SetVarKind(v, typesinternal.LocalVar)
signature := types.ObjectString(v, qual)
return *hoverRange, &hoverResult{
- signature: signature,
- singleLine: signature,
- symbolName: v.Name(),
+ Signature: signature,
+ SingleLine: signature,
+ SymbolName: v.Name(),
}, nil
}
@@ -310,6 +344,42 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
// By default, types.ObjectString provides a reasonable signature.
signature := objectString(obj, qual, declPos, declPGF.Tok, spec)
+
+ // When hovering over a reference to a promoted struct field,
+ // show the implicitly selected intervening fields.
+ cur, ok := pgf.Cursor.FindByPos(pos, pos)
+ if !ok {
+ return protocol.Range{}, nil, fmt.Errorf("Invalid hover position, failed to get cursor")
+ }
+ if obj, ok := obj.(*types.Var); ok && obj.IsField() {
+ if selExpr, ok := cur.Parent().Node().(*ast.SelectorExpr); ok {
+ sel := pkg.TypesInfo().Selections[selExpr]
+ if len(sel.Index()) > 1 {
+ var buf bytes.Buffer
+ buf.WriteString(" // through ")
+ t := typesinternal.Unpointer(sel.Recv())
+ for i, index := range sel.Index()[:len(sel.Index())-1] {
+ if i > 0 {
+ buf.WriteString(", ")
+ }
+ field := typesinternal.Unpointer(t.Underlying()).(*types.Struct).Field(index)
+ t = field.Type()
+ // Inv: fieldType is N or *N for some NamedOrAlias type N.
+ if ptr, ok := t.(*types.Pointer); ok {
+ buf.WriteString("*")
+ t = ptr.Elem()
+ }
+ // Be defensive in case of ill-typed code:
+ if named, ok := t.(typesinternal.NamedOrAlias); ok {
+ buf.WriteString(named.Obj().Name())
+ }
+ }
+ // Update signature to include embedded struct info.
+ signature += buf.String()
+ }
+ }
+ }
+
singleLineSignature := signature
// Display struct tag for struct fields at the end of the signature.
@@ -342,15 +412,10 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
// use the default build config for all other types, even
// if they embed platform-variant types.
//
- var sizeOffset string // optional size/offset description
- // debugging #69362: unexpected nil Defs[ident] value (?)
- _ = ident.Pos() // (can't be nil due to check after referencedObject)
- _ = pkg.TypesInfo() // (can't be nil due to check in call to inferredSignature)
- _ = pkg.TypesInfo().Defs // (can't be nil due to nature of cache.Package)
- if def, ok := pkg.TypesInfo().Defs[ident]; ok {
- _ = def.Pos() // can't be nil due to reasoning in #69362.
- }
- if def, ok := pkg.TypesInfo().Defs[ident]; ok && ident.Pos() == def.Pos() {
+ var sizeOffset string
+
+ // As painfully learned in golang/go#69362, Defs can contain nil entries.
+ if def, _ := pkg.TypesInfo().Defs[ident]; def != nil && ident.Pos() == def.Pos() {
// This is the declaring identifier.
// (We can't simply use ident.Pos() == obj.Pos() because
// referencedObject prefers the TypeName for an embedded field).
@@ -393,46 +458,20 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
_, isTypeName := obj.(*types.TypeName)
_, isTypeParam := types.Unalias(obj.Type()).(*types.TypeParam)
if isTypeName && !isTypeParam {
- spec, ok := spec.(*ast.TypeSpec)
- if !ok {
- // We cannot find a TypeSpec for this type or alias declaration
- // (that is not a type parameter or a built-in).
- // This should be impossible even for ill-formed trees;
- // we suspect that AST repair may be creating inconsistent
- // positions. Don't report a bug in that case. (#64241)
- errorf := fmt.Errorf
- if !declPGF.Fixed() {
- errorf = bug.Errorf
- }
- return protocol.Range{}, nil, errorf("type name %q without type spec", obj.Name())
+ var spec1 *ast.TypeSpec
+ typeDecl, spec1, err = typeDeclContent(declPGF, declPos, obj)
+ if err != nil {
+ return protocol.Range{}, nil, err
}
- // Format the type's declaration syntax.
- {
- // Don't duplicate comments.
- spec2 := *spec
- spec2.Doc = nil
- spec2.Comment = nil
-
- var b strings.Builder
- b.WriteString("type ")
- fset := tokeninternal.FileSetFor(declPGF.Tok)
- // TODO(adonovan): use a smarter formatter that omits
- // inaccessible fields (non-exported ones from other packages).
- if err := format.Node(&b, fset, &spec2); err != nil {
- return protocol.Range{}, nil, err
- }
- typeDecl = b.String()
-
- // Splice in size/offset at end of first line.
- // "type T struct { // size=..."
- if sizeOffset != "" {
- nl := strings.IndexByte(typeDecl, '\n')
- if nl < 0 {
- nl = len(typeDecl)
- }
- typeDecl = typeDecl[:nl] + " // " + sizeOffset + typeDecl[nl:]
+ // Splice in size/offset at end of first line.
+ // "type T struct { // size=..."
+ if sizeOffset != "" {
+ nl := strings.IndexByte(typeDecl, '\n')
+ if nl < 0 {
+ nl = len(typeDecl)
}
+ typeDecl = typeDecl[:nl] + " // " + sizeOffset + typeDecl[nl:]
}
// Promoted fields
@@ -467,7 +506,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
// already been displayed when the node was formatted
// above. Don't list these again.
var skip map[string]bool
- if iface, ok := spec.Type.(*ast.InterfaceType); ok {
+ if iface, ok := spec1.Type.(*ast.InterfaceType); ok {
if iface.Methods.List != nil {
for _, m := range iface.Methods.List {
if len(m.Names) == 1 {
@@ -509,6 +548,12 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
}
}
+ // realTypeDecl is defined to store the underlying definition of an alias.
+ realTypeDecl, _ := findRhsTypeDecl(ctx, snapshot, pkg, obj) // tolerate the error
+ if realTypeDecl != "" {
+ typeDecl += fmt.Sprintf("\n\n%s", realTypeDecl)
+ }
+
// Compute link data (on pkg.go.dev or other documentation host).
//
// If linkPath is empty, the symbol is not linkable.
@@ -606,7 +651,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
linkPath = ""
} else if linkMeta.Module != nil && linkMeta.Module.Version != "" {
mod := linkMeta.Module
- linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1)
+ linkPath = strings.Replace(linkPath, mod.Path, cache.ResolvedString(mod), 1)
}
var footer string
@@ -615,13 +660,13 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
}
return *hoverRange, &hoverResult{
- synopsis: doc.Synopsis(docText),
- fullDocumentation: docText,
- singleLine: singleLineSignature,
- symbolName: linkName,
- signature: signature,
- linkPath: linkPath,
- linkAnchor: anchor,
+ Synopsis: doc.Synopsis(docText),
+ FullDocumentation: docText,
+ SingleLine: singleLineSignature,
+ SymbolName: linkName,
+ Signature: signature,
+ LinkPath: linkPath,
+ LinkAnchor: anchor,
typeDecl: typeDecl,
methods: methods,
promotedFields: fields,
@@ -629,6 +674,39 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
}, nil
}
+// typeDeclContent returns a well formatted type definition.
+func typeDeclContent(declPGF *parsego.File, declPos token.Pos, obj types.Object) (string, *ast.TypeSpec, error) {
+ _, spec, _ := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^3
+ // Don't duplicate comments.
+ spec1, ok := spec.(*ast.TypeSpec)
+ if !ok {
+ // We cannot find a TypeSpec for this type or alias declaration
+ // (that is not a type parameter or a built-in).
+ // This should be impossible even for ill-formed trees;
+ // we suspect that AST repair may be creating inconsistent
+ // positions. Don't report a bug in that case. (#64241)
+ errorf := fmt.Errorf
+ if !declPGF.Fixed() {
+ errorf = bug.Errorf
+ }
+ return "", nil, errorf("type name %q without type spec", obj.Name())
+ }
+ spec2 := *spec1
+ spec2.Doc = nil
+ spec2.Comment = nil
+
+ var b strings.Builder
+ b.WriteString("type ")
+ fset := tokeninternal.FileSetFor(declPGF.Tok)
+ // TODO(adonovan): use a smarter formatter that omits
+ // inaccessible fields (non-exported ones from other packages).
+ if err := format.Node(&b, fset, &spec2); err != nil {
+ return "", nil, err
+ }
+ typeDecl := b.String()
+ return typeDecl, spec1, nil
+}
+
// hoverBuiltin computes hover information when hovering over a builtin
// identifier.
func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*hoverResult, error) {
@@ -638,8 +716,8 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec
if obj.Name() == "Error" {
signature := obj.String()
return &hoverResult{
- signature: signature,
- singleLine: signature,
+ Signature: signature,
+ SingleLine: signature,
// TODO(rfindley): these are better than the current behavior.
// SymbolName: "(error).Error",
// LinkPath: "builtin",
@@ -682,13 +760,13 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec
docText := comment.Text()
return &hoverResult{
- synopsis: doc.Synopsis(docText),
- fullDocumentation: docText,
- signature: signature,
- singleLine: obj.String(),
- symbolName: obj.Name(),
- linkPath: "builtin",
- linkAnchor: obj.Name(),
+ Synopsis: doc.Synopsis(docText),
+ FullDocumentation: docText,
+ Signature: signature,
+ SingleLine: obj.String(),
+ SymbolName: obj.Name(),
+ LinkPath: "builtin",
+ LinkAnchor: obj.Name(),
}, nil
}
@@ -740,9 +818,9 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa
docText := comment.Text()
return rng, &hoverResult{
- signature: "package " + string(impMetadata.Name),
- synopsis: doc.Synopsis(docText),
- fullDocumentation: docText,
+ Signature: "package " + string(impMetadata.Name),
+ Synopsis: doc.Synopsis(docText),
+ FullDocumentation: docText,
}, nil
}
@@ -798,9 +876,9 @@ func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *h
}
return rng, &hoverResult{
- signature: "package " + string(pkg.Metadata().Name),
- synopsis: doc.Synopsis(docText),
- fullDocumentation: docText,
+ Signature: "package " + string(pkg.Metadata().Name),
+ Synopsis: doc.Synopsis(docText),
+ FullDocumentation: docText,
footer: footer,
}, nil
}
@@ -926,8 +1004,8 @@ func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Ran
}
hover := b.String()
return rng, &hoverResult{
- synopsis: hover,
- fullDocumentation: hover,
+ Synopsis: hover,
+ FullDocumentation: hover,
}, nil
}
@@ -966,7 +1044,7 @@ func hoverReturnStatement(pgf *parsego.File, path []ast.Node, ret *ast.ReturnStm
}
buf.WriteByte(')')
return rng, &hoverResult{
- signature: buf.String(),
+ Signature: buf.String(),
}, nil
}
@@ -1005,9 +1083,9 @@ func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Ra
}
res := &hoverResult{
- signature: fmt.Sprintf("Embedding %q", pattern),
- synopsis: s.String(),
- fullDocumentation: s.String(),
+ Signature: fmt.Sprintf("Embedding %q", pattern),
+ Synopsis: s.String(),
+ FullDocumentation: s.String(),
}
return rng, res, nil
}
@@ -1242,10 +1320,17 @@ func formatHover(h *hoverResult, options *settings.Options, pkgURL func(path Pac
switch options.HoverKind {
case settings.SingleLine:
- return h.singleLine, nil
+ return h.SingleLine, nil
case settings.NoDocumentation:
- return maybeFenced(h.signature), nil
+ return maybeFenced(h.Signature), nil
+
+ case settings.Structured:
+ b, err := json.Marshal(h)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
case settings.SynopsisDocumentation, settings.FullDocumentation:
var sections [][]string // assembled below
@@ -1256,20 +1341,20 @@ func formatHover(h *hoverResult, options *settings.Options, pkgURL func(path Pac
// but not Signature, which is redundant (= TypeDecl + "\n" + Methods).
// For all other symbols, we display Signature;
// TypeDecl and Methods are empty.
- // (Now that JSON is no more, we could rationalize this.)
+ // TODO(golang/go#70233): When JSON is no more, we could rationalize this.
if h.typeDecl != "" {
sections = append(sections, []string{maybeFenced(h.typeDecl)})
} else {
- sections = append(sections, []string{maybeFenced(h.signature)})
+ sections = append(sections, []string{maybeFenced(h.Signature)})
}
// Doc section.
var doc string
switch options.HoverKind {
case settings.SynopsisDocumentation:
- doc = h.synopsis
+ doc = h.Synopsis
case settings.FullDocumentation:
- doc = h.fullDocumentation
+ doc = h.FullDocumentation
}
if options.PreferredContentFormat == protocol.Markdown {
doc = DocCommentToMarkdown(doc, options)
@@ -1392,7 +1477,7 @@ func StdSymbolOf(obj types.Object) *stdlib.Symbol {
// If pkgURL is non-nil, it should be used to generate doc links.
func formatLink(h *hoverResult, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) string {
- if options.LinksInHover == settings.LinksInHover_None || h.linkPath == "" {
+ if options.LinksInHover == settings.LinksInHover_None || h.LinkPath == "" {
return ""
}
var url protocol.URI
@@ -1400,26 +1485,26 @@ func formatLink(h *hoverResult, options *settings.Options, pkgURL func(path Pack
if pkgURL != nil { // LinksInHover == "gopls"
// Discard optional module version portion.
// (Ideally the hoverResult would retain the structure...)
- path := h.linkPath
- if module, versionDir, ok := strings.Cut(h.linkPath, "@"); ok {
+ path := h.LinkPath
+ if module, versionDir, ok := strings.Cut(h.LinkPath, "@"); ok {
// "module@version/dir"
path = module
if _, dir, ok := strings.Cut(versionDir, "/"); ok {
path += "/" + dir
}
}
- url = pkgURL(PackagePath(path), h.linkAnchor)
+ url = pkgURL(PackagePath(path), h.LinkAnchor)
caption = "in gopls doc viewer"
} else {
if options.LinkTarget == "" {
return ""
}
- url = cache.BuildLink(options.LinkTarget, h.linkPath, h.linkAnchor)
+ url = cache.BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor)
caption = "on " + options.LinkTarget
}
switch options.PreferredContentFormat {
case protocol.Markdown:
- return fmt.Sprintf("[`%s` %s](%s)", h.symbolName, caption, url)
+ return fmt.Sprintf("[`%s` %s](%s)", h.SymbolName, caption, url)
case protocol.PlainText:
return ""
default:
@@ -1454,16 +1539,10 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe
stack := make([]ast.Node, 0, 20)
// Allocate the closure once, outside the loop.
- f := func(n ast.Node) bool {
+ f := func(n ast.Node, stack []ast.Node) bool {
if found {
return false
}
- if n != nil {
- stack = append(stack, n) // push
- } else {
- stack = stack[:len(stack)-1] // pop
- return false
- }
// Skip subtrees (incl. files) that don't contain the search point.
if !(n.Pos() <= pos && pos < n.End()) {
@@ -1548,7 +1627,7 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe
return true
}
for _, file := range files {
- ast.Inspect(file, f)
+ internalastutil.PreorderStack(file, stack, f)
if found {
return decl, spec, field
}
diff --git a/gopls/internal/golang/identifier_test.go b/gopls/internal/golang/identifier_test.go
index 8206d8731ae..0823793466f 100644
--- a/gopls/internal/golang/identifier_test.go
+++ b/gopls/internal/golang/identifier_test.go
@@ -41,7 +41,6 @@ func TestSearchForEnclosing(t *testing.T) {
}
for _, test := range tests {
- test := test
t.Run(test.desc, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors|parser.SkipObjectResolution)
diff --git a/gopls/internal/golang/implementation.go b/gopls/internal/golang/implementation.go
index fe0a34a1c80..675b232d0eb 100644
--- a/gopls/internal/golang/implementation.go
+++ b/gopls/internal/golang/implementation.go
@@ -11,8 +11,9 @@ import (
"go/ast"
"go/token"
"go/types"
+ "iter"
"reflect"
- "sort"
+ "slices"
"strings"
"sync"
@@ -21,11 +22,16 @@ import (
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/cache/methodsets"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
+ "golang.org/x/tools/gopls/internal/util/moreiters"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/astutil/edge"
"golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/typesinternal"
)
// This file defines the new implementation of the 'implementation'
@@ -57,30 +63,70 @@ func Implementation(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
if err != nil {
return nil, err
}
-
- // Sort and de-duplicate locations.
- sort.Slice(locs, func(i, j int) bool {
- return protocol.CompareLocation(locs[i], locs[j]) < 0
- })
- out := locs[:0]
- for _, loc := range locs {
- if len(out) == 0 || out[len(out)-1] != loc {
- out = append(out, loc)
- }
- }
- locs = out
-
+ slices.SortFunc(locs, protocol.CompareLocation)
+ locs = slices.Compact(locs) // de-duplicate
return locs, nil
}
func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.Location, error) {
- // First, find the object referenced at the cursor by type checking the
- // current package.
- obj, pkg, err := implementsObj(ctx, snapshot, fh.URI(), pp)
+ // Type check the current package.
+ pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+ pos, err := pgf.PositionPos(pp)
if err != nil {
return nil, err
}
+ // Find implementations based on func signatures.
+ if locs, err := implFuncs(pkg, pgf, pos); err != errNotHandled {
+ return locs, err
+ }
+
+ // Find implementations based on method sets.
+ var (
+ locsMu sync.Mutex
+ locs []protocol.Location
+ )
+ // relation=0 here means infer direction of the relation
+ // (Supertypes/Subtypes) from concreteness of query type/method.
+ // (Ideally the implementations request would provide directionality
+ // so that one could ask for, say, the superinterfaces of io.ReadCloser;
+ // see https://github.com/golang/go/issues/68641#issuecomment-2269293762.)
+ const relation = methodsets.TypeRelation(0)
+ err = implementationsMsets(ctx, snapshot, pkg, pgf, pos, relation, func(_ metadata.PackagePath, _ string, _ bool, loc protocol.Location) {
+ locsMu.Lock()
+ locs = append(locs, loc)
+ locsMu.Unlock()
+ })
+ return locs, err
+}
+
+// An implYieldFunc is a callback called for each match produced by the implementation machinery.
+// - name describes the type or method.
+// - abstract indicates that the result is an interface type or interface method.
+//
+// implYieldFunc implementations must be concurrency-safe.
+type implYieldFunc func(pkgpath metadata.PackagePath, name string, abstract bool, loc protocol.Location)
+
+// implementationsMsets computes implementations of the type at the
+// specified position, by method sets.
+//
+// rel specifies the desired direction of the relation: Subtype,
+// Supertype, or both. As a special case, zero means infer the
+// direction from the concreteness of the query object: Supertype for
+// a concrete type, Subtype for an interface.
+//
+// It is shared by Implementations and TypeHierarchy.
+func implementationsMsets(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, pos token.Pos, rel methodsets.TypeRelation, yield implYieldFunc) error {
+ // First, find the object referenced at the cursor.
+ // The object may be declared in a different package.
+ obj, err := implementsObj(pkg.TypesInfo(), pgf.File, pos)
+ if err != nil {
+ return err
+ }
+
// If the resulting object has a position, we can expand the search to types
// in the declaring package(s). In this case, we must re-type check these
// packages in the same realm.
@@ -99,11 +145,11 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
declURI = protocol.URIFromPath(declPosn.Filename)
declMPs, err := snapshot.MetadataForFile(ctx, declURI)
if err != nil {
- return nil, err
+ return err
}
metadata.RemoveIntermediateTestVariants(&declMPs)
if len(declMPs) == 0 {
- return nil, fmt.Errorf("no packages for file %s", declURI)
+ return fmt.Errorf("no packages for file %s", declURI)
}
ids := make([]PackageID, len(declMPs))
for i, mp := range declMPs {
@@ -111,7 +157,7 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
}
localPkgs, err = snapshot.TypeCheck(ctx, ids...)
if err != nil {
- return nil, err
+ return err
}
}
@@ -119,23 +165,9 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
// Is the selected identifier a type name or method?
// (For methods, report the corresponding method names.)
- //
- // This logic is reused for local queries.
- typeOrMethod := func(obj types.Object) (types.Type, *types.Func) {
- switch obj := obj.(type) {
- case *types.TypeName:
- return obj.Type(), nil
- case *types.Func:
- // For methods, use the receiver type, which may be anonymous.
- if recv := obj.Signature().Recv(); recv != nil {
- return recv.Type(), obj
- }
- }
- return nil, nil
- }
queryType, queryMethod := typeOrMethod(obj)
if queryType == nil {
- return nil, bug.Errorf("%s is not a type or method", obj.Name()) // should have been handled by implementsObj
+ return bug.Errorf("%s is not a type or method", obj.Name()) // should have been handled by implementsObj
}
// Compute the method-set fingerprint used as a key to the global search.
@@ -143,7 +175,15 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
if !hasMethods {
// A type with no methods yields an empty result.
// (No point reporting that every type satisfies 'any'.)
- return nil, nil
+ return nil
+ }
+
+ // If the client specified no relation, infer it
+ // from the concreteness of the query type.
+ if rel == 0 {
+ rel = cond(types.IsInterface(queryType),
+ methodsets.Subtype,
+ methodsets.Supertype)
}
// The global search needs to look at every package in the
@@ -157,7 +197,7 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
// be optimized by being applied as soon as each package is available.
globalMetas, err := snapshot.AllMetadata(ctx)
if err != nil {
- return nil, err
+ return err
}
metadata.RemoveIntermediateTestVariants(&globalMetas)
globalIDs := make([]PackageID, 0, len(globalMetas))
@@ -174,36 +214,32 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
}
indexes, err := snapshot.MethodSets(ctx, globalIDs...)
if err != nil {
- return nil, fmt.Errorf("querying method sets: %v", err)
+ return fmt.Errorf("querying method sets: %v", err)
}
// Search local and global packages in parallel.
- var (
- group errgroup.Group
- locsMu sync.Mutex
- locs []protocol.Location
- )
+ var group errgroup.Group
+
// local search
- for _, localPkg := range localPkgs {
+ for _, pkg := range localPkgs {
// The localImplementations algorithm assumes needle and haystack
// belong to a single package (="realm" of types symbol identities),
// so we need to recompute obj for each local package.
// (By contrast the global algorithm is name-based.)
- declPkg := localPkg
group.Go(func() error {
- pkgID := declPkg.Metadata().ID
- declFile, err := declPkg.File(declURI)
+ pkgID := pkg.Metadata().ID
+
+ // Find declaring identifier based on (URI, offset)
+ // so that localImplementations can locate the
+ // corresponding obj/queryType/queryMethod in pkg.
+ declFile, err := pkg.File(declURI)
if err != nil {
return err // "can't happen"
}
-
- // Find declaration of corresponding object
- // in this package based on (URI, offset).
pos, err := safetoken.Pos(declFile.Tok, declOffset)
if err != nil {
return err // also "can't happen"
}
- // TODO(adonovan): simplify: use objectsAt?
path := pathEnclosingObjNode(declFile.File, pos)
if path == nil {
return ErrNoIdentFound // checked earlier
@@ -212,27 +248,16 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
if !ok {
return ErrNoIdentFound // checked earlier
}
- // Shadow obj, queryType, and queryMethod in this package.
- obj := declPkg.TypesInfo().ObjectOf(id) // may be nil
- queryType, queryMethod := typeOrMethod(obj)
- if queryType == nil {
- return fmt.Errorf("querying method sets in package %q: %v", pkgID, err)
- }
- localLocs, err := localImplementations(ctx, snapshot, declPkg, queryType, queryMethod)
- if err != nil {
+ if err := localImplementations(ctx, snapshot, pkg, id, rel, yield); err != nil {
return fmt.Errorf("querying local implementations %q: %v", pkgID, err)
}
- locsMu.Lock()
- locs = append(locs, localLocs...)
- locsMu.Unlock()
return nil
})
}
// global search
for _, index := range indexes {
- index := index
group.Go(func() error {
- for _, res := range index.Search(key, queryMethod) {
+ for _, res := range index.Search(key, rel, queryMethod) {
loc := res.Location
// Map offsets to protocol.Locations in parallel (may involve I/O).
group.Go(func() error {
@@ -240,20 +265,33 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
if err != nil {
return err
}
- locsMu.Lock()
- locs = append(locs, ploc)
- locsMu.Unlock()
+ yield(index.PkgPath, res.TypeName, res.IsInterface, ploc)
return nil
})
}
return nil
})
}
- if err := group.Wait(); err != nil {
- return nil, err
- }
+ return group.Wait()
+}
- return locs, nil
+// typeOrMethod returns the type and optional method to use in an
+// Implementations operation on the specified symbol.
+// It returns a nil type to indicate that the query should not proceed.
+//
+// (It is factored out to allow it to be used both in the query package
+// then (in [localImplementations]) again in the declarating package.)
+func typeOrMethod(obj types.Object) (types.Type, *types.Func) {
+ switch obj := obj.(type) {
+ case *types.TypeName:
+ return obj.Type(), nil
+ case *types.Func:
+ // For methods, use the receiver type, which may be anonymous.
+ if recv := obj.Signature().Recv(); recv != nil {
+ return recv.Type(), obj
+ }
+ }
+ return nil, nil
}
// offsetToLocation converts an offset-based position to a protocol.Location,
@@ -272,21 +310,9 @@ func offsetToLocation(ctx context.Context, snapshot *cache.Snapshot, filename st
return m.OffsetLocation(start, end)
}
-// implementsObj returns the object to query for implementations, which is a
-// type name or method.
-//
-// The returned Package is the narrowest package containing ppos, which is the
-// package using the resulting obj but not necessarily the declaring package.
-func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, ppos protocol.Position) (types.Object, *cache.Package, error) {
- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, uri)
- if err != nil {
- return nil, nil, err
- }
- pos, err := pgf.PositionPos(ppos)
- if err != nil {
- return nil, nil, err
- }
-
+// implementsObj returns the object to query for implementations,
+// which is a type name or method.
+func implementsObj(info *types.Info, file *ast.File, pos token.Pos) (types.Object, error) {
// This function inherits the limitation of its predecessor in
// requiring the selection to be an identifier (of a type or
// method). But there's no fundamental reason why one could
@@ -297,44 +323,46 @@ func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.D
// subexpression such as x.f().)
// TODO(adonovan): simplify: use objectsAt?
- path := pathEnclosingObjNode(pgf.File, pos)
+ path := pathEnclosingObjNode(file, pos)
if path == nil {
- return nil, nil, ErrNoIdentFound
+ return nil, ErrNoIdentFound
}
id, ok := path[0].(*ast.Ident)
if !ok {
- return nil, nil, ErrNoIdentFound
+ return nil, ErrNoIdentFound
}
// Is the object a type or method? Reject other kinds.
- obj := pkg.TypesInfo().Uses[id]
+ obj := info.Uses[id]
if obj == nil {
// Check uses first (unlike ObjectOf) so that T in
// struct{T} is treated as a reference to a type,
// not a declaration of a field.
- obj = pkg.TypesInfo().Defs[id]
+ obj = info.Defs[id]
}
switch obj := obj.(type) {
case *types.TypeName:
// ok
case *types.Func:
if obj.Signature().Recv() == nil {
- return nil, nil, fmt.Errorf("%s is a function, not a method", id.Name)
+ return nil, fmt.Errorf("%s is a function, not a method (query at 'func' token to find matching signatures)", id.Name)
}
case nil:
- return nil, nil, fmt.Errorf("%s denotes unknown object", id.Name)
+ return nil, fmt.Errorf("%s denotes unknown object", id.Name)
default:
// e.g. *types.Var -> "var".
kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types."))
- return nil, nil, fmt.Errorf("%s is a %s, not a type", id.Name, kind)
+ // TODO(adonovan): improve upon "nil is a nil, not a type".
+ return nil, fmt.Errorf("%s is a %s, not a type", id.Name, kind)
}
- return obj, pkg, nil
+ return obj, nil
}
// localImplementations searches within pkg for declarations of all
-// types that are assignable to/from the query type, and returns a new
-// unordered array of their locations.
+// supertypes (if rel contains Supertype) or subtypes (if rel contains
+// Subtype) of the type or method declared by id within the same
+// package, and returns a new unordered array of their locations.
//
// If method is non-nil, the function instead returns the location
// of each type's method (if any) of that ID.
@@ -343,50 +371,67 @@ func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.D
// function's results may include type declarations that are local to
// a function body. The global search index excludes such types
// because reliably naming such types is hard.)
-func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, queryType types.Type, method *types.Func) ([]protocol.Location, error) {
+//
+// Results are reported via the the yield function.
+func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, id *ast.Ident, rel methodsets.TypeRelation, yield implYieldFunc) error {
+ queryType, queryMethod := typeOrMethod(pkg.TypesInfo().Defs[id])
+ if queryType == nil {
+ return bug.Errorf("can't find corresponding symbol for %q in package %q", id.Name, pkg)
+ }
queryType = methodsets.EnsurePointer(queryType)
var msets typeutil.MethodSetCache
+ matches := func(candidateType types.Type) bool {
+ // Test the direction of the relation.
+ // The client may request either direction or both
+ // (e.g. when the client is References),
+ // and the Result reports each test independently;
+ // both tests succeed when comparing identical
+ // interface types.
+ var got methodsets.TypeRelation
+ if rel&methodsets.Supertype != 0 && implements(&msets, queryType, candidateType) {
+ got |= methodsets.Supertype
+ }
+ if rel&methodsets.Subtype != 0 && implements(&msets, candidateType, queryType) {
+ got |= methodsets.Subtype
+ }
+ return got != 0
+ }
+
// Scan through all type declarations in the syntax.
- var locs []protocol.Location
- var methodLocs []methodsets.Location
for _, pgf := range pkg.CompiledGoFiles() {
- ast.Inspect(pgf.File, func(n ast.Node) bool {
- spec, ok := n.(*ast.TypeSpec)
- if !ok {
- return true // not a type declaration
+ for cur := range pgf.Cursor.Preorder((*ast.TypeSpec)(nil)) {
+ spec := cur.Node().(*ast.TypeSpec)
+ if spec.Name == id {
+ continue // avoid self-comparison of query type
}
def := pkg.TypesInfo().Defs[spec.Name]
if def == nil {
- return true // "can't happen" for types
+ continue // "can't happen" for types
}
if def.(*types.TypeName).IsAlias() {
- return true // skip type aliases to avoid duplicate reporting
+ continue // skip type aliases to avoid duplicate reporting
}
candidateType := methodsets.EnsurePointer(def.Type())
-
- // The historical behavior enshrined by this
- // function rejects cases where both are
- // (nontrivial) interface types?
- // That seems like useful information; see #68641.
- // TODO(adonovan): UX: report I/I pairs too?
- // The same question appears in the global algorithm (methodsets).
- if !concreteImplementsIntf(&msets, candidateType, queryType) {
- return true // not assignable
+ if !matches(candidateType) {
+ continue
}
// Ignore types with empty method sets.
// (No point reporting that every type satisfies 'any'.)
mset := msets.MethodSet(candidateType)
if mset.Len() == 0 {
- return true
+ continue
}
- if method == nil {
+ isInterface := types.IsInterface(def.Type())
+
+ if queryMethod == nil {
// Found matching type.
- locs = append(locs, mustLocation(pgf, spec.Name))
- return true
+ loc := mustLocation(pgf, spec.Name)
+ yield(pkg.Metadata().PkgPath, spec.Name.Name, isInterface, loc)
+ continue
}
// Find corresponding method.
@@ -397,42 +442,39 @@ func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *ca
// but it's easier to walk the method set.
for i := 0; i < mset.Len(); i++ {
m := mset.At(i).Obj()
- if m.Id() == method.Id() {
+ if m.Pos() == id.Pos() {
+ continue // avoid self-comparison of query method
+ }
+ if m.Id() == queryMethod.Id() {
posn := safetoken.StartPosition(pkg.FileSet(), m.Pos())
- methodLocs = append(methodLocs, methodsets.Location{
- Filename: posn.Filename,
- Start: posn.Offset,
- End: posn.Offset + len(m.Name()),
- })
+ loc, err := offsetToLocation(ctx, snapshot, posn.Filename, posn.Offset, posn.Offset+len(m.Name()))
+ if err != nil {
+ return err
+ }
+ yield(pkg.Metadata().PkgPath, m.Name(), isInterface, loc)
break
}
}
- return true
- })
- }
-
- // Finally convert method positions to protocol form by reading the files.
- for _, mloc := range methodLocs {
- loc, err := offsetToLocation(ctx, snapshot, mloc.Filename, mloc.Start, mloc.End)
- if err != nil {
- return nil, err
}
- locs = append(locs, loc)
}
- // Special case: for types that satisfy error, report builtin.go (see #59527).
- if types.Implements(queryType, errorInterfaceType) {
+ // Special case: for types that satisfy error,
+ // report error in builtin.go (see #59527).
+ //
+ // (An inconsistency: we always report the type error
+ // even when the query was for the method error.Error.)
+ if matches(errorType) {
loc, err := errorLocation(ctx, snapshot)
if err != nil {
- return nil, err
+ return err
}
- locs = append(locs, loc)
+ yield("", "error", true, loc)
}
- return locs, nil
+ return nil
}
-var errorInterfaceType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface)
+var errorType = types.Universe.Lookup("error").Type()
// errorLocation returns the location of the 'error' type in builtin.go.
func errorLocation(ctx context.Context, snapshot *cache.Snapshot) (protocol.Location, error) {
@@ -452,28 +494,14 @@ func errorLocation(ctx context.Context, snapshot *cache.Snapshot) (protocol.Loca
return protocol.Location{}, fmt.Errorf("built-in error type not found")
}
-// concreteImplementsIntf reports whether x is an interface type
-// implemented by concrete type y, or vice versa.
-//
+// implements reports whether x implements y.
// If one or both types are generic, the result indicates whether the
// interface may be implemented under some instantiation.
-func concreteImplementsIntf(msets *typeutil.MethodSetCache, x, y types.Type) bool {
- xiface := types.IsInterface(x)
- yiface := types.IsInterface(y)
-
- // Make sure exactly one is an interface type.
- // TODO(adonovan): rescind this policy choice and report
- // I/I relationships. See CL 619719 + issue #68641.
- if xiface == yiface {
+func implements(msets *typeutil.MethodSetCache, x, y types.Type) bool {
+ if !types.IsInterface(y) {
return false
}
- // Rearrange if needed so x is the concrete type.
- if xiface {
- x, y = y, x
- }
- // Inv: y is an interface type.
-
// For each interface method of y, check that x has it too.
// It is not necessary to compute x's complete method set.
//
@@ -491,133 +519,327 @@ func concreteImplementsIntf(msets *typeutil.MethodSetCache, x, y types.Type) boo
if !ok {
return false // x lacks a method of y
}
- if !unify(xm.Signature(), ym.Signature()) {
+ if !unify(xm.Signature(), ym.Signature(), nil) {
return false // signatures do not match
}
}
return true // all methods found
}
-// unify reports whether the types of x and y match, allowing free
-// type parameters to stand for anything at all, without regard to
-// consistency of substitutions.
+// unify reports whether the types of x and y match.
//
-// TODO(adonovan): implement proper unification (#63982), finding the
-// most general unifier across all the interface methods.
+// If unifier is nil, unify reports only whether it succeeded.
+// If unifier is non-nil, it is populated with the values
+// of type parameters determined during a successful unification.
+// If unification succeeds without binding a type parameter, that parameter
+// will not be present in the map.
//
-// See also: unify in cache/methodsets/fingerprint, which uses a
-// similar ersatz unification approach on type fingerprints, for
-// the global index.
-func unify(x, y types.Type) bool {
- x = types.Unalias(x)
- y = types.Unalias(y)
-
- // For now, allow a type parameter to match anything,
- // without regard to consistency of substitutions.
- if is[*types.TypeParam](x) || is[*types.TypeParam](y) {
- return true
+// On entry, the unifier's contents are treated as the values of already-bound type
+// parameters, constraining the unification.
+//
+// For example, if unifier is an empty (not nil) map on entry, then the types
+//
+// func[T any](T, int)
+//
+// and
+//
+// func[U any](bool, U)
+//
+// will unify, with T=bool and U=int.
+// That is, the contents of unifier after unify returns will be
+//
+// {T: bool, U: int}
+//
+// where "T" is the type parameter T and "bool" is the basic type for bool.
+//
+// But if unifier is {T: int} is int on entry, then unification will fail, because T
+// does not unify with bool.
+//
+// Unify does not preserve aliases. For example, given the following:
+//
+// type String = string
+// type A[T] = T
+//
+// unification succeeds with T bound to string, not String.
+//
+// See also: unify in cache/methodsets/fingerprint, which implements
+// unification for type fingerprints, for the global index.
+//
+// BUG: literal interfaces are not handled properly. But this function is currently
+// used only for signatures, where such types are very rare.
+func unify(x, y types.Type, unifier map[*types.TypeParam]types.Type) bool {
+ // bindings[tp] is the binding for type parameter tp.
+ // Although type parameters are nominally bound to types, each bindings[tp]
+ // is a pointer to a type, so unbound variables that unify can share a binding.
+ bindings := map[*types.TypeParam]*types.Type{}
+
+ // Bindings is initialized with pointers to the provided types.
+ for tp, t := range unifier {
+ bindings[tp] = &t
}
- if reflect.TypeOf(x) != reflect.TypeOf(y) {
- return false // mismatched types
- }
-
- switch x := x.(type) {
- case *types.Array:
- y := y.(*types.Array)
- return x.Len() == y.Len() &&
- unify(x.Elem(), y.Elem())
-
- case *types.Basic:
- y := y.(*types.Basic)
- return x.Kind() == y.Kind()
-
- case *types.Chan:
- y := y.(*types.Chan)
- return x.Dir() == y.Dir() &&
- unify(x.Elem(), y.Elem())
-
- case *types.Interface:
- y := y.(*types.Interface)
- // TODO(adonovan): fix: for correctness, we must check
- // that both interfaces have the same set of methods
- // modulo type parameters, while avoiding the risk of
- // unbounded interface recursion.
- //
- // Since non-empty interface literals are vanishingly
- // rare in methods signatures, we ignore this for now.
- // If more precision is needed we could compare method
- // names and arities, still without full recursion.
- return x.NumMethods() == y.NumMethods()
-
- case *types.Map:
- y := y.(*types.Map)
- return unify(x.Key(), y.Key()) &&
- unify(x.Elem(), y.Elem())
-
- case *types.Named:
- y := y.(*types.Named)
- if x.Origin() != y.Origin() {
- return false // different named types
+ // bindingFor returns the *types.Type in bindings for tp if tp is not nil,
+ // creating one if needed.
+ bindingFor := func(tp *types.TypeParam) *types.Type {
+ if tp == nil {
+ return nil
}
- xtargs := x.TypeArgs()
- ytargs := y.TypeArgs()
- if xtargs.Len() != ytargs.Len() {
- return false // arity error (ill-typed)
+ b := bindings[tp]
+ if b == nil {
+ b = new(types.Type)
+ bindings[tp] = b
}
- for i := range xtargs.Len() {
- if !unify(xtargs.At(i), ytargs.At(i)) {
- return false // mismatched type args
+ return b
+ }
+
+ // bind sets b to t if b does not occur in t.
+ bind := func(b *types.Type, t types.Type) bool {
+ for tp := range typeParams(t) {
+ if b == bindings[tp] {
+ return false // failed "occurs" check
}
}
+ *b = t
return true
+ }
+
+ // uni performs the actual unification.
+ depth := 0
+ var uni func(x, y types.Type) bool
+ uni = func(x, y types.Type) bool {
+ // Panic if recursion gets too deep, to detect bugs before
+ // overflowing the stack.
+ depth++
+ defer func() { depth-- }()
+ if depth > 100 {
+ panic("unify: max depth exceeded")
+ }
- case *types.Pointer:
- y := y.(*types.Pointer)
- return unify(x.Elem(), y.Elem())
+ x = types.Unalias(x)
+ y = types.Unalias(y)
- case *types.Signature:
- y := y.(*types.Signature)
- return x.Variadic() == y.Variadic() &&
- unify(x.Params(), y.Params()) &&
- unify(x.Results(), y.Results())
+ tpx, _ := x.(*types.TypeParam)
+ tpy, _ := y.(*types.TypeParam)
+ if tpx != nil || tpy != nil {
+ // Identical type params unify.
+ if tpx == tpy {
+ return true
+ }
+ bx := bindingFor(tpx)
+ by := bindingFor(tpy)
- case *types.Slice:
- y := y.(*types.Slice)
- return unify(x.Elem(), y.Elem())
+ // If both args are type params and neither is bound, have them share a binding.
+ if bx != nil && by != nil && *bx == nil && *by == nil {
+ // Arbitrarily give y's binding to x.
+ bindings[tpx] = by
+ return true
+ }
+ // Treat param bindings like original args in what follows.
+ if bx != nil && *bx != nil {
+ x = *bx
+ }
+ if by != nil && *by != nil {
+ y = *by
+ }
+ // If the x param is unbound, bind it to y.
+ if bx != nil && *bx == nil {
+ return bind(bx, y)
+ }
+ // If the y param is unbound, bind it to x.
+ if by != nil && *by == nil {
+ return bind(by, x)
+ }
+ // Unify the binding of a bound parameter.
+ return uni(x, y)
+ }
- case *types.Struct:
- y := y.(*types.Struct)
- if x.NumFields() != y.NumFields() {
- return false
+ // Neither arg is a type param.
+
+ if reflect.TypeOf(x) != reflect.TypeOf(y) {
+ return false // mismatched types
}
- for i := range x.NumFields() {
- xf := x.Field(i)
- yf := y.Field(i)
- if xf.Embedded() != yf.Embedded() ||
- xf.Name() != yf.Name() ||
- x.Tag(i) != y.Tag(i) ||
- !xf.Exported() && xf.Pkg() != yf.Pkg() ||
- !unify(xf.Type(), yf.Type()) {
+
+ switch x := x.(type) {
+ case *types.Array:
+ y := y.(*types.Array)
+ return x.Len() == y.Len() &&
+ uni(x.Elem(), y.Elem())
+
+ case *types.Basic:
+ y := y.(*types.Basic)
+ return x.Kind() == y.Kind()
+
+ case *types.Chan:
+ y := y.(*types.Chan)
+ return x.Dir() == y.Dir() &&
+ uni(x.Elem(), y.Elem())
+
+ case *types.Interface:
+ y := y.(*types.Interface)
+ // TODO(adonovan,jba): fix: for correctness, we must check
+ // that both interfaces have the same set of methods
+ // modulo type parameters, while avoiding the risk of
+ // unbounded interface recursion.
+ //
+ // Since non-empty interface literals are vanishingly
+ // rare in methods signatures, we ignore this for now.
+ // If more precision is needed we could compare method
+ // names and arities, still without full recursion.
+ return x.NumMethods() == y.NumMethods()
+
+ case *types.Map:
+ y := y.(*types.Map)
+ return uni(x.Key(), y.Key()) &&
+ uni(x.Elem(), y.Elem())
+
+ case *types.Named:
+ y := y.(*types.Named)
+ if x.Origin() != y.Origin() {
+ return false // different named types
+ }
+ xtargs := x.TypeArgs()
+ ytargs := y.TypeArgs()
+ if xtargs.Len() != ytargs.Len() {
+ return false // arity error (ill-typed)
+ }
+ for i := range xtargs.Len() {
+ if !uni(xtargs.At(i), ytargs.At(i)) {
+ return false // mismatched type args
+ }
+ }
+ return true
+
+ case *types.Pointer:
+ y := y.(*types.Pointer)
+ return uni(x.Elem(), y.Elem())
+
+ case *types.Signature:
+ y := y.(*types.Signature)
+ return x.Variadic() == y.Variadic() &&
+ uni(x.Params(), y.Params()) &&
+ uni(x.Results(), y.Results())
+
+ case *types.Slice:
+ y := y.(*types.Slice)
+ return uni(x.Elem(), y.Elem())
+
+ case *types.Struct:
+ y := y.(*types.Struct)
+ if x.NumFields() != y.NumFields() {
return false
}
+ for i := range x.NumFields() {
+ xf := x.Field(i)
+ yf := y.Field(i)
+ if xf.Embedded() != yf.Embedded() ||
+ xf.Name() != yf.Name() ||
+ x.Tag(i) != y.Tag(i) ||
+ !xf.Exported() && xf.Pkg() != yf.Pkg() ||
+ !uni(xf.Type(), yf.Type()) {
+ return false
+ }
+ }
+ return true
+
+ case *types.Tuple:
+ y := y.(*types.Tuple)
+ if x.Len() != y.Len() {
+ return false
+ }
+ for i := range x.Len() {
+ if !uni(x.At(i).Type(), y.At(i).Type()) {
+ return false
+ }
+ }
+ return true
+
+ default: // incl. *Union, *TypeParam
+ panic(fmt.Sprintf("unexpected Type %#v", x))
}
- return true
+ }
- case *types.Tuple:
- y := y.(*types.Tuple)
- if x.Len() != y.Len() {
- return false
+ if !uni(x, y) {
+ clear(unifier)
+ return false
+ }
+
+ // Populate the input map with the resulting types.
+ if unifier != nil {
+ for tparam, tptr := range bindings {
+ unifier[tparam] = *tptr
}
- for i := range x.Len() {
- if !unify(x.At(i).Type(), y.At(i).Type()) {
- return false
+ }
+ return true
+}
+
+// typeParams yields all the free type parameters within t that are relevant for
+// unification.
+func typeParams(t types.Type) iter.Seq[*types.TypeParam] {
+
+ return func(yield func(*types.TypeParam) bool) {
+ seen := map[*types.TypeParam]bool{} // yield each type param only once
+
+ // tps(t) yields each TypeParam in t and returns false to stop.
+ var tps func(types.Type) bool
+ tps = func(t types.Type) bool {
+ t = types.Unalias(t)
+
+ switch t := t.(type) {
+ case *types.TypeParam:
+ if seen[t] {
+ return true
+ }
+ seen[t] = true
+ return yield(t)
+
+ case *types.Basic:
+ return true
+
+ case *types.Array:
+ return tps(t.Elem())
+
+ case *types.Chan:
+ return tps(t.Elem())
+
+ case *types.Interface:
+ // TODO(jba): implement.
+ return true
+
+ case *types.Map:
+ return tps(t.Key()) && tps(t.Elem())
+
+ case *types.Named:
+ if t.Origin() == t {
+ // generic type: look at type params
+ return moreiters.Every(t.TypeParams().TypeParams(),
+ func(tp *types.TypeParam) bool { return tps(tp) })
+ }
+ // instantiated type: look at type args
+ return moreiters.Every(t.TypeArgs().Types(), tps)
+
+ case *types.Pointer:
+ return tps(t.Elem())
+
+ case *types.Signature:
+ return tps(t.Params()) && tps(t.Results())
+
+ case *types.Slice:
+ return tps(t.Elem())
+
+ case *types.Struct:
+ return moreiters.Every(t.Fields(),
+ func(v *types.Var) bool { return tps(v.Type()) })
+
+ case *types.Tuple:
+ return moreiters.Every(t.Variables(),
+ func(v *types.Var) bool { return tps(v.Type()) })
+
+ default: // incl. *Union
+ panic(fmt.Sprintf("unexpected Type %#v", t))
}
}
- return true
- default: // incl. *Union, *TypeParam
- panic(fmt.Sprintf("unexpected Type %#v", x))
+ tps(t)
}
}
@@ -659,6 +881,7 @@ func pathEnclosingObjNode(f *ast.File, pos token.Pos) []ast.Node {
// handled this by calling astutil.PathEnclosingInterval twice,
// once for "pos" and once for "pos-1".
found = n.Pos() <= pos && pos <= n.End()
+
case *ast.ImportSpec:
if n.Path.Pos() <= pos && pos < n.Path.End() {
found = true
@@ -668,6 +891,7 @@ func pathEnclosingObjNode(f *ast.File, pos token.Pos) []ast.Node {
path = append(path, n.Name)
}
}
+
case *ast.StarExpr:
// Follow star expressions to the inner identifier.
if pos == n.Star {
@@ -683,9 +907,223 @@ func pathEnclosingObjNode(f *ast.File, pos token.Pos) []ast.Node {
}
// Reverse path so leaf is first element.
- for i := 0; i < len(path)/2; i++ {
- path[i], path[len(path)-1-i] = path[len(path)-1-i], path[i]
+ slices.Reverse(path)
+ return path
+}
+
+// --- Implementations based on signature types --
+
+// implFuncs finds Implementations based on func types.
+//
+// Just as an interface type abstracts a set of concrete methods, a
+// function type abstracts a set of concrete functions. Gopls provides
+// analogous operations for navigating from abstract to concrete and
+// back in the domain of function types.
+//
+// A single type (for example http.HandlerFunc) can have both an
+// underlying type of function (types.Signature) and have methods that
+// cause it to implement an interface. To avoid a confusing user
+// interface we want to separate the two operations so that the user
+// can unambiguously specify the query they want.
+//
+// So, whereas Implementations queries on interface types are usually
+// keyed by an identifier of a named type, Implementations queries on
+// function types are keyed by the "func" keyword, or by the "(" of a
+// call expression. The query relates two sets of locations:
+//
+// 1. the "func" token of each function declaration (FuncDecl or
+// FuncLit). These are analogous to declarations of concrete
+// methods.
+//
+// 2. uses of abstract functions:
+//
+// (a) the "func" token of each FuncType that is not part of
+// Func{Decl,Lit}. These are analogous to interface{...} types.
+//
+// (b) the "(" paren of each dynamic call on a value of an
+// abstract function type. These are analogous to references to
+// interface method names, but no names are involved, which has
+// historically made them hard to search for.
+//
+// An Implementations query on a location in set 1 returns set 2,
+// and vice versa.
+//
+// implFuncs returns errNotHandled to indicate that we should try the
+// regular method-sets algorithm.
+func implFuncs(pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) {
+ curSel, ok := pgf.Cursor.FindByPos(pos, pos)
+ if !ok {
+ return nil, fmt.Errorf("no code selected")
}
- return path
+ info := pkg.TypesInfo()
+ if info.Types == nil || info.Defs == nil || info.Uses == nil {
+ panic("one of info.Types, .Defs or .Uses is nil")
+ }
+
+ // Find innermost enclosing FuncType or CallExpr.
+ //
+ // We are looking for specific tokens (FuncType.Func and
+ // CallExpr.Lparen), but FindPos prefers an adjoining
+ // subexpression: given f(x) without additional spaces between
+ // tokens, FindPos always returns either f or x, never the
+ // CallExpr itself. Thus we must ascend the tree.
+ //
+ // Another subtlety: due to an edge case in go/ast, FindPos at
+ // FuncDecl.Type.Func does not return FuncDecl.Type, only the
+ // FuncDecl, because the orders of tree positions and tokens
+ // are inconsistent. Consequently, the ancestors for a "func"
+ // token of Func{Lit,Decl} do not include FuncType, hence the
+ // explicit cases below.
+ for cur := range curSel.Enclosing(
+ (*ast.FuncDecl)(nil),
+ (*ast.FuncLit)(nil),
+ (*ast.FuncType)(nil),
+ (*ast.CallExpr)(nil),
+ ) {
+ switch n := cur.Node().(type) {
+ case *ast.FuncDecl, *ast.FuncLit:
+ if inToken(n.Pos(), "func", pos) {
+ // Case 1: concrete function declaration.
+ // Report uses of corresponding function types.
+ switch n := n.(type) {
+ case *ast.FuncDecl:
+ return funcUses(pkg, info.Defs[n.Name].Type())
+ case *ast.FuncLit:
+ return funcUses(pkg, info.TypeOf(n.Type))
+ }
+ }
+
+ case *ast.FuncType:
+ if n.Func.IsValid() && inToken(n.Func, "func", pos) && !beneathFuncDef(cur) {
+ // Case 2a: function type.
+ // Report declarations of corresponding concrete functions.
+ return funcDefs(pkg, info.TypeOf(n))
+ }
+
+ case *ast.CallExpr:
+ if inToken(n.Lparen, "(", pos) {
+ t := dynamicFuncCallType(info, n)
+ if t == nil {
+ return nil, fmt.Errorf("not a dynamic function call")
+ }
+ // Case 2b: dynamic call of function value.
+ // Report declarations of corresponding concrete functions.
+ return funcDefs(pkg, t)
+ }
+ }
+ }
+
+ // It's probably a query of a named type or method.
+ // Fall back to the method-sets computation.
+ return nil, errNotHandled
+}
+
+var errNotHandled = errors.New("not handled")
+
+// funcUses returns all locations in the workspace that are dynamic
+// uses of the specified function type.
+func funcUses(pkg *cache.Package, t types.Type) ([]protocol.Location, error) {
+ var locs []protocol.Location
+
+ // local search
+ for _, pgf := range pkg.CompiledGoFiles() {
+ for cur := range pgf.Cursor.Preorder((*ast.CallExpr)(nil), (*ast.FuncType)(nil)) {
+ var pos, end token.Pos
+ var ftyp types.Type
+ switch n := cur.Node().(type) {
+ case *ast.CallExpr:
+ ftyp = dynamicFuncCallType(pkg.TypesInfo(), n)
+ pos, end = n.Lparen, n.Lparen+token.Pos(len("("))
+
+ case *ast.FuncType:
+ if !beneathFuncDef(cur) {
+ // func type (not def)
+ ftyp = pkg.TypesInfo().TypeOf(n)
+ pos, end = n.Func, n.Func+token.Pos(len("func"))
+ }
+ }
+ if ftyp == nil {
+ continue // missing type information
+ }
+ if unify(t, ftyp, nil) {
+ loc, err := pgf.PosLocation(pos, end)
+ if err != nil {
+ return nil, err
+ }
+ locs = append(locs, loc)
+ }
+ }
+ }
+
+ // TODO(adonovan): implement global search
+
+ return locs, nil
+}
+
+// funcDefs returns all locations in the workspace that define
+// functions of the specified type.
+func funcDefs(pkg *cache.Package, t types.Type) ([]protocol.Location, error) {
+ var locs []protocol.Location
+
+ // local search
+ for _, pgf := range pkg.CompiledGoFiles() {
+ for curFn := range pgf.Cursor.Preorder((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) {
+ fn := curFn.Node()
+ var ftyp types.Type
+ switch fn := fn.(type) {
+ case *ast.FuncDecl:
+ ftyp = pkg.TypesInfo().Defs[fn.Name].Type()
+ case *ast.FuncLit:
+ ftyp = pkg.TypesInfo().TypeOf(fn)
+ }
+ if ftyp == nil {
+ continue // missing type information
+ }
+ if unify(t, ftyp, nil) {
+ pos := fn.Pos()
+ loc, err := pgf.PosLocation(pos, pos+token.Pos(len("func")))
+ if err != nil {
+ return nil, err
+ }
+ locs = append(locs, loc)
+ }
+ }
+ }
+
+ // TODO(adonovan): implement global search, by analogy with
+ // methodsets algorithm.
+ //
+ // One optimization: if any signature type has free package
+ // names, look for matches only in packages among the rdeps of
+ // those packages.
+
+ return locs, nil
+}
+
+// beneathFuncDef reports whether the specified FuncType cursor is a
+// child of Func{Decl,Lit}.
+func beneathFuncDef(cur cursor.Cursor) bool {
+ switch ek, _ := cur.ParentEdge(); ek {
+ case edge.FuncDecl_Type, edge.FuncLit_Type:
+ return true
+ }
+ return false
+}
+
+// dynamicFuncCallType reports whether call is a dynamic (non-method) function call.
+// If so, it returns the function type, otherwise nil.
+//
+// Tested via ../test/marker/testdata/implementation/signature.txt.
+func dynamicFuncCallType(info *types.Info, call *ast.CallExpr) types.Type {
+ if typesinternal.ClassifyCall(info, call) == typesinternal.CallDynamic {
+ return info.Types[call.Fun].Type.Underlying()
+ }
+ return nil
+}
+
+// inToken reports whether pos is within the token of
+// the specified position and string.
+func inToken(tokPos token.Pos, tokStr string, pos token.Pos) bool {
+ return tokPos <= pos && pos <= tokPos+token.Pos(len(tokStr))
}
diff --git a/gopls/internal/golang/implementation_test.go b/gopls/internal/golang/implementation_test.go
new file mode 100644
index 00000000000..b7253bb8bf7
--- /dev/null
+++ b/gopls/internal/golang/implementation_test.go
@@ -0,0 +1,303 @@
+// Copyright 2025 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 golang
+
+import (
+ "go/types"
+ "maps"
+ "testing"
+
+ "golang.org/x/tools/internal/testfiles"
+ "golang.org/x/tools/txtar"
+)
+
+func TestUnify(t *testing.T) {
+ // Most cases from TestMatches in gopls/internal/util/fingerprint/fingerprint_test.go.
+ const src = `
+-- go.mod --
+module example.com
+go 1.24
+
+-- a/a.go --
+package a
+
+type Int = int
+type String = string
+
+// Eq.Equal matches casefold.Equal.
+type Eq[T any] interface { Equal(T, T) bool }
+type casefold struct{}
+func (casefold) Equal(x, y string) bool
+
+// A matches AString.
+type A[T any] = struct { x T }
+type AString = struct { x string }
+
+// B matches anything!
+type B[T any] = T
+
+func C1[T any](int, T, ...string) T { panic(0) }
+func C2[U any](int, int, ...U) bool { panic(0) }
+func C3(int, bool, ...string) rune
+func C4(int, bool, ...string)
+func C5(int, float64, bool, string) bool
+func C6(int, bool, ...string) bool
+
+func DAny[T any](Named[T]) { panic(0) }
+func DString(Named[string])
+func DInt(Named[int])
+
+type Named[T any] struct { x T }
+
+func E1(byte) rune
+func E2(uint8) int32
+func E3(int8) uint32
+
+// generic vs. generic
+func F1[T any](T) { panic(0) }
+func F2[T any](*T) { panic(0) }
+func F3[T any](T, T) { panic(0) }
+func F4[U any](U, *U) {panic(0) }
+func F4a[U any](U, Named[U]) {panic(0) }
+func F5[T, U any](T, U, U) { panic(0) }
+func F6[T any](T, int, T) { panic(0) }
+func F7[T any](bool, T, T) { panic(0) }
+func F8[V any](*V, int, int) { panic(0) }
+func F9[V any](V, *V, V) { panic(0) }
+`
+ type tmap = map[*types.TypeParam]types.Type
+
+ var (
+ boolType = types.Typ[types.Bool]
+ intType = types.Typ[types.Int]
+ stringType = types.Typ[types.String]
+ )
+
+ pkg := testfiles.LoadPackages(t, txtar.Parse([]byte(src)), "./a")[0]
+ scope := pkg.Types.Scope()
+
+ tparam := func(name string, index int) *types.TypeParam {
+ obj := scope.Lookup(name)
+ var tps *types.TypeParamList
+ switch obj := obj.(type) {
+ case *types.Func:
+ tps = obj.Signature().TypeParams()
+ case *types.TypeName:
+ if n, ok := obj.Type().(*types.Named); ok {
+ tps = n.TypeParams()
+ } else {
+ tps = obj.Type().(*types.Alias).TypeParams()
+ }
+ default:
+ t.Fatalf("unsupported object of type %T", obj)
+ }
+ return tps.At(index)
+ }
+
+ for _, test := range []struct {
+ x, y string // the symbols in the above source code whose types to unify
+ method string // optional field or method
+ params tmap // initial values of type params
+ want bool // success or failure
+ wantParams tmap // expected output
+ }{
+ {
+ // In Eq[T], T is bound to string.
+ x: "Eq",
+ y: "casefold",
+ method: "Equal",
+ want: true,
+ wantParams: tmap{tparam("Eq", 0): stringType},
+ },
+ {
+ // If we unify A[T] and A[string], T should be bound to string.
+ x: "A",
+ y: "AString",
+ want: true,
+ wantParams: tmap{tparam("A", 0): stringType},
+ },
+ {x: "A", y: "Eq", want: false}, // completely unrelated
+ {
+ x: "B",
+ y: "String",
+ want: true,
+ wantParams: tmap{tparam("B", 0): stringType},
+ },
+ {
+ x: "B",
+ y: "Int",
+ want: true,
+ wantParams: tmap{tparam("B", 0): intType},
+ },
+ {
+ x: "B",
+ y: "A",
+ want: true,
+ // B's T is bound to A's struct { x T }
+ wantParams: tmap{tparam("B", 0): scope.Lookup("A").Type().Underlying()},
+ },
+ {
+ // C1's U unifies with C6's bool.
+ x: "C1",
+ y: "C6",
+ wantParams: tmap{tparam("C1", 0): boolType},
+ want: true,
+ },
+ // C1 fails to unify with C2 because C1's T must be bound to both int and bool.
+ {x: "C1", y: "C2", want: false},
+ // The remaining "C" cases fail for less interesting reasons, usually different numbers
+ // or types of parameters or results.
+ {x: "C1", y: "C3", want: false},
+ {x: "C1", y: "C4", want: false},
+ {x: "C1", y: "C5", want: false},
+ {x: "C2", y: "C3", want: false},
+ {x: "C2", y: "C4", want: false},
+ {x: "C3", y: "C4", want: false},
+ {
+ x: "DAny",
+ y: "DString",
+ want: true,
+ wantParams: tmap{tparam("DAny", 0): stringType},
+ },
+ {x: "DString", y: "DInt", want: false}, // different instantiations of Named
+ {x: "E1", y: "E2", want: true}, // byte and rune are just aliases
+ {x: "E2", y: "E3", want: false},
+
+ // The following tests cover all of the type param cases of unify.
+ {
+ // F1[*int] = F2[int], for example
+ // F1's T is bound to a pointer to F2's T.
+ x: "F1",
+ // F2's T is unbound: any instantiation works.
+ y: "F2",
+ want: true,
+ wantParams: tmap{tparam("F1", 0): types.NewPointer(tparam("F2", 0))},
+ },
+ {x: "F3", y: "F4", want: false}, // would require U identical to *U, prevented by occur check
+ {x: "F3", y: "F4a", want: false}, // occur check through Named[T]
+ {
+ x: "F5",
+ y: "F6",
+ want: true,
+ wantParams: tmap{
+ tparam("F5", 0): intType,
+ tparam("F5", 1): intType,
+ tparam("F6", 0): intType,
+ },
+ },
+ {x: "F6", y: "F7", want: false}, // both are bound
+ {
+ x: "F5",
+ y: "F6",
+ params: tmap{tparam("F6", 0): intType}, // consistent with the result
+ want: true,
+ wantParams: tmap{
+ tparam("F5", 0): intType,
+ tparam("F5", 1): intType,
+ tparam("F6", 0): intType,
+ },
+ },
+ {
+ x: "F5",
+ y: "F6",
+ params: tmap{tparam("F6", 0): boolType}, // not consistent
+ want: false,
+ },
+ {x: "F6", y: "F7", want: false}, // both are bound
+ {
+ // T=*V, U=int, V=int
+ x: "F5",
+ y: "F8",
+ want: true,
+ wantParams: tmap{
+ tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
+ tparam("F5", 1): intType,
+ },
+ },
+ {
+ // T=*V, U=int, V=int
+ // Partial initial information is fine, as long as it's consistent.
+ x: "F5",
+ y: "F8",
+ want: true,
+ params: tmap{tparam("F5", 1): intType},
+ wantParams: tmap{
+ tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
+ tparam("F5", 1): intType,
+ },
+ },
+ {
+ // T=*V, U=int, V=int
+ // Partial initial information is fine, as long as it's consistent.
+ x: "F5",
+ y: "F8",
+ want: true,
+ params: tmap{tparam("F5", 0): types.NewPointer(tparam("F8", 0))},
+ wantParams: tmap{
+ tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
+ tparam("F5", 1): intType,
+ },
+ },
+ {x: "F5", y: "F9", want: false}, // T is unbound, V is bound, and T occurs in V
+ {
+ // T bound to Named[T']
+ x: "F1",
+ y: "DAny",
+ want: true,
+ wantParams: tmap{
+ tparam("F1", 0): scope.Lookup("DAny").(*types.Func).Signature().Params().At(0).Type()},
+ },
+ } {
+
+ lookup := func(name string) types.Type {
+ obj := scope.Lookup(name)
+ if obj == nil {
+ t.Fatalf("Lookup %s failed", name)
+ }
+ if test.method != "" {
+ obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, pkg.Types, test.method)
+ if obj == nil {
+ t.Fatalf("Lookup %s.%s failed", name, test.method)
+ }
+ }
+ return obj.Type()
+ }
+
+ check := func(a, b string, want, compareParams bool) {
+ t.Helper()
+
+ ta := lookup(a)
+ tb := lookup(b)
+
+ var gotParams tmap
+ if test.params == nil {
+ // Get the unifier even if there are no input params.
+ gotParams = tmap{}
+ } else {
+ gotParams = maps.Clone(test.params)
+ }
+ got := unify(ta, tb, gotParams)
+ if got != want {
+ t.Errorf("a=%s b=%s method=%s: unify returned %t for these inputs:\n- %s\n- %s",
+ a, b, test.method, got, ta, tb)
+ return
+ }
+ if !compareParams {
+ return
+ }
+ if !maps.EqualFunc(gotParams, test.wantParams, types.Identical) {
+ t.Errorf("x=%s y=%s method=%s: params: got %v, want %v",
+ a, b, test.method, gotParams, test.wantParams)
+ }
+ }
+
+ check(test.x, test.y, test.want, true)
+ // unify is symmetric
+ check(test.y, test.x, test.want, true)
+ // unify is reflexive
+ check(test.x, test.x, true, false)
+ check(test.y, test.y, true, false)
+ }
+}
diff --git a/gopls/internal/golang/inlay_hint.go b/gopls/internal/golang/inlay_hint.go
index bc85745cb0b..617231a4f8c 100644
--- a/gopls/internal/golang/inlay_hint.go
+++ b/gopls/internal/golang/inlay_hint.go
@@ -14,9 +14,11 @@ import (
"strings"
"golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
+ "golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
@@ -47,7 +49,7 @@ func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pR
}
info := pkg.TypesInfo()
- q := typesinternal.FileQualifier(pgf.File, pkg.Types())
+ qual := typesinternal.FileQualifier(pgf.File, pkg.Types())
// Set the range to the full file if the range is not valid.
start, end := pgf.File.FileStart, pgf.File.FileEnd
@@ -63,20 +65,16 @@ func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pR
}
var hints []protocol.InlayHint
- ast.Inspect(pgf.File, func(node ast.Node) bool {
- // If not in range, we can stop looking.
- if node == nil || node.End() < start || node.Pos() > end {
- return false
- }
+ if curSubrange, ok := pgf.Cursor.FindByPos(start, end); ok {
+ add := func(hint protocol.InlayHint) { hints = append(hints, hint) }
for _, fn := range enabledHints {
- hints = append(hints, fn(node, pgf.Mapper, pgf.Tok, info, &q)...)
+ fn(info, pgf, qual, curSubrange, add)
}
- return true
- })
+ }
return hints, nil
}
-type inlayHintFunc func(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint
+type inlayHintFunc func(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint))
var allInlayHints = map[settings.InlayHint]inlayHintFunc{
settings.AssignVariableTypes: assignVariableTypes,
@@ -88,259 +86,246 @@ var allInlayHints = map[settings.InlayHint]inlayHintFunc{
settings.FunctionTypeParameters: funcTypeParams,
}
-func parameterNames(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
- callExpr, ok := node.(*ast.CallExpr)
- if !ok {
- return nil
- }
- t := info.TypeOf(callExpr.Fun)
- if t == nil {
- return nil
- }
- signature, ok := typeparams.CoreType(t).(*types.Signature)
- if !ok {
- return nil
+func parameterNames(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
+ for curCall := range cur.Preorder((*ast.CallExpr)(nil)) {
+ callExpr := curCall.Node().(*ast.CallExpr)
+ t := info.TypeOf(callExpr.Fun)
+ if t == nil {
+ continue
+ }
+ signature, ok := typeparams.CoreType(t).(*types.Signature)
+ if !ok {
+ continue
+ }
+
+ for i, v := range callExpr.Args {
+ start, err := pgf.PosPosition(v.Pos())
+ if err != nil {
+ continue
+ }
+ params := signature.Params()
+ // When a function has variadic params, we skip args after
+ // params.Len().
+ if i > params.Len()-1 {
+ break
+ }
+ param := params.At(i)
+ // param.Name is empty for built-ins like append
+ if param.Name() == "" {
+ continue
+ }
+ // Skip the parameter name hint if the arg matches
+ // the parameter name.
+ if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() {
+ continue
+ }
+
+ label := param.Name()
+ if signature.Variadic() && i == params.Len()-1 {
+ label = label + "..."
+ }
+ add(protocol.InlayHint{
+ Position: start,
+ Label: buildLabel(label + ":"),
+ Kind: protocol.Parameter,
+ PaddingRight: true,
+ })
+ }
}
+}
- var hints []protocol.InlayHint
- for i, v := range callExpr.Args {
- start, err := m.PosPosition(tf, v.Pos())
- if err != nil {
+func funcTypeParams(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
+ for curCall := range cur.Preorder((*ast.CallExpr)(nil)) {
+ call := curCall.Node().(*ast.CallExpr)
+ id, ok := call.Fun.(*ast.Ident)
+ if !ok {
continue
}
- params := signature.Params()
- // When a function has variadic params, we skip args after
- // params.Len().
- if i > params.Len()-1 {
- break
- }
- param := params.At(i)
- // param.Name is empty for built-ins like append
- if param.Name() == "" {
+ inst := info.Instances[id]
+ if inst.TypeArgs == nil {
continue
}
- // Skip the parameter name hint if the arg matches
- // the parameter name.
- if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() {
+ start, err := pgf.PosPosition(id.End())
+ if err != nil {
continue
}
-
- label := param.Name()
- if signature.Variadic() && i == params.Len()-1 {
- label = label + "..."
+ var args []string
+ for i := 0; i < inst.TypeArgs.Len(); i++ {
+ args = append(args, inst.TypeArgs.At(i).String())
+ }
+ if len(args) == 0 {
+ continue
}
- hints = append(hints, protocol.InlayHint{
- Position: start,
- Label: buildLabel(label + ":"),
- Kind: protocol.Parameter,
- PaddingRight: true,
+ add(protocol.InlayHint{
+ Position: start,
+ Label: buildLabel("[" + strings.Join(args, ", ") + "]"),
+ Kind: protocol.Type,
})
}
- return hints
}
-func funcTypeParams(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
- ce, ok := node.(*ast.CallExpr)
- if !ok {
- return nil
- }
- id, ok := ce.Fun.(*ast.Ident)
- if !ok {
- return nil
- }
- inst := info.Instances[id]
- if inst.TypeArgs == nil {
- return nil
- }
- start, err := m.PosPosition(tf, id.End())
- if err != nil {
- return nil
- }
- var args []string
- for i := 0; i < inst.TypeArgs.Len(); i++ {
- args = append(args, inst.TypeArgs.At(i).String())
- }
- if len(args) == 0 {
- return nil
- }
- return []protocol.InlayHint{{
- Position: start,
- Label: buildLabel("[" + strings.Join(args, ", ") + "]"),
- Kind: protocol.Type,
- }}
-}
-
-func assignVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
- stmt, ok := node.(*ast.AssignStmt)
- if !ok || stmt.Tok != token.DEFINE {
- return nil
- }
-
- var hints []protocol.InlayHint
- for _, v := range stmt.Lhs {
- if h := variableType(v, m, tf, info, q); h != nil {
- hints = append(hints, *h)
+func assignVariableTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
+ for curAssign := range cur.Preorder((*ast.AssignStmt)(nil)) {
+ stmt := curAssign.Node().(*ast.AssignStmt)
+ if stmt.Tok != token.DEFINE {
+ continue
+ }
+ for _, v := range stmt.Lhs {
+ variableType(info, pgf, qual, v, add)
}
}
- return hints
}
-func rangeVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
- rStmt, ok := node.(*ast.RangeStmt)
- if !ok {
- return nil
- }
- var hints []protocol.InlayHint
- if h := variableType(rStmt.Key, m, tf, info, q); h != nil {
- hints = append(hints, *h)
- }
- if h := variableType(rStmt.Value, m, tf, info, q); h != nil {
- hints = append(hints, *h)
+func rangeVariableTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
+ for curRange := range cur.Preorder((*ast.RangeStmt)(nil)) {
+ rStmt := curRange.Node().(*ast.RangeStmt)
+ variableType(info, pgf, qual, rStmt.Key, add)
+ variableType(info, pgf, qual, rStmt.Value, add)
}
- return hints
}
-func variableType(e ast.Expr, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint {
+func variableType(info *types.Info, pgf *parsego.File, qual types.Qualifier, e ast.Expr, add func(protocol.InlayHint)) {
typ := info.TypeOf(e)
if typ == nil {
- return nil
+ return
}
- end, err := m.PosPosition(tf, e.End())
+ end, err := pgf.PosPosition(e.End())
if err != nil {
- return nil
+ return
}
- return &protocol.InlayHint{
+ add(protocol.InlayHint{
Position: end,
- Label: buildLabel(types.TypeString(typ, *q)),
+ Label: buildLabel(types.TypeString(typ, qual)),
Kind: protocol.Type,
PaddingLeft: true,
- }
+ })
}
-func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
- genDecl, ok := node.(*ast.GenDecl)
- if !ok || genDecl.Tok != token.CONST {
- return nil
- }
-
- var hints []protocol.InlayHint
- for _, v := range genDecl.Specs {
- spec, ok := v.(*ast.ValueSpec)
- if !ok {
- continue
- }
- end, err := m.PosPosition(tf, v.End())
- if err != nil {
+func constantValues(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
+ for curDecl := range cur.Preorder((*ast.GenDecl)(nil)) {
+ genDecl := curDecl.Node().(*ast.GenDecl)
+ if genDecl.Tok != token.CONST {
continue
}
- // Show hints when values are missing or at least one value is not
- // a basic literal.
- showHints := len(spec.Values) == 0
- checkValues := len(spec.Names) == len(spec.Values)
- var values []string
- for i, w := range spec.Names {
- obj, ok := info.ObjectOf(w).(*types.Const)
- if !ok || obj.Val().Kind() == constant.Unknown {
- return nil
+
+ for _, v := range genDecl.Specs {
+ spec, ok := v.(*ast.ValueSpec)
+ if !ok {
+ continue
}
- if checkValues {
- switch spec.Values[i].(type) {
- case *ast.BadExpr:
- return nil
- case *ast.BasicLit:
- default:
- if obj.Val().Kind() != constant.Bool {
- showHints = true
+ end, err := pgf.PosPosition(v.End())
+ if err != nil {
+ continue
+ }
+ // Show hints when values are missing or at least one value is not
+ // a basic literal.
+ showHints := len(spec.Values) == 0
+ checkValues := len(spec.Names) == len(spec.Values)
+ var values []string
+ for i, w := range spec.Names {
+ obj, ok := info.ObjectOf(w).(*types.Const)
+ if !ok || obj.Val().Kind() == constant.Unknown {
+ continue
+ }
+ if checkValues {
+ switch spec.Values[i].(type) {
+ case *ast.BadExpr:
+ continue
+ case *ast.BasicLit:
+ default:
+ if obj.Val().Kind() != constant.Bool {
+ showHints = true
+ }
}
}
+ values = append(values, fmt.Sprintf("%v", obj.Val()))
}
- values = append(values, fmt.Sprintf("%v", obj.Val()))
- }
- if !showHints || len(values) == 0 {
- continue
+ if !showHints || len(values) == 0 {
+ continue
+ }
+ add(protocol.InlayHint{
+ Position: end,
+ Label: buildLabel("= " + strings.Join(values, ", ")),
+ PaddingLeft: true,
+ })
}
- hints = append(hints, protocol.InlayHint{
- Position: end,
- Label: buildLabel("= " + strings.Join(values, ", ")),
- PaddingLeft: true,
- })
}
- return hints
}
-func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
- compLit, ok := node.(*ast.CompositeLit)
- if !ok {
- return nil
- }
- typ := info.TypeOf(compLit)
- if typ == nil {
- return nil
- }
- typ = typesinternal.Unpointer(typ)
- strct, ok := typeparams.CoreType(typ).(*types.Struct)
- if !ok {
- return nil
- }
+func compositeLiteralFields(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
+ for curCompLit := range cur.Preorder((*ast.CompositeLit)(nil)) {
+ compLit, ok := curCompLit.Node().(*ast.CompositeLit)
+ if !ok {
+ continue
+ }
+ typ := info.TypeOf(compLit)
+ if typ == nil {
+ continue
+ }
+ typ = typesinternal.Unpointer(typ)
+ strct, ok := typeparams.CoreType(typ).(*types.Struct)
+ if !ok {
+ continue
+ }
- var hints []protocol.InlayHint
- var allEdits []protocol.TextEdit
- for i, v := range compLit.Elts {
- if _, ok := v.(*ast.KeyValueExpr); !ok {
- start, err := m.PosPosition(tf, v.Pos())
- if err != nil {
- continue
- }
- if i > strct.NumFields()-1 {
- break
+ var hints []protocol.InlayHint
+ var allEdits []protocol.TextEdit
+ for i, v := range compLit.Elts {
+ if _, ok := v.(*ast.KeyValueExpr); !ok {
+ start, err := pgf.PosPosition(v.Pos())
+ if err != nil {
+ continue
+ }
+ if i > strct.NumFields()-1 {
+ break
+ }
+ hints = append(hints, protocol.InlayHint{
+ Position: start,
+ Label: buildLabel(strct.Field(i).Name() + ":"),
+ Kind: protocol.Parameter,
+ PaddingRight: true,
+ })
+ allEdits = append(allEdits, protocol.TextEdit{
+ Range: protocol.Range{Start: start, End: start},
+ NewText: strct.Field(i).Name() + ": ",
+ })
}
- hints = append(hints, protocol.InlayHint{
- Position: start,
- Label: buildLabel(strct.Field(i).Name() + ":"),
- Kind: protocol.Parameter,
- PaddingRight: true,
- })
- allEdits = append(allEdits, protocol.TextEdit{
- Range: protocol.Range{Start: start, End: start},
- NewText: strct.Field(i).Name() + ": ",
- })
+ }
+ // It is not allowed to have a mix of keyed and unkeyed fields, so
+ // have the text edits add keys to all fields.
+ for i := range hints {
+ hints[i].TextEdits = allEdits
+ add(hints[i])
}
}
- // It is not allowed to have a mix of keyed and unkeyed fields, so
- // have the text edits add keys to all fields.
- for i := range hints {
- hints[i].TextEdits = allEdits
- }
- return hints
}
-func compositeLiteralTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
- compLit, ok := node.(*ast.CompositeLit)
- if !ok {
- return nil
- }
- typ := info.TypeOf(compLit)
- if typ == nil {
- return nil
- }
- if compLit.Type != nil {
- return nil
- }
- prefix := ""
- if t, ok := typeparams.CoreType(typ).(*types.Pointer); ok {
- typ = t.Elem()
- prefix = "&"
- }
- // The type for this composite literal is implicit, add an inlay hint.
- start, err := m.PosPosition(tf, compLit.Lbrace)
- if err != nil {
- return nil
+func compositeLiteralTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
+ for curCompLit := range cur.Preorder((*ast.CompositeLit)(nil)) {
+ compLit := curCompLit.Node().(*ast.CompositeLit)
+ typ := info.TypeOf(compLit)
+ if typ == nil {
+ continue
+ }
+ if compLit.Type != nil {
+ continue
+ }
+ prefix := ""
+ if t, ok := typeparams.CoreType(typ).(*types.Pointer); ok {
+ typ = t.Elem()
+ prefix = "&"
+ }
+ // The type for this composite literal is implicit, add an inlay hint.
+ start, err := pgf.PosPosition(compLit.Lbrace)
+ if err != nil {
+ continue
+ }
+ add(protocol.InlayHint{
+ Position: start,
+ Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, qual))),
+ Kind: protocol.Type,
+ })
}
- return []protocol.InlayHint{{
- Position: start,
- Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))),
- Kind: protocol.Type,
- }}
}
func buildLabel(s string) []protocol.InlayHintLabelPart {
diff --git a/gopls/internal/golang/invertifcondition.go b/gopls/internal/golang/invertifcondition.go
index 0fb7d1e4d0a..c8cd7deef5e 100644
--- a/gopls/internal/golang/invertifcondition.go
+++ b/gopls/internal/golang/invertifcondition.go
@@ -8,17 +8,24 @@ import (
"fmt"
"go/ast"
"go/token"
- "go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/internal/astutil/cursor"
)
// invertIfCondition is a singleFileFixFunc that inverts an if/else statement
-func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- ifStatement, _, err := canInvertIfCondition(file, start, end)
+func invertIfCondition(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ var (
+ fset = pkg.FileSet()
+ src = pgf.Src
+ )
+
+ ifStatement, _, err := canInvertIfCondition(pgf.Cursor, start, end)
if err != nil {
return nil, nil, err
}
@@ -35,10 +42,7 @@ func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, fi
// version of the original if body
sourcePos := safetoken.StartPosition(fset, ifStatement.Pos())
- indent := sourcePos.Column - 1
- if indent < 0 {
- indent = 0
- }
+ indent := max(sourcePos.Column-1, 0)
standaloneBodyText := ifBodyToStandaloneCode(fset, ifStatement.Body, src)
replaceElse = analysis.TextEdit{
@@ -241,7 +245,9 @@ func invertAndOr(fset *token.FileSet, expr *ast.BinaryExpr, src []byte) ([]byte,
// canInvertIfCondition reports whether we can do invert-if-condition on the
// code in the given range.
-func canInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) {
+func canInvertIfCondition(curFile cursor.Cursor, start, end token.Pos) (*ast.IfStmt, bool, error) {
+ file := curFile.Node().(*ast.File)
+ // TODO(adonovan): simplify, using Cursor.
path, _ := astutil.PathEnclosingInterval(file, start, end)
for _, node := range path {
stmt, isIfStatement := node.(*ast.IfStmt)
diff --git a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go
index b6a9823957d..cb161671726 100644
--- a/gopls/internal/golang/lines.go
+++ b/gopls/internal/golang/lines.go
@@ -12,20 +12,22 @@ import (
"bytes"
"go/ast"
"go/token"
- "go/types"
"slices"
"sort"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/internal/astutil/cursor"
)
// canSplitLines checks whether we can split lists of elements inside
// an enclosing curly bracket/parens into separate lines.
-func canSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
- itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end)
+func canSplitLines(curFile cursor.Cursor, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
+ itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, curFile, nil, start, end)
if itemType == "" {
return "", false, nil
}
@@ -47,8 +49,8 @@ func canSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (s
// canJoinLines checks whether we can join lists of elements inside an
// enclosing curly bracket/parens into a single line.
-func canJoinLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
- itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end)
+func canJoinLines(curFile cursor.Cursor, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
+ itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, curFile, nil, start, end)
if itemType == "" {
return "", false, nil
}
@@ -84,23 +86,25 @@ func canSplitJoinLines(items []ast.Node, comments []*ast.CommentGroup) bool {
}
// splitLines is a singleFile fixer.
-func splitLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end)
+func splitLines(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ fset := pkg.FileSet()
+ itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, pgf.Cursor, pgf.Src, start, end)
if itemType == "" {
return nil, nil, nil // no fix available
}
- return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ",\n", "\n", ",\n"+indent, indent+"\t"), nil
+ return fset, processLines(fset, items, comments, pgf.Src, braceOpen, braceClose, ",\n", "\n", ",\n"+indent, indent+"\t"), nil
}
// joinLines is a singleFile fixer.
-func joinLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end)
+func joinLines(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ fset := pkg.FileSet()
+ itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, pgf.Cursor, pgf.Src, start, end)
if itemType == "" {
return nil, nil, nil // no fix available
}
- return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ", ", "", "", ""), nil
+ return fset, processLines(fset, items, comments, pgf.Src, braceOpen, braceClose, ", ", "", "", ""), nil
}
// processLines is the common operation for both split and join lines because this split/join operation is
@@ -166,11 +170,14 @@ func processLines(fset *token.FileSet, items []ast.Node, comments []*ast.Comment
}
// findSplitJoinTarget returns the first curly bracket/parens that encloses the current cursor.
-func findSplitJoinTarget(fset *token.FileSet, file *ast.File, src []byte, start, end token.Pos) (itemType string, items []ast.Node, comments []*ast.CommentGroup, indent string, open, close token.Pos) {
+func findSplitJoinTarget(fset *token.FileSet, curFile cursor.Cursor, src []byte, start, end token.Pos) (itemType string, items []ast.Node, comments []*ast.CommentGroup, indent string, open, close token.Pos) {
isCursorInside := func(nodePos, nodeEnd token.Pos) bool {
return nodePos < start && end < nodeEnd
}
+ file := curFile.Node().(*ast.File)
+ // TODO(adonovan): simplify, using Cursor.
+
findTarget := func() (targetType string, target ast.Node, open, close token.Pos) {
path, _ := astutil.PathEnclosingInterval(file, start, end)
for _, node := range path {
diff --git a/gopls/internal/golang/modify_tags.go b/gopls/internal/golang/modify_tags.go
new file mode 100644
index 00000000000..46748c841d1
--- /dev/null
+++ b/gopls/internal/golang/modify_tags.go
@@ -0,0 +1,84 @@
+// Copyright 2025 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 golang
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "go/ast"
+ "go/format"
+ "go/token"
+
+ "github.com/fatih/gomodifytags/modifytags"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
+ "golang.org/x/tools/gopls/internal/util/moreiters"
+ internalastutil "golang.org/x/tools/internal/astutil"
+ "golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/diff"
+ "golang.org/x/tools/internal/tokeninternal"
+)
+
+// Finds the start and end positions of the enclosing struct or returns an error if none is found.
+func findEnclosingStruct(c cursor.Cursor) (token.Pos, token.Pos, error) {
+ for cur := range c.Enclosing((*ast.StructType)(nil)) {
+ return cur.Node().Pos(), cur.Node().End(), nil
+ }
+ return token.NoPos, token.NoPos, fmt.Errorf("no struct enclosing the given positions")
+}
+
+// ModifyTags applies the given struct tag modifications to the specified struct.
+func ModifyTags(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, args command.ModifyTagsArgs, m *modifytags.Modification) ([]protocol.DocumentChange, error) {
+ pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching package file: %v", err)
+ }
+ start, end, err := pgf.RangePos(args.Range)
+ if err != nil {
+ return nil, fmt.Errorf("error getting position information: %v", err)
+ }
+ // If the cursor is at a point and not a selection, we should use the entire enclosing struct.
+ if start == end {
+ cur, ok := pgf.Cursor.FindByPos(start, end)
+ if !ok {
+ return nil, fmt.Errorf("error finding start and end positions: %v", err)
+ }
+ curStruct, ok := moreiters.First(cur.Enclosing((*ast.StructType)(nil)))
+ if !ok {
+ return nil, fmt.Errorf("no enclosing struct type")
+ }
+ start, end = curStruct.Node().Pos(), curStruct.Node().End()
+ }
+
+ // Create a copy of the file node in order to avoid race conditions when we modify the node in Apply.
+ cloned := internalastutil.CloneNode(pgf.File)
+ fset := tokeninternal.FileSetFor(pgf.Tok)
+
+ if err = m.Apply(fset, cloned, start, end); err != nil {
+ return nil, fmt.Errorf("could not modify tags: %v", err)
+ }
+
+ // Construct a list of DocumentChanges based on the diff between the formatted node and the
+ // original file content.
+ var after bytes.Buffer
+ if err := format.Node(&after, fset, cloned); err != nil {
+ return nil, err
+ }
+ edits := diff.Bytes(pgf.Src, after.Bytes())
+ if len(edits) == 0 {
+ return nil, nil
+ }
+ textedits, err := protocol.EditsFromDiffEdits(pgf.Mapper, edits)
+ if err != nil {
+ return nil, fmt.Errorf("error computing edits for %s: %v", args.URI, err)
+ }
+ return []protocol.DocumentChange{
+ protocol.DocumentChangeEdit(fh, textedits),
+ }, nil
+}
diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go
index a5f9cc97fa4..9f2b2bf51a4 100644
--- a/gopls/internal/golang/pkgdoc.go
+++ b/gopls/internal/golang/pkgdoc.go
@@ -14,7 +14,7 @@ package golang
// - rewrite using html/template.
// Or factor with golang.org/x/pkgsite/internal/godoc/dochtml.
// - emit breadcrumbs for parent + sibling packages.
-// - list promoted methods---we have type information!
+// - list promoted methods---we have type information! (golang/go#67158)
// - gather Example tests, following go/doc and pkgsite.
// - add option for doc.AllDecls: show non-exported symbols too.
// - style the
bullets in the index as invisible.
@@ -39,7 +39,6 @@ import (
"go/token"
"go/types"
"html"
- "iter"
"path/filepath"
"slices"
"strings"
@@ -329,7 +328,17 @@ func PackageDocHTML(viewID string, pkg *cache.Package, web Web) ([]byte, error)
filterValues(&t.Vars)
filterFuncs(&t.Funcs)
filterFuncs(&t.Methods)
- return unexported(t.Name)
+ if unexported(t.Name) {
+ // If an unexported type has an exported constructor function,
+ // treat the constructor as an ordinary standalone function.
+ // We will sort Funcs again below.
+ docpkg.Funcs = append(docpkg.Funcs, t.Funcs...)
+ return true // delete this type
+ }
+ return false // keep this type
+ })
+ slices.SortFunc(docpkg.Funcs, func(x, y *doc.Func) int {
+ return strings.Compare(x.Name, y.Name)
})
}
@@ -666,7 +675,7 @@ window.addEventListener('load', function() {
cloneTparams(sig.RecvTypeParams()),
cloneTparams(sig.TypeParams()),
types.NewTuple(append(
- slices.Collect(tupleVariables(sig.Params()))[:3],
+ slices.Collect(sig.Params().Variables())[:3],
types.NewParam(0, nil, "", types.Typ[types.Invalid]))...),
sig.Results(),
false) // any final ...T parameter is truncated
@@ -851,17 +860,3 @@ window.addEventListener('load', function() {
return buf.Bytes(), nil
}
-
-// tupleVariables returns a go1.23 iterator over the variables of a tuple type.
-//
-// Example: for v := range tuple.Variables() { ... }
-// TODO(adonovan): use t.Variables in go1.24.
-func tupleVariables(t *types.Tuple) iter.Seq[*types.Var] {
- return func(yield func(v *types.Var) bool) {
- for i := range t.Len() {
- if !yield(t.At(i)) {
- break
- }
- }
- }
-}
diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go
index 3ecaab6e3e1..7fe054a5a7d 100644
--- a/gopls/internal/golang/references.go
+++ b/gopls/internal/golang/references.go
@@ -441,7 +441,6 @@ func ordinaryReferences(ctx context.Context, snapshot *cache.Snapshot, uri proto
// corresponding methods (see above), which expand the global search.
// The target objects are identified by (PkgPath, objectpath).
for id := range expansions {
- id := id
group.Go(func() error {
// TODO(adonovan): opt: batch these TypeChecks.
pkgs, err := snapshot.TypeCheck(ctx, id)
@@ -521,11 +520,11 @@ func expandMethodSearch(ctx context.Context, snapshot *cache.Snapshot, workspace
var mu sync.Mutex // guards addRdeps, targets, expansions
var group errgroup.Group
for i, index := range indexes {
- i := i
index := index
group.Go(func() error {
- // Consult index for matching methods.
- results := index.Search(key, method)
+ // Consult index for matching (super/sub) methods.
+ const want = methodsets.Supertype | methodsets.Subtype
+ results := index.Search(key, want, method)
if len(results) == 0 {
return nil
}
@@ -583,7 +582,7 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo
var msets typeutil.MethodSetCache
// matches reports whether obj either is or corresponds to a target.
- // (Correspondence is defined as usual for interface methods.)
+ // (Correspondence is defined as usual for interface methods: super/subtype.)
matches := func(obj types.Object) bool {
if containsOrigin(targets, obj) {
return true
@@ -591,7 +590,8 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo
if methodRecvs != nil && obj.Name() == methodName {
if orecv := effectiveReceiver(obj); orecv != nil {
for _, mrecv := range methodRecvs {
- if concreteImplementsIntf(&msets, orecv, mrecv) {
+ if implements(&msets, orecv, mrecv) ||
+ implements(&msets, mrecv, orecv) {
return true
}
}
@@ -602,14 +602,12 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo
// Scan through syntax looking for uses of one of the target objects.
for _, pgf := range pkg.CompiledGoFiles() {
- ast.Inspect(pgf.File, func(n ast.Node) bool {
- if id, ok := n.(*ast.Ident); ok {
- if obj, ok := pkg.TypesInfo().Uses[id]; ok && matches(obj) {
- report(mustLocation(pgf, id), false)
- }
+ for curId := range pgf.Cursor.Preorder((*ast.Ident)(nil)) {
+ id := curId.Node().(*ast.Ident)
+ if obj, ok := pkg.TypesInfo().Uses[id]; ok && matches(obj) {
+ report(mustLocation(pgf, id), false)
}
- return true
- })
+ }
}
return nil
}
diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go
index 26e9d0a5a52..f23f179c6ff 100644
--- a/gopls/internal/golang/rename.go
+++ b/gopls/internal/golang/rename.go
@@ -51,9 +51,11 @@ import (
"go/printer"
"go/token"
"go/types"
+ "maps"
"path"
"path/filepath"
"regexp"
+ "slices"
"sort"
"strconv"
"strings"
@@ -69,8 +71,10 @@ import (
"golang.org/x/tools/gopls/internal/protocol"
goplsastutil "golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
+ "golang.org/x/tools/gopls/internal/util/moreiters"
"golang.org/x/tools/gopls/internal/util/safetoken"
internalastutil "golang.org/x/tools/internal/astutil"
+ "golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/typesinternal"
@@ -167,14 +171,7 @@ func PrepareRename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle,
func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File) (*PrepareItem, error) {
// Does the client support file renaming?
- fileRenameSupported := false
- for _, op := range snapshot.Options().SupportedResourceOperations {
- if op == protocol.Rename {
- fileRenameSupported = true
- break
- }
- }
- if !fileRenameSupported {
+ if !slices.Contains(snapshot.Options().SupportedResourceOperations, protocol.Rename) {
return nil, errors.New("can't rename package: LSP client does not support file renaming")
}
@@ -436,13 +433,7 @@ func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp pro
// become reordered) and that are either identical or
// non-overlapping.
diff.SortEdits(edits)
- filtered := edits[:0]
- for i, edit := range edits {
- if i == 0 || edit != filtered[len(filtered)-1] {
- filtered = append(filtered, edit)
- }
- }
- edits = filtered
+ edits = slices.Compact(edits)
// TODO(adonovan): the logic above handles repeat edits to the
// same file URI (e.g. as a member of package p and p_test) but
@@ -482,6 +473,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
// computes the union across all variants.)
var targets map[types.Object]ast.Node
var pkg *cache.Package
+ var cur cursor.Cursor // of selected Ident or ImportSpec
{
mps, err := snapshot.MetadataForFile(ctx, f.URI())
if err != nil {
@@ -505,6 +497,11 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
if err != nil {
return nil, err
}
+ var ok bool
+ cur, ok = pgf.Cursor.FindByPos(pos, pos)
+ if !ok {
+ return nil, fmt.Errorf("can't find cursor for selection")
+ }
objects, _, err := objectsAt(pkg.TypesInfo(), pgf.File, pos)
if err != nil {
return nil, err
@@ -533,7 +530,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
//
// Note that unlike Funcs, TypeNames are always canonical (they are "left"
// of the type parameters, unlike methods).
- switch obj.(type) { // avoid "obj :=" since cases reassign the var
+ switch obj0 := obj.(type) { // avoid "obj :=" since cases reassign the var
case *types.TypeName:
if _, ok := types.Unalias(obj.Type()).(*types.TypeParam); ok {
// As with capitalized function parameters below, type parameters are
@@ -541,7 +538,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
goto skipObjectPath
}
case *types.Func:
- obj = obj.(*types.Func).Origin()
+ obj = obj0.Origin()
case *types.Var:
// TODO(adonovan): do vars need the origin treatment too? (issue #58462)
@@ -555,7 +552,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
// objectpath, the classifies them as local vars, but as
// they came from export data they lack syntax and the
// correct scope tree (issue #61294).
- if !obj.(*types.Var).IsField() && !typesinternal.IsPackageLevel(obj) {
+ if !obj0.IsField() && !typesinternal.IsPackageLevel(obj) {
goto skipObjectPath
}
}
@@ -571,8 +568,36 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
for obj := range targets {
objects = append(objects, obj)
}
+
editMap, _, err := renameObjects(newName, pkg, objects...)
- return editMap, err
+ if err != nil {
+ return nil, err
+ }
+
+ // If the selected identifier is a receiver declaration,
+ // also rename receivers of other methods of the same type
+ // that don't already have the desired name.
+ // Quietly discard edits from any that can't be renamed.
+ //
+ // We interpret renaming the receiver declaration as
+ // intent for the broader renaming; renaming a use of
+ // the receiver effects only the local renaming.
+ if id, ok := cur.Node().(*ast.Ident); ok && id.Pos() == obj.Pos() {
+ if curDecl, ok := moreiters.First(cur.Enclosing((*ast.FuncDecl)(nil))); ok {
+ decl := curDecl.Node().(*ast.FuncDecl) // enclosing func
+ if decl.Recv != nil &&
+ len(decl.Recv.List) > 0 &&
+ len(decl.Recv.List[0].Names) > 0 {
+ recv := pkg.TypesInfo().Defs[decl.Recv.List[0].Names[0]]
+ if recv == obj {
+ // TODO(adonovan): simplify the above 7 lines to
+ // to "if obj.(*Var).Kind==Recv" in go1.25.
+ renameReceivers(pkg, recv.(*types.Var), newName, editMap)
+ }
+ }
+ }
+ }
+ return editMap, nil
}
// Exported: search globally.
@@ -632,6 +657,39 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
return renameExported(pkgs, declPkgPath, declObjPath, newName)
}
+// renameReceivers renames all receivers of methods of the same named
+// type as recv. The edits of each successful renaming are added to
+// editMap; the failed ones are quietly discarded.
+func renameReceivers(pkg *cache.Package, recv *types.Var, newName string, editMap map[protocol.DocumentURI][]diff.Edit) {
+ _, named := typesinternal.ReceiverNamed(recv)
+ if named == nil {
+ return
+ }
+
+ // Find receivers of other methods of the same named type.
+ for m := range named.Origin().Methods() {
+ recv2 := m.Signature().Recv()
+ if recv2 == recv {
+ continue // don't re-rename original receiver
+ }
+ if recv2.Name() == newName {
+ continue // no renaming needed
+ }
+ editMap2, _, err := renameObjects(newName, pkg, recv2)
+ if err != nil {
+ continue // ignore secondary failures
+ }
+
+ // Since all methods (and their comments)
+ // are disjoint, and don't affect imports,
+ // we can safely assume that all edits are
+ // nonconflicting and disjoint.
+ for uri, edits := range editMap2 {
+ editMap[uri] = append(editMap[uri], edits...)
+ }
+ }
+}
+
// typeCheckReverseDependencies returns the type-checked packages for
// the reverse dependencies of all packages variants containing
// file declURI. The packages are in some topological order.
@@ -657,9 +715,7 @@ func typeCheckReverseDependencies(ctx context.Context, snapshot *cache.Snapshot,
return nil, err
}
allRdeps[variant.ID] = variant // include self
- for id, meta := range rdeps {
- allRdeps[id] = meta
- }
+ maps.Copy(allRdeps, rdeps)
}
var ids []PackageID
for id, meta := range allRdeps {
@@ -1083,7 +1139,12 @@ func renameImports(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.P
continue // not the import we're looking for
}
- pkgname := pkg.TypesInfo().Implicits[imp].(*types.PkgName)
+ pkgname, ok := pkg.TypesInfo().Implicits[imp].(*types.PkgName)
+ if !ok {
+ // "can't happen", but be defensive (#71656)
+ return fmt.Errorf("internal error: missing type information for %s import at %s",
+ imp.Path.Value, safetoken.StartPosition(pkg.FileSet(), imp.Pos()))
+ }
pkgScope := pkg.Types().Scope()
fileScope := pkg.TypesInfo().Scopes[f.File]
diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go
index 280795abe5e..6b89cabbe81 100644
--- a/gopls/internal/golang/rename_check.go
+++ b/gopls/internal/golang/rename_check.go
@@ -45,13 +45,15 @@ import (
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/internal/astutil/cursor"
+ "golang.org/x/tools/internal/astutil/edge"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/refactor/satisfy"
)
// errorf reports an error (e.g. conflict) and prevents file modification.
-func (r *renamer) errorf(pos token.Pos, format string, args ...interface{}) {
+func (r *renamer) errorf(pos token.Pos, format string, args ...any) {
// Conflict error messages in the old gorename tool (whence this
// logic originated) contain rich information associated with
// multiple source lines, such as:
@@ -338,64 +340,58 @@ func deeper(x, y *types.Scope) bool {
// lexical block enclosing the reference. If fn returns false the
// iteration is terminated and findLexicalRefs returns false.
func forEachLexicalRef(pkg *cache.Package, obj types.Object, fn func(id *ast.Ident, block *types.Scope) bool) bool {
+ filter := []ast.Node{
+ (*ast.Ident)(nil),
+ (*ast.SelectorExpr)(nil),
+ (*ast.CompositeLit)(nil),
+ }
ok := true
- var stack []ast.Node
-
- var visit func(n ast.Node) bool
- visit = func(n ast.Node) bool {
- if n == nil {
- stack = stack[:len(stack)-1] // pop
- return false
- }
+ var visit func(cur cursor.Cursor) (descend bool)
+ visit = func(cur cursor.Cursor) (descend bool) {
if !ok {
return false // bail out
}
-
- stack = append(stack, n) // push
- switch n := n.(type) {
+ switch n := cur.Node().(type) {
case *ast.Ident:
if pkg.TypesInfo().Uses[n] == obj {
- block := enclosingBlock(pkg.TypesInfo(), stack)
+ block := enclosingBlock(pkg.TypesInfo(), cur)
if !fn(n, block) {
ok = false
}
}
- return visit(nil) // pop stack
case *ast.SelectorExpr:
// don't visit n.Sel
- ast.Inspect(n.X, visit)
- return visit(nil) // pop stack, don't descend
+ cur.ChildAt(edge.SelectorExpr_X, -1).Inspect(filter, visit)
+ return false // don't descend
case *ast.CompositeLit:
// Handle recursion ourselves for struct literals
// so we don't visit field identifiers.
tv, ok := pkg.TypesInfo().Types[n]
if !ok {
- return visit(nil) // pop stack, don't descend
+ return false // don't descend
}
if is[*types.Struct](typeparams.CoreType(typeparams.Deref(tv.Type))) {
if n.Type != nil {
- ast.Inspect(n.Type, visit)
+ cur.ChildAt(edge.CompositeLit_Type, -1).Inspect(filter, visit)
}
- for _, elt := range n.Elts {
- if kv, ok := elt.(*ast.KeyValueExpr); ok {
- ast.Inspect(kv.Value, visit)
- } else {
- ast.Inspect(elt, visit)
+ for i, elt := range n.Elts {
+ curElt := cur.ChildAt(edge.CompositeLit_Elts, i)
+ if _, ok := elt.(*ast.KeyValueExpr); ok {
+ // skip kv.Key
+ curElt = curElt.ChildAt(edge.KeyValueExpr_Value, -1)
}
+ curElt.Inspect(filter, visit)
}
- return visit(nil) // pop stack, don't descend
+ return false // don't descend
}
}
return true
}
- for _, f := range pkg.Syntax() {
- ast.Inspect(f, visit)
- if len(stack) != 0 {
- panic(stack)
- }
+ for _, pgf := range pkg.CompiledGoFiles() {
+ pgf.Cursor.Inspect(filter, visit)
if !ok {
break
}
@@ -404,11 +400,10 @@ func forEachLexicalRef(pkg *cache.Package, obj types.Object, fn func(id *ast.Ide
}
// enclosingBlock returns the innermost block logically enclosing the
-// specified AST node (an ast.Ident), specified in the form of a path
-// from the root of the file, [file...n].
-func enclosingBlock(info *types.Info, stack []ast.Node) *types.Scope {
- for i := range stack {
- n := stack[len(stack)-1-i]
+// AST node (an ast.Ident), specified as a Cursor.
+func enclosingBlock(info *types.Info, curId cursor.Cursor) *types.Scope {
+ for cur := range curId.Enclosing() {
+ n := cur.Node()
// For some reason, go/types always associates a
// function's scope with its FuncType.
// See comments about scope above.
@@ -472,14 +467,15 @@ func (r *renamer) checkStructField(from *types.Var) {
// This struct is also a named type.
// We must check for direct (non-promoted) field/field
// and method/field conflicts.
- named := r.pkg.TypesInfo().Defs[spec.Name].Type()
- prev, indices, _ := types.LookupFieldOrMethod(named, true, r.pkg.Types(), r.to)
- if len(indices) == 1 {
- r.errorf(from.Pos(), "renaming this field %q to %q",
- from.Name(), r.to)
- r.errorf(prev.Pos(), "\twould conflict with this %s",
- objectKind(prev))
- return // skip checkSelections to avoid redundant errors
+ if tname := r.pkg.TypesInfo().Defs[spec.Name]; tname != nil {
+ prev, indices, _ := types.LookupFieldOrMethod(tname.Type(), true, r.pkg.Types(), r.to)
+ if len(indices) == 1 {
+ r.errorf(from.Pos(), "renaming this field %q to %q",
+ from.Name(), r.to)
+ r.errorf(prev.Pos(), "\twould conflict with this %s",
+ objectKind(prev))
+ return // skip checkSelections to avoid redundant errors
+ }
}
} else {
// This struct is not a named type.
diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go
index cb3f2cfd478..f0286ff1fb3 100644
--- a/gopls/internal/golang/semtok.go
+++ b/gopls/internal/golang/semtok.go
@@ -17,6 +17,7 @@ import (
"log"
"path/filepath"
"regexp"
+ "slices"
"strconv"
"strings"
"time"
@@ -210,7 +211,7 @@ func (tv *tokenVisitor) comment(c *ast.Comment, importByName map[string]*types.P
}
pos := c.Pos()
- for _, line := range strings.Split(c.Text, "\n") {
+ for line := range strings.SplitSeq(c.Text, "\n") {
last := 0
for _, idx := range docLinkRegex.FindAllStringSubmatchIndex(line, -1) {
@@ -616,7 +617,7 @@ func (tv *tokenVisitor) ident(id *ast.Ident) {
obj types.Object
ok bool
)
- if obj, ok = tv.info.Defs[id]; obj != nil {
+ if obj, _ = tv.info.Defs[id]; obj != nil {
// definition
mods = append(mods, semtok.ModDefinition)
tok, mods = tv.appendObjectModifiers(mods, obj)
@@ -721,10 +722,8 @@ func (tv *tokenVisitor) unkIdent(id *ast.Ident) (semtok.Type, []semtok.Modifier)
return semtok.TokType, nil
}
case *ast.ValueSpec:
- for _, p := range parent.Names {
- if p == id {
- return semtok.TokVariable, def
- }
+ if slices.Contains(parent.Names, id) {
+ return semtok.TokVariable, def
}
for _, p := range parent.Values {
if p == id {
diff --git a/gopls/internal/golang/snapshot.go b/gopls/internal/golang/snapshot.go
index c381c962d08..30199d45463 100644
--- a/gopls/internal/golang/snapshot.go
+++ b/gopls/internal/golang/snapshot.go
@@ -14,19 +14,9 @@ import (
"golang.org/x/tools/gopls/internal/protocol"
)
-// NarrowestMetadataForFile returns metadata for the narrowest package
-// (the one with the fewest files) that encloses the specified file.
-// The result may be a test variant, but never an intermediate test variant.
+//go:fix inline
func NarrowestMetadataForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*metadata.Package, error) {
- mps, err := snapshot.MetadataForFile(ctx, uri)
- if err != nil {
- return nil, err
- }
- metadata.RemoveIntermediateTestVariants(&mps)
- if len(mps) == 0 {
- return nil, fmt.Errorf("no package metadata for file %s", uri)
- }
- return mps[0], nil
+ return snapshot.NarrowestMetadataForFile(ctx, uri)
}
// NarrowestPackageForFile is a convenience function that selects the narrowest
diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go
index a04a82988c5..c85080f8a0c 100644
--- a/gopls/internal/golang/stub.go
+++ b/gopls/internal/golang/stub.go
@@ -31,8 +31,7 @@ import (
// methods of the concrete type that is assigned to an interface type
// at the cursor position.
func stubMissingInterfaceMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
- nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
- si := stubmethods.GetIfaceStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes, start)
+ si := stubmethods.GetIfaceStubInfo(pkg.FileSet(), pkg.TypesInfo(), pgf, start, end)
if si == nil {
return nil, nil, fmt.Errorf("nil interface request")
}
@@ -43,8 +42,7 @@ func stubMissingInterfaceMethodsFixer(ctx context.Context, snapshot *cache.Snaps
// method that the user may want to generate based on CallExpr
// at the cursor position.
func stubMissingCalledFunctionFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
- nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
- si := stubmethods.GetCallStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes, start)
+ si := stubmethods.GetCallStubInfo(pkg.FileSet(), pkg.TypesInfo(), pgf, start, end)
if si == nil {
return nil, nil, fmt.Errorf("invalid type request")
}
diff --git a/gopls/internal/golang/stubmethods/stubcalledfunc.go b/gopls/internal/golang/stubmethods/stubcalledfunc.go
index 1b1b6aba7de..b4b59340d83 100644
--- a/gopls/internal/golang/stubmethods/stubcalledfunc.go
+++ b/gopls/internal/golang/stubmethods/stubcalledfunc.go
@@ -13,6 +13,8 @@ import (
"strings"
"unicode"
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/typesinternal"
)
@@ -34,7 +36,9 @@ type CallStubInfo struct {
// GetCallStubInfo extracts necessary information to generate a method definition from
// a CallExpr.
-func GetCallStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos token.Pos) *CallStubInfo {
+func GetCallStubInfo(fset *token.FileSet, info *types.Info, pgf *parsego.File, start, end token.Pos) *CallStubInfo {
+ // TODO(adonovan): simplify, using pgf.Cursor.
+ path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
for i, n := range path {
switch n := n.(type) {
case *ast.CallExpr:
diff --git a/gopls/internal/golang/stubmethods/stubmethods.go b/gopls/internal/golang/stubmethods/stubmethods.go
index f380f5b984d..43842264d70 100644
--- a/gopls/internal/golang/stubmethods/stubmethods.go
+++ b/gopls/internal/golang/stubmethods/stubmethods.go
@@ -15,8 +15,10 @@ import (
"go/types"
"strings"
+ "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/typesinternal"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/typesutil"
)
@@ -49,7 +51,12 @@ type IfaceStubInfo struct {
// function call. This is essentially what the refactor/satisfy does,
// more generally. Refactor to share logic, after auditing 'satisfy'
// for safety on ill-typed code.
-func GetIfaceStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos token.Pos) *IfaceStubInfo {
+func GetIfaceStubInfo(fset *token.FileSet, info *types.Info, pgf *parsego.File, pos, end token.Pos) *IfaceStubInfo {
+ // TODO(adonovan): simplify, using Cursor:
+ // curErr, _ := pgf.Cursor.FindPos(pos, end)
+ // for cur := range curErr.Enclosing() {
+ // switch n := cur.Node().(type) {...
+ path, _ := astutil.PathEnclosingInterval(pgf.File, pos, end)
for _, n := range path {
switch n := n.(type) {
case *ast.ValueSpec:
diff --git a/gopls/internal/golang/symbols.go b/gopls/internal/golang/symbols.go
index 14f2703441c..53fbb663800 100644
--- a/gopls/internal/golang/symbols.go
+++ b/gopls/internal/golang/symbols.go
@@ -82,21 +82,29 @@ func DocumentSymbols(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
// The PackageSymbol data type contains the same fields as protocol.DocumentSymbol, with
// an additional int field "File" that stores the index of that symbol's file in the
// PackageSymbolsResult.Files.
+// Symbols are gathered using syntax rather than type information because type checking is
+// significantly slower. Syntax information provides enough value to the user without
+// causing a lag when loading symbol information across different files.
func PackageSymbols(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (command.PackageSymbolsResult, error) {
ctx, done := event.Start(ctx, "source.PackageSymbols")
defer done()
- mp, err := NarrowestMetadataForFile(ctx, snapshot, uri)
- if err != nil {
- return command.PackageSymbolsResult{}, err
+ pkgFiles := []protocol.DocumentURI{uri}
+
+ // golang/vscode-go#3681: do our best if the file is not in a package.
+ // TODO(rfindley): revisit this in the future once there is more graceful
+ // handling in VS Code.
+ if mp, err := NarrowestMetadataForFile(ctx, snapshot, uri); err == nil {
+ pkgFiles = mp.CompiledGoFiles
}
- pkgfiles := mp.CompiledGoFiles
- // Maps receiver name to the methods that use it
- receiverToMethods := make(map[string][]command.PackageSymbol)
- // Maps type symbol name to its index in symbols
- typeSymbolToIdx := make(map[string]int)
- var symbols []command.PackageSymbol
- for fidx, f := range pkgfiles {
+
+ var (
+ pkgName string
+ symbols []command.PackageSymbol
+ receiverToMethods = make(map[string][]command.PackageSymbol) // receiver name -> methods
+ typeSymbolToIdx = make(map[string]int) // type name -> index in symbols
+ )
+ for fidx, f := range pkgFiles {
fh, err := snapshot.ReadFile(ctx, f)
if err != nil {
return command.PackageSymbolsResult{}, err
@@ -105,6 +113,9 @@ func PackageSymbols(ctx context.Context, snapshot *cache.Snapshot, uri protocol.
if err != nil {
return command.PackageSymbolsResult{}, err
}
+ if pkgName == "" && pgf.File != nil && pgf.File.Name != nil {
+ pkgName = pgf.File.Name.Name
+ }
for _, decl := range pgf.File.Decls {
switch decl := decl.(type) {
case *ast.FuncDecl:
@@ -154,8 +165,8 @@ func PackageSymbols(ctx context.Context, snapshot *cache.Snapshot, uri protocol.
}
}
return command.PackageSymbolsResult{
- PackageName: string(mp.Name),
- Files: pkgfiles,
+ PackageName: pkgName,
+ Files: pkgFiles,
Symbols: symbols,
}, nil
diff --git a/gopls/internal/golang/type_hierarchy.go b/gopls/internal/golang/type_hierarchy.go
new file mode 100644
index 00000000000..bbcd5325d7b
--- /dev/null
+++ b/gopls/internal/golang/type_hierarchy.go
@@ -0,0 +1,157 @@
+// Copyright 2025 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 golang
+
+import (
+ "context"
+ "fmt"
+ "go/token"
+ "go/types"
+ "slices"
+ "strings"
+ "sync"
+
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/methodsets"
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/protocol"
+)
+
+// Type hierarchy support (using method sets)
+//
+// TODO(adonovan):
+// - Support type hierarchy by signatures (using Kind=Function).
+// As with Implementations by signature matching, needs more UX thought.
+//
+// - Allow methods too (using Kind=Method)? It's not exactly in the
+// spirit of TypeHierarchy but it would be useful and it's easy
+// enough to support.
+//
+// FIXME: fix pkg=command-line-arguments problem with query initiated at "error" in builtins.go
+
+// PrepareTypeHierarchy returns the TypeHierarchyItems for the types at the selected position.
+func PrepareTypeHierarchy(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.TypeHierarchyItem, error) {
+ pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+ pos, err := pgf.PositionPos(pp)
+ if err != nil {
+ return nil, err
+ }
+
+ // For now, we require that the selection be a type name.
+ _, obj, _ := referencedObject(pkg, pgf, pos)
+ if obj == nil {
+ return nil, fmt.Errorf("not a symbol")
+ }
+ tname, ok := obj.(*types.TypeName)
+ if !ok {
+ return nil, fmt.Errorf("not a type name")
+ }
+
+ // Find declaration.
+ var declLoc protocol.Location
+ if isBuiltin(obj) {
+ pgf, id, err := builtinDecl(ctx, snapshot, obj)
+ if err != nil {
+ return nil, err
+ }
+ declLoc, err = pgf.NodeLocation(id)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ declLoc, err = mapPosition(ctx, pkg.FileSet(), snapshot, tname.Pos(), tname.Pos()+token.Pos(len(tname.Name())))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ pkgpath := "builtin"
+ if tname.Pkg() != nil {
+ pkgpath = tname.Pkg().Path()
+ }
+
+ return []protocol.TypeHierarchyItem{{
+ Name: tname.Name(),
+ Kind: cond(types.IsInterface(tname.Type()), protocol.Interface, protocol.Class),
+ Detail: pkgpath,
+ URI: declLoc.URI,
+ Range: declLoc.Range, // (in theory this should be the entire declaration)
+ SelectionRange: declLoc.Range,
+ }}, nil
+}
+
+// Subtypes reports information about subtypes of the selected type.
+func Subtypes(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, item protocol.TypeHierarchyItem) ([]protocol.TypeHierarchyItem, error) {
+ return relatedTypes(ctx, snapshot, fh, item, methodsets.Subtype)
+}
+
+// Subtypes reports information about supertypes of the selected type.
+func Supertypes(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, item protocol.TypeHierarchyItem) ([]protocol.TypeHierarchyItem, error) {
+ return relatedTypes(ctx, snapshot, fh, item, methodsets.Supertype)
+}
+
+// relatedTypes is the common implementation of {Super,Sub}types.
+func relatedTypes(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, item protocol.TypeHierarchyItem, rel methodsets.TypeRelation) ([]protocol.TypeHierarchyItem, error) {
+ pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+ pos, err := pgf.PositionPos(item.Range.Start)
+ if err != nil {
+ return nil, err
+ }
+
+ var (
+ itemsMu sync.Mutex
+ items []protocol.TypeHierarchyItem
+ )
+ err = implementationsMsets(ctx, snapshot, pkg, pgf, pos, rel, func(pkgpath metadata.PackagePath, name string, abstract bool, loc protocol.Location) {
+ if pkgpath == "" {
+ pkgpath = "builtin"
+ }
+
+ itemsMu.Lock()
+ defer itemsMu.Unlock()
+ items = append(items, protocol.TypeHierarchyItem{
+ Name: name,
+ Kind: cond(abstract, protocol.Interface, protocol.Class),
+ Detail: string(pkgpath),
+ URI: loc.URI,
+ Range: loc.Range, // (in theory this should be the entire declaration)
+ SelectionRange: loc.Range,
+ })
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Sort by (package, name, URI, range) then
+ // de-duplicate based on the same 4-tuple
+ cmp := func(x, y protocol.TypeHierarchyItem) int {
+ if d := strings.Compare(x.Detail, y.Detail); d != 0 {
+ // Rank the original item's package first.
+ if d := boolCompare(x.Detail == item.Detail, y.Detail == item.Detail); d != 0 {
+ return -d
+ }
+ return d
+ }
+ if d := strings.Compare(x.Name, y.Name); d != 0 {
+ return d
+ }
+ if d := strings.Compare(string(x.URI), string(y.URI)); d != 0 {
+ return d
+ }
+ return protocol.CompareRange(x.SelectionRange, y.Range)
+ }
+ slices.SortFunc(items, cmp)
+ eq := func(x, y protocol.TypeHierarchyItem) bool { return cmp(x, y) == 0 }
+ items = slices.CompactFunc(items, eq)
+
+ return items, nil
+}
diff --git a/gopls/internal/golang/undeclared.go b/gopls/internal/golang/undeclared.go
index 0615386e9bf..515da9bd891 100644
--- a/gopls/internal/golang/undeclared.go
+++ b/gopls/internal/golang/undeclared.go
@@ -16,6 +16,8 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/typesinternal"
)
@@ -69,8 +71,14 @@ func undeclaredFixTitle(path []ast.Node, errMsg string) string {
}
// createUndeclared generates a suggested declaration for an undeclared variable or function.
-func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
- pos := start // don't use the end
+func createUndeclared(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
+ var (
+ fset = pkg.FileSet()
+ info = pkg.TypesInfo()
+ file = pgf.File
+ pos = start // don't use end
+ )
+ // TODO(adonovan): simplify, using Cursor.
path, _ := astutil.PathEnclosingInterval(file, pos, pos)
if len(path) < 2 {
return nil, nil, fmt.Errorf("no expression found")
@@ -83,7 +91,7 @@ func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte,
// Check for a possible call expression, in which case we should add a
// new function declaration.
if isCallPosition(path) {
- return newFunctionDeclaration(path, file, pkg, info, fset)
+ return newFunctionDeclaration(path, file, pkg.Types(), info, fset)
}
var (
firstRef *ast.Ident // We should insert the new declaration before the first occurrence of the undefined ident.
@@ -129,7 +137,7 @@ func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte,
if err != nil {
return nil, nil, fmt.Errorf("could not locate insertion point: %v", err)
}
- indent, err := calculateIndentation(content, fset.File(file.FileStart), insertBeforeStmt)
+ indent, err := calculateIndentation(pgf.Src, fset.File(file.FileStart), insertBeforeStmt)
if err != nil {
return nil, nil, err
}
@@ -138,7 +146,7 @@ func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte,
// Default to 0.
typs = []types.Type{types.Typ[types.Int]}
}
- expr, _ := typesinternal.ZeroExpr(typs[0], typesinternal.FileQualifier(file, pkg))
+ expr, _ := typesinternal.ZeroExpr(typs[0], typesinternal.FileQualifier(file, pkg.Types()))
assignStmt := &ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent(ident.Name)},
Tok: token.DEFINE,
@@ -243,7 +251,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package,
// results is used as an argument
case *types.Tuple:
n := t.Len()
- for i := 0; i < n; i++ {
+ for i := range n {
name := typeToArgName(t.At(i).Type())
nameCounts[name]++
diff --git a/gopls/internal/golang/util.go b/gopls/internal/golang/util.go
index 23fd3443fac..5c54bfcf751 100644
--- a/gopls/internal/golang/util.go
+++ b/gopls/internal/golang/util.go
@@ -11,6 +11,7 @@ import (
"go/token"
"go/types"
"regexp"
+ "slices"
"strings"
"unicode"
@@ -19,16 +20,11 @@ import (
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
- "golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/tokeninternal"
)
-// IsGenerated gets and reads the file denoted by uri and reports
-// whether it contains a "generated file" comment as described at
-// https://golang.org/s/generatedcode.
-//
-// TODO(adonovan): opt: this function does too much.
-// Move snapshot.ReadFile into the caller (most of which have already done it).
+// IsGenerated reads and parses the header of the file denoted by uri
+// and reports whether it [ast.IsGenerated].
func IsGenerated(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) bool {
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
@@ -38,17 +34,7 @@ func IsGenerated(ctx context.Context, snapshot *cache.Snapshot, uri protocol.Doc
if err != nil {
return false
}
- for _, commentGroup := range pgf.File.Comments {
- for _, comment := range commentGroup.List {
- if matched := generatedRx.MatchString(comment.Text); matched {
- // Check if comment is at the beginning of the line in source.
- if safetoken.Position(pgf.Tok, comment.Slash).Column == 1 {
- return true
- }
- }
- }
- }
- return false
+ return ast.IsGenerated(pgf.File)
}
// adjustedObjEnd returns the end position of obj, possibly modified for
@@ -76,11 +62,6 @@ func adjustedObjEnd(obj types.Object) token.Pos {
return obj.Pos() + token.Pos(nameLen)
}
-// Matches cgo generated comment as well as the proposed standard:
-//
-// https://golang.org/s/generatedcode
-var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
-
// FormatNode returns the "pretty-print" output for an ast node.
func FormatNode(fset *token.FileSet, n ast.Node) string {
var buf strings.Builder
@@ -109,10 +90,8 @@ func findFileInDeps(s metadata.Source, mp *metadata.Package, uri protocol.Docume
return nil
}
seen[mp.ID] = true
- for _, cgf := range mp.CompiledGoFiles {
- if cgf == uri {
- return mp
- }
+ if slices.Contains(mp.CompiledGoFiles, uri) {
+ return mp
}
for _, dep := range mp.DepsByPkgPath {
mp := s.Metadata(dep)
@@ -381,9 +360,9 @@ func AbbreviateVarName(s string) string {
return b.String()
}
-// copyrightComment returns the copyright comment group from the input file, or
+// CopyrightComment returns the copyright comment group from the input file, or
// nil if not found.
-func copyrightComment(file *ast.File) *ast.CommentGroup {
+func CopyrightComment(file *ast.File) *ast.CommentGroup {
if len(file.Comments) == 0 {
return nil
}
diff --git a/gopls/internal/golang/workspace_symbol.go b/gopls/internal/golang/workspace_symbol.go
index feba6081515..1a0819b4d52 100644
--- a/gopls/internal/golang/workspace_symbol.go
+++ b/gopls/internal/golang/workspace_symbol.go
@@ -293,17 +293,14 @@ func (c comboMatcher) match(chunks []string) (int, float64) {
func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherType settings.SymbolMatcher, symbolizer symbolizer, query string) ([]protocol.SymbolInformation, error) {
// Extract symbols from all files.
var work []symbolFile
- var roots []string
seen := make(map[protocol.DocumentURI]*metadata.Package) // only scan each file once
for _, snapshot := range snapshots {
// Use the root view URIs for determining (lexically)
// whether a URI is in any open workspace.
folderURI := snapshot.Folder()
- roots = append(roots, strings.TrimRight(string(folderURI), "/"))
- filters := snapshot.Options().DirectoryFilters
- filterer := cache.NewFilterer(filters)
+ pathIncluded := cache.PathIncludeFunc(snapshot.Options().DirectoryFilters)
folder := filepath.ToSlash(folderURI.Path())
var (
@@ -373,7 +370,7 @@ func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherTyp
uri := sp.Files[i]
norm := filepath.ToSlash(uri.Path())
nm := strings.TrimPrefix(norm, folder)
- if filterer.Disallow(nm) {
+ if !pathIncluded(nm) {
continue
}
// Only scan each file once.
@@ -392,7 +389,7 @@ func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherTyp
// which we merge at the end.
nmatchers := runtime.GOMAXPROCS(-1) // matching is CPU bound
results := make(chan *symbolStore)
- for i := 0; i < nmatchers; i++ {
+ for i := range nmatchers {
go func(i int) {
matcher := buildMatcher(matcherType, query)
store := new(symbolStore)
@@ -406,7 +403,7 @@ func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherTyp
// Gather and merge results as they arrive.
var unified symbolStore
- for i := 0; i < nmatchers; i++ {
+ for range nmatchers {
store := <-results
for _, syms := range store.res {
if syms != nil {
diff --git a/gopls/internal/golang/workspace_symbol_test.go b/gopls/internal/golang/workspace_symbol_test.go
index 4982b767754..fbfec8e1204 100644
--- a/gopls/internal/golang/workspace_symbol_test.go
+++ b/gopls/internal/golang/workspace_symbol_test.go
@@ -47,7 +47,7 @@ func TestParseQuery(t *testing.T) {
}
}
-func TestFiltererDisallow(t *testing.T) {
+func TestPathIncludeFunc(t *testing.T) {
tests := []struct {
filters []string
included []string
@@ -119,18 +119,24 @@ func TestFiltererDisallow(t *testing.T) {
[]string{"a/b/c.go", "bb"},
[]string{"b/c/d.go", "b"},
},
+ // golang/vscode-go#3692
+ {
+ []string{"-**/foo", "+**/bar"},
+ []string{"bar/a.go", "a/bar/b.go"},
+ []string{"foo/a.go", "a/foo/b.go"},
+ },
}
for _, test := range tests {
- filterer := cache.NewFilterer(test.filters)
+ pathIncluded := cache.PathIncludeFunc(test.filters)
for _, inc := range test.included {
- if filterer.Disallow(inc) {
+ if !pathIncluded(inc) {
t.Errorf("Filters %v excluded %v, wanted included", test.filters, inc)
}
}
for _, exc := range test.excluded {
- if !filterer.Disallow(exc) {
+ if pathIncluded(exc) {
t.Errorf("Filters %v included %v, wanted excluded", test.filters, exc)
}
}
diff --git a/gopls/internal/licenses/gen-licenses.sh b/gopls/internal/licenses/gen-licenses.sh
index a39f87ce845..b615e566324 100755
--- a/gopls/internal/licenses/gen-licenses.sh
+++ b/gopls/internal/licenses/gen-licenses.sh
@@ -25,9 +25,9 @@ END
# are known to have the same license.
mods=$(go list -deps -f '{{with .Module}}{{.Path}}{{end}}' golang.org/x/tools/gopls | sort -u | grep -v golang.org)
for mod in $mods; do
- # Find the license file, either LICENSE or COPYING, and add it to the result.
+ # Find the license file, either LICENSE, COPYING, or LICENSE.md and add it to the result.
dir=$(go list -m -f {{.Dir}} $mod)
- license=$(ls -1 $dir | grep -E -i '^(LICENSE|COPYING)$')
+ license=$(ls -1 $dir | grep -E -i '^(LICENSE|LICENSE.md|COPYING)?$')
echo "-- $mod $license --" >> $tempfile
echo >> $tempfile
sed 's/^-- / &/' $dir/$license >> $tempfile
diff --git a/gopls/internal/licenses/licenses.go b/gopls/internal/licenses/licenses.go
index e8c5ba9c691..ee73aba2e41 100644
--- a/gopls/internal/licenses/licenses.go
+++ b/gopls/internal/licenses/licenses.go
@@ -30,6 +30,122 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+-- github.com/fatih/camelcase LICENSE.md --
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Fatih Arslan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-- github.com/fatih/gomodifytags LICENSE --
+
+Copyright (c) 2017, Fatih Arslan
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of gomodifytags nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-- github.com/fatih/structtag LICENSE --
+
+Copyright (c) 2017, Fatih Arslan
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of structtag nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+This software includes some portions from Go. Go is used under the terms of the
+BSD like license.
+
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The Go gopher was designed by Renee French. http://reneefrench.blogspot.com/ The design is licensed under the Creative Commons 3.0 Attributions license. Read this article for more details: https://blog.golang.org/gopher
+
-- github.com/google/go-cmp LICENSE --
Copyright (c) 2017 The Go Authors. All rights reserved.
diff --git a/gopls/internal/lsprpc/binder_test.go b/gopls/internal/lsprpc/binder_test.go
index 042056e7777..7072529d1c6 100644
--- a/gopls/internal/lsprpc/binder_test.go
+++ b/gopls/internal/lsprpc/binder_test.go
@@ -56,7 +56,7 @@ func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) j
serverHandler := protocol.ServerHandlerV2(server)
// Wrap the server handler to inject the client into each request context, so
// that log events are reflected back to the client.
- wrapped := jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
+ wrapped := jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) {
ctx = protocol.WithClient(ctx, client)
return serverHandler.Handle(ctx, req)
})
@@ -105,7 +105,7 @@ func (e *TestEnv) dial(ctx context.Context, t *testing.T, dialer jsonrpc2_v2.Dia
l, _ := e.serve(ctx, t, NewForwardBinder(dialer))
dialer = l.Dialer()
}
- conn, err := jsonrpc2_v2.Dial(ctx, dialer, client)
+ conn, err := jsonrpc2_v2.Dial(ctx, dialer, client, nil)
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/lsprpc/commandinterceptor_test.go b/gopls/internal/lsprpc/commandinterceptor_test.go
index 7c83ef993f0..3cfa2e35a7f 100644
--- a/gopls/internal/lsprpc/commandinterceptor_test.go
+++ b/gopls/internal/lsprpc/commandinterceptor_test.go
@@ -15,9 +15,9 @@ import (
. "golang.org/x/tools/gopls/internal/lsprpc"
)
-func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) (interface{}, error)) Middleware {
+func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) (any, error)) Middleware {
return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler {
- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
+ return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) {
if req.Method == "workspace/executeCommand" {
var params protocol.ExecuteCommandParams
if err := json.Unmarshal(req.Params, ¶ms); err == nil {
@@ -35,9 +35,9 @@ func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams)
func TestCommandInterceptor(t *testing.T) {
const command = "foo"
caught := false
- intercept := func(_ *protocol.ExecuteCommandParams) (interface{}, error) {
+ intercept := func(_ *protocol.ExecuteCommandParams) (any, error) {
caught = true
- return map[string]interface{}{}, nil
+ return map[string]any{}, nil
}
ctx := context.Background()
@@ -50,7 +50,7 @@ func TestCommandInterceptor(t *testing.T) {
params := &protocol.ExecuteCommandParams{
Command: command,
}
- var res interface{}
+ var res any
err := conn.Call(ctx, "workspace/executeCommand", params).Await(ctx, &res)
if err != nil {
t.Fatal(err)
diff --git a/gopls/internal/lsprpc/dialer.go b/gopls/internal/lsprpc/dialer.go
index a5f038df9f1..b9aabe4947b 100644
--- a/gopls/internal/lsprpc/dialer.go
+++ b/gopls/internal/lsprpc/dialer.go
@@ -97,7 +97,7 @@ func (d *autoDialer) dialNet(ctx context.Context) (net.Conn, error) {
const retries = 5
// It can take some time for the newly started server to bind to our address,
// so we retry for a bit.
- for retry := 0; retry < retries; retry++ {
+ for retry := range retries {
startDial := time.Now()
netConn, err = net.DialTimeout(d.network, d.addr, dialTimeout)
if err == nil {
diff --git a/gopls/internal/lsprpc/export_test.go b/gopls/internal/lsprpc/export_test.go
index 509129870dc..1caf22415cb 100644
--- a/gopls/internal/lsprpc/export_test.go
+++ b/gopls/internal/lsprpc/export_test.go
@@ -26,7 +26,7 @@ type Canceler struct {
Conn *jsonrpc2_v2.Connection
}
-func (c *Canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
+func (c *Canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) {
if req.Method != "$/cancelRequest" {
return nil, jsonrpc2_v2.ErrNotHandled
}
@@ -34,13 +34,8 @@ func (c *Canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (inter
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrParse, err)
}
- var id jsonrpc2_v2.ID
- switch raw := params.ID.(type) {
- case float64:
- id = jsonrpc2_v2.Int64ID(int64(raw))
- case string:
- id = jsonrpc2_v2.StringID(raw)
- default:
+ id, err := jsonrpc2_v2.MakeID(params.ID)
+ if err != nil {
return nil, fmt.Errorf("%w: invalid ID type %T", jsonrpc2_v2.ErrParse, params.ID)
}
c.Conn.Cancel(id)
@@ -62,10 +57,10 @@ func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection)
client := protocol.ClientDispatcherV2(conn)
clientBinder := NewClientBinder(func(context.Context, protocol.Server) protocol.Client { return client })
- serverConn, err := jsonrpc2_v2.Dial(context.Background(), b.dialer, clientBinder)
+ serverConn, err := jsonrpc2_v2.Dial(context.Background(), b.dialer, clientBinder, nil)
if err != nil {
return jsonrpc2_v2.ConnectionOptions{
- Handler: jsonrpc2_v2.HandlerFunc(func(context.Context, *jsonrpc2_v2.Request) (interface{}, error) {
+ Handler: jsonrpc2_v2.HandlerFunc(func(context.Context, *jsonrpc2_v2.Request) (any, error) {
return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrInternal, err)
}),
}
diff --git a/gopls/internal/lsprpc/goenv.go b/gopls/internal/lsprpc/goenv.go
index 52ec08ff7eb..2b8b94345ca 100644
--- a/gopls/internal/lsprpc/goenv.go
+++ b/gopls/internal/lsprpc/goenv.go
@@ -12,7 +12,7 @@ import (
"golang.org/x/tools/internal/gocommand"
)
-func getGoEnv(ctx context.Context, env map[string]interface{}) (map[string]string, error) {
+func getGoEnv(ctx context.Context, env map[string]any) (map[string]string, error) {
var runEnv []string
for k, v := range env {
runEnv = append(runEnv, fmt.Sprintf("%s=%s", k, v))
diff --git a/gopls/internal/lsprpc/goenv_test.go b/gopls/internal/lsprpc/goenv_test.go
index 6c41540fafb..bc39228c614 100644
--- a/gopls/internal/lsprpc/goenv_test.go
+++ b/gopls/internal/lsprpc/goenv_test.go
@@ -21,7 +21,7 @@ import (
func GoEnvMiddleware() (Middleware, error) {
return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler {
- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
+ return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) {
if req.Method == "initialize" {
if err := addGoEnvToInitializeRequestV2(ctx, req); err != nil {
event.Error(ctx, "adding go env to initialize", err)
@@ -39,20 +39,20 @@ func addGoEnvToInitializeRequestV2(ctx context.Context, req *jsonrpc2_v2.Request
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
return err
}
- var opts map[string]interface{}
+ var opts map[string]any
switch v := params.InitializationOptions.(type) {
case nil:
- opts = make(map[string]interface{})
- case map[string]interface{}:
+ opts = make(map[string]any)
+ case map[string]any:
opts = v
default:
return fmt.Errorf("unexpected type for InitializationOptions: %T", v)
}
envOpt, ok := opts["env"]
if !ok {
- envOpt = make(map[string]interface{})
+ envOpt = make(map[string]any)
}
- env, ok := envOpt.(map[string]interface{})
+ env, ok := envOpt.(map[string]any)
if !ok {
return fmt.Errorf("env option is %T, expected a map", envOpt)
}
@@ -108,8 +108,8 @@ func TestGoEnvMiddleware(t *testing.T) {
conn := env.dial(ctx, t, l.Dialer(), noopBinder, true)
dispatch := protocol.ServerDispatcherV2(conn)
initParams := &protocol.ParamInitialize{}
- initParams.InitializationOptions = map[string]interface{}{
- "env": map[string]interface{}{
+ initParams.InitializationOptions = map[string]any{
+ "env": map[string]any{
"GONOPROXY": "example.com",
},
}
@@ -120,7 +120,7 @@ func TestGoEnvMiddleware(t *testing.T) {
if server.params == nil {
t.Fatalf("initialize params are unset")
}
- envOpts := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{})
+ envOpts := server.params.InitializationOptions.(map[string]any)["env"].(map[string]any)
// Check for an arbitrary Go variable. It should be set.
if _, ok := envOpts["GOPRIVATE"]; !ok {
diff --git a/gopls/internal/lsprpc/lsprpc.go b/gopls/internal/lsprpc/lsprpc.go
index b77557c9a4b..3d26bdd6896 100644
--- a/gopls/internal/lsprpc/lsprpc.go
+++ b/gopls/internal/lsprpc/lsprpc.go
@@ -323,20 +323,20 @@ func addGoEnvToInitializeRequest(ctx context.Context, r jsonrpc2.Request) (jsonr
if err := json.Unmarshal(r.Params(), ¶ms); err != nil {
return nil, err
}
- var opts map[string]interface{}
+ var opts map[string]any
switch v := params.InitializationOptions.(type) {
case nil:
- opts = make(map[string]interface{})
- case map[string]interface{}:
+ opts = make(map[string]any)
+ case map[string]any:
opts = v
default:
return nil, fmt.Errorf("unexpected type for InitializationOptions: %T", v)
}
envOpt, ok := opts["env"]
if !ok {
- envOpt = make(map[string]interface{})
+ envOpt = make(map[string]any)
}
- env, ok := envOpt.(map[string]interface{})
+ env, ok := envOpt.(map[string]any)
if !ok {
return nil, fmt.Errorf(`env option is %T, expected a map`, envOpt)
}
@@ -368,7 +368,7 @@ func (f *forwarder) replyWithDebugAddress(outerCtx context.Context, r jsonrpc2.R
event.Log(outerCtx, "no debug instance to start")
return r
}
- return func(ctx context.Context, result interface{}, outerErr error) error {
+ return func(ctx context.Context, result any, outerErr error) error {
if outerErr != nil {
return r(ctx, result, outerErr)
}
@@ -392,7 +392,7 @@ func (f *forwarder) replyWithDebugAddress(outerCtx context.Context, r jsonrpc2.R
addr, err = di.Serve(outerCtx, addr)
if err != nil {
event.Error(outerCtx, "starting debug server", err)
- return r(ctx, result, outerErr)
+ return r(ctx, result, err)
}
urls := []string{"http://" + addr}
modified.URLs = append(urls, modified.URLs...)
diff --git a/gopls/internal/lsprpc/lsprpc_test.go b/gopls/internal/lsprpc/lsprpc_test.go
index c4ccab71a3e..c8f0267cc3c 100644
--- a/gopls/internal/lsprpc/lsprpc_test.go
+++ b/gopls/internal/lsprpc/lsprpc_test.go
@@ -52,13 +52,12 @@ func (s PingServer) DidOpen(ctx context.Context, params *protocol.DidOpenTextDoc
}
func TestClientLogging(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
+ ctx := t.Context()
server := PingServer{}
client := FakeClient{Logs: make(chan string, 10)}
- ctx = debug.WithInstance(ctx, "")
+ ctx = debug.WithInstance(ctx)
ss := NewStreamServer(cache.New(nil), false, nil).(*StreamServer)
ss.serverForTest = server
ts := servertest.NewPipeServer(ss, nil)
@@ -121,7 +120,7 @@ func checkClose(t *testing.T, closer func() error) {
func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) {
t.Helper()
- serveCtx := debug.WithInstance(ctx, "")
+ serveCtx := debug.WithInstance(ctx)
ss := NewStreamServer(cache.New(nil), false, nil).(*StreamServer)
ss.serverForTest = s
tsDirect := servertest.NewTCPServer(serveCtx, ss, nil)
@@ -212,10 +211,9 @@ func TestDebugInfoLifecycle(t *testing.T) {
}
}()
- baseCtx, cancel := context.WithCancel(context.Background())
- defer cancel()
- clientCtx := debug.WithInstance(baseCtx, "")
- serverCtx := debug.WithInstance(baseCtx, "")
+ baseCtx := t.Context()
+ clientCtx := debug.WithInstance(baseCtx)
+ serverCtx := debug.WithInstance(baseCtx)
cache := cache.New(nil)
ss := NewStreamServer(cache, false, nil)
@@ -302,8 +300,8 @@ func TestEnvForwarding(t *testing.T) {
conn.Go(ctx, jsonrpc2.MethodNotFound)
dispatch := protocol.ServerDispatcher(conn)
initParams := &protocol.ParamInitialize{}
- initParams.InitializationOptions = map[string]interface{}{
- "env": map[string]interface{}{
+ initParams.InitializationOptions = map[string]any{
+ "env": map[string]any{
"GONOPROXY": "example.com",
},
}
@@ -314,7 +312,7 @@ func TestEnvForwarding(t *testing.T) {
if server.params == nil {
t.Fatalf("initialize params are unset")
}
- env := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{})
+ env := server.params.InitializationOptions.(map[string]any)["env"].(map[string]any)
// Check for an arbitrary Go variable. It should be set.
if _, ok := env["GOPRIVATE"]; !ok {
diff --git a/gopls/internal/lsprpc/middleware_test.go b/gopls/internal/lsprpc/middleware_test.go
index 526c7343b78..afa6ae78d2f 100644
--- a/gopls/internal/lsprpc/middleware_test.go
+++ b/gopls/internal/lsprpc/middleware_test.go
@@ -154,7 +154,7 @@ func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder {
// Wrap the delegated handler to accept the handshake.
delegate := opts.Handler
- opts.Handler = jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
+ opts.Handler = jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) {
if req.Method == HandshakeMethod {
var peerInfo PeerInfo
if err := json.Unmarshal(req.Params, &peerInfo); err != nil {
diff --git a/gopls/internal/mod/diagnostics.go b/gopls/internal/mod/diagnostics.go
index a89c148d7a7..52f3704ed0f 100644
--- a/gopls/internal/mod/diagnostics.go
+++ b/gopls/internal/mod/diagnostics.go
@@ -34,7 +34,7 @@ func ParseDiagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[protoc
return collectDiagnostics(ctx, snapshot, parseDiagnostics)
}
-// Diagnostics returns diagnostics from running go mod tidy.
+// TidyDiagnostics returns diagnostics from running go mod tidy.
func TidyDiagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[protocol.DocumentURI][]*cache.Diagnostic, error) {
ctx, done := event.Start(ctx, "mod.Diagnostics", snapshot.Labels()...)
defer done()
@@ -69,7 +69,6 @@ func collectDiagnostics(ctx context.Context, snapshot *cache.Snapshot, diagFn fu
reports := make(map[protocol.DocumentURI][]*cache.Diagnostic)
for _, uri := range snapshot.View().ModFiles() {
- uri := uri
g.Go(func() error {
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
diff --git a/gopls/internal/mod/hover.go b/gopls/internal/mod/hover.go
index 458c5ce67d5..b9b026674fa 100644
--- a/gopls/internal/mod/hover.go
+++ b/gopls/internal/mod/hover.go
@@ -8,10 +8,12 @@ import (
"bytes"
"context"
"fmt"
+ "slices"
"sort"
"strings"
"golang.org/x/mod/modfile"
+ "golang.org/x/mod/module"
"golang.org/x/mod/semver"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
@@ -24,16 +26,8 @@ import (
)
func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
- var found bool
- for _, uri := range snapshot.View().ModFiles() {
- if fh.URI() == uri {
- found = true
- break
- }
- }
-
// We only provide hover information for the view's go.mod files.
- if !found {
+ if !slices.Contains(snapshot.View().ModFiles(), fh.URI()) {
return nil, nil
}
@@ -116,7 +110,7 @@ func hoverOnRequireStatement(ctx context.Context, pm *cache.ParsedModule, offset
options := snapshot.Options()
isPrivate := snapshot.IsGoPrivatePath(req.Mod.Path)
header := formatHeader(req.Mod.Path, options)
- explanation = formatExplanation(explanation, req, options, isPrivate)
+ explanation = formatExplanation(explanation, pm.ReplaceMap, req, options, isPrivate)
vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck)
return &protocol.Hover{
@@ -327,7 +321,7 @@ func vulnerablePkgsInfo(findings []*govulncheck.Finding, useMarkdown bool) strin
return b.String()
}
-func formatExplanation(text string, req *modfile.Require, options *settings.Options, isPrivate bool) string {
+func formatExplanation(text string, replaceMap map[module.Version]module.Version, req *modfile.Require, options *settings.Options, isPrivate bool) string {
text = strings.TrimSuffix(text, "\n")
splt := strings.Split(text, "\n")
length := len(splt)
@@ -348,7 +342,17 @@ func formatExplanation(text string, req *modfile.Require, options *settings.Opti
if !isPrivate && options.PreferredContentFormat == protocol.Markdown {
target := imp
if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
- target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
+ mod := req.Mod
+ // respect the repalcement when constructing a module link.
+ if m, ok := replaceMap[req.Mod]; ok {
+ // Have: 'replace A v1.2.3 => A vx.x.x' or 'replace A v1.2.3 => B vx.x.x'.
+ mod = m
+ } else if m, ok := replaceMap[module.Version{Path: req.Mod.Path}]; ok &&
+ !modfile.IsDirectoryPath(m.Path) { // exclude local replacement.
+ // Have: 'replace A => A vx.x.x' or 'replace A => B vx.x.x'.
+ mod = m
+ }
+ target = strings.Replace(target, req.Mod.Path, mod.String(), 1)
}
reference = fmt.Sprintf("[%s](%s)", imp, cache.BuildLink(options.LinkTarget, target, ""))
}
diff --git a/gopls/internal/progress/progress_test.go b/gopls/internal/progress/progress_test.go
index 642103ae025..687f99ba4a1 100644
--- a/gopls/internal/progress/progress_test.go
+++ b/gopls/internal/progress/progress_test.go
@@ -107,7 +107,6 @@ func TestProgressTracker_Reporting(t *testing.T) {
wantEnded: 1,
},
} {
- test := test
t.Run(test.name, func(t *testing.T) {
ctx, tracker, client := setup()
ctx, cancel := context.WithCancel(ctx)
diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go
index c9b18a40cb8..b6c12e4b50c 100644
--- a/gopls/internal/protocol/command/command_gen.go
+++ b/gopls/internal/protocol/command/command_gen.go
@@ -46,6 +46,7 @@ const (
ListKnownPackages Command = "gopls.list_known_packages"
MaybePromptForTelemetry Command = "gopls.maybe_prompt_for_telemetry"
MemStats Command = "gopls.mem_stats"
+ ModifyTags Command = "gopls.modify_tags"
Modules Command = "gopls.modules"
PackageSymbols Command = "gopls.package_symbols"
Packages Command = "gopls.packages"
@@ -91,6 +92,7 @@ var Commands = []Command{
ListKnownPackages,
MaybePromptForTelemetry,
MemStats,
+ ModifyTags,
Modules,
PackageSymbols,
Packages,
@@ -242,6 +244,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte
return nil, s.MaybePromptForTelemetry(ctx)
case MemStats:
return s.MemStats(ctx)
+ case ModifyTags:
+ var a0 ModifyTagsArgs
+ if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+ return nil, err
+ }
+ return nil, s.ModifyTags(ctx, a0)
case Modules:
var a0 ModulesArgs
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@@ -530,6 +538,14 @@ func NewMemStatsCommand(title string) *protocol.Command {
}
}
+func NewModifyTagsCommand(title string, a0 ModifyTagsArgs) *protocol.Command {
+ return &protocol.Command{
+ Title: title,
+ Command: ModifyTags.String(),
+ Arguments: MustMarshalArgs(a0),
+ }
+}
+
func NewModulesCommand(title string, a0 ModulesArgs) *protocol.Command {
return &protocol.Command{
Title: title,
diff --git a/gopls/internal/protocol/command/commandmeta/meta.go b/gopls/internal/protocol/command/commandmeta/meta.go
index f147898e192..7c3a3acc12f 100644
--- a/gopls/internal/protocol/command/commandmeta/meta.go
+++ b/gopls/internal/protocol/command/commandmeta/meta.go
@@ -224,10 +224,7 @@ func lspName(methodName string) string {
func splitCamel(s string) []string {
var words []string
for len(s) > 0 {
- last := strings.LastIndexFunc(s, unicode.IsUpper)
- if last < 0 {
- last = 0
- }
+ last := max(strings.LastIndexFunc(s, unicode.IsUpper), 0)
if last == len(s)-1 {
// Group initialisms as a single word.
last = 1 + strings.LastIndexFunc(s[:last], func(r rune) bool { return !unicode.IsUpper(r) })
diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go
index 32e03dd388a..01d41dec473 100644
--- a/gopls/internal/protocol/command/interface.go
+++ b/gopls/internal/protocol/command/interface.go
@@ -297,6 +297,9 @@ type Interface interface {
// PackageSymbols: Return information about symbols in the given file's package.
PackageSymbols(context.Context, PackageSymbolsArgs) (PackageSymbolsResult, error)
+
+ // ModifyTags: Add or remove struct tags on a given node.
+ ModifyTags(context.Context, ModifyTagsArgs) error
}
type RunTestsArgs struct {
@@ -814,6 +817,9 @@ type PackageSymbol struct {
Detail string `json:"detail,omitempty"`
+ // protocol.SymbolKind maps an integer to an enum:
+ // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind
+ // i.e. File = 1
Kind protocol.SymbolKind `json:"kind"`
Tags []protocol.SymbolTag `json:"tags,omitempty"`
@@ -827,3 +833,19 @@ type PackageSymbol struct {
// Index of this symbol's file in PackageSymbolsResult.Files
File int `json:"file,omitempty"`
}
+
+// ModifyTagsArgs holds variables that determine how struct tags are modified.
+type ModifyTagsArgs struct {
+ URI protocol.DocumentURI // uri of the file to be modified
+ Range protocol.Range // range in the file for where to modify struct tags
+ Add string // comma-separated list of tags to add; i.e. "json,xml"
+ AddOptions string // comma-separated list of options to add, per tag; i.e. "json=omitempty"
+ Remove string // comma-separated list of tags to remove
+ RemoveOptions string // comma-separated list of options to remove
+ Clear bool // if set, clear all tags. tags are cleared before any new tags are added
+ ClearOptions bool // if set, clear all tag options; options are cleared before any new options are added
+ Overwrite bool // if set, replace existing tags when adding
+ SkipUnexportedFields bool // if set, do not modify tags on unexported struct fields
+ Transform string // transform rule for adding tags; i.e. "snakecase"
+ ValueFormat string // format for the tag's value, after transformation; for example "column:{field}"
+}
diff --git a/gopls/internal/protocol/edits.go b/gopls/internal/protocol/edits.go
index 5f70c4efdb5..c5d3592a8ee 100644
--- a/gopls/internal/protocol/edits.go
+++ b/gopls/internal/protocol/edits.go
@@ -6,6 +6,7 @@ package protocol
import (
"fmt"
+ "slices"
"golang.org/x/tools/internal/diff"
)
@@ -16,7 +17,7 @@ func EditsFromDiffEdits(m *Mapper, edits []diff.Edit) ([]TextEdit, error) {
// LSP doesn't require TextEditArray to be sorted:
// this is the receiver's concern. But govim, and perhaps
// other clients have historically relied on the order.
- edits = append([]diff.Edit(nil), edits...)
+ edits = slices.Clone(edits)
diff.SortEdits(edits)
result := make([]TextEdit, len(edits))
diff --git a/gopls/internal/protocol/generate/generate.go b/gopls/internal/protocol/generate/generate.go
index 2bb14790940..fef8ef417eb 100644
--- a/gopls/internal/protocol/generate/generate.go
+++ b/gopls/internal/protocol/generate/generate.go
@@ -32,7 +32,7 @@ func generateDoc(out *bytes.Buffer, doc string) {
return
}
var list bool
- for _, line := range strings.Split(doc, "\n") {
+ for line := range strings.SplitSeq(doc, "\n") {
// Lists in metaModel.json start with a dash.
// To make a go doc list they have to be preceded
// by a blank line, and indented.
@@ -54,39 +54,44 @@ func generateDoc(out *bytes.Buffer, doc string) {
// decide if a property is optional, and if it needs a *
// return ",omitempty" if it is optional, and "*" if it needs a pointer
-func propStar(name string, t NameType, gotype string) (string, string) {
- var opt, star string
+func propStar(name string, t NameType, gotype string) (omitempty, indirect bool) {
if t.Optional {
- star = "*"
- opt = ",omitempty"
+ switch gotype {
+ case "uint32", "int32":
+ // in FoldingRange.endLine, 0 and empty have different semantics
+ // There seem to be no other cases.
+ default:
+ indirect = true
+ omitempty = true
+ }
}
if strings.HasPrefix(gotype, "[]") || strings.HasPrefix(gotype, "map[") {
- star = "" // passed by reference, so no need for *
+ indirect = false // passed by reference, so no need for *
} else {
switch gotype {
- case "bool", "uint32", "int32", "string", "interface{}", "any":
- star = "" // gopls compatibility if t.Optional
+ case "bool", "string", "interface{}", "any":
+ indirect = false // gopls compatibility if t.Optional
}
}
- ostar, oopt := star, opt
+ oind, oomit := indirect, omitempty
if newStar, ok := goplsStar[prop{name, t.Name}]; ok {
switch newStar {
case nothing:
- star, opt = "", ""
+ indirect, omitempty = false, false
case wantStar:
- star, opt = "*", ""
+ indirect, omitempty = false, false
case wantOpt:
- star, opt = "", ",omitempty"
+ indirect, omitempty = false, true
case wantOptStar:
- star, opt = "*", ",omitempty"
+ indirect, omitempty = true, true
}
- if star == ostar && opt == oopt { // no change
- log.Printf("goplsStar[ {%q, %q} ](%d) useless %s/%s %s/%s", name, t.Name, t.Line, ostar, star, oopt, opt)
+ if indirect == oind && omitempty == oomit { // no change
+ log.Printf("goplsStar[ {%q, %q} ](%d) useless %v/%v %v/%v", name, t.Name, t.Line, oind, indirect, oomit, omitempty)
}
usedGoplsStar[prop{name, t.Name}] = true
}
- return opt, star
+ return
}
func goName(s string) string {
diff --git a/gopls/internal/protocol/generate/output.go b/gopls/internal/protocol/generate/output.go
index ba9d0cb909f..5eaa0cba969 100644
--- a/gopls/internal/protocol/generate/output.go
+++ b/gopls/internal/protocol/generate/output.go
@@ -273,10 +273,17 @@ func genProps(out *bytes.Buffer, props []NameType, name string) {
tp = newNm
}
// it's a pointer if it is optional, or for gopls compatibility
- opt, star := propStar(name, p, tp)
- json := fmt.Sprintf(" `json:\"%s%s\"`", p.Name, opt)
+ omit, star := propStar(name, p, tp)
+ json := fmt.Sprintf(" `json:\"%s\"`", p.Name)
+ if omit {
+ json = fmt.Sprintf(" `json:\"%s,omitempty\"`", p.Name)
+ }
generateDoc(out, p.Documentation)
- fmt.Fprintf(out, "\t%s %s%s %s\n", goName(p.Name), star, tp, json)
+ if star {
+ fmt.Fprintf(out, "\t%s *%s %s\n", goName(p.Name), tp, json)
+ } else {
+ fmt.Fprintf(out, "\t%s %s %s\n", goName(p.Name), tp, json)
+ }
}
}
diff --git a/gopls/internal/protocol/json_test.go b/gopls/internal/protocol/json_test.go
index 9aac110fa3b..2c03095a84c 100644
--- a/gopls/internal/protocol/json_test.go
+++ b/gopls/internal/protocol/json_test.go
@@ -103,15 +103,9 @@ func tryChange(start, end int, repl string) error {
var p, q protocol.ParamInitialize
mod := input[:start] + repl + input[end:]
excerpt := func() (string, string) {
- a := start - 5
- if a < 0 {
- a = 0
- }
- b := end + 5
- if b > len(input) {
- // trusting repl to be no longer than what it replaces
- b = len(input)
- }
+ a := max(start-5, 0)
+ // trusting repl to be no longer than what it replaces
+ b := min(end+5, len(input))
ma := input[a:b]
mb := mod[a:b]
return ma, mb
diff --git a/gopls/internal/protocol/semtok/semtok.go b/gopls/internal/protocol/semtok/semtok.go
index 6b05b8bb5e2..86332d37e1a 100644
--- a/gopls/internal/protocol/semtok/semtok.go
+++ b/gopls/internal/protocol/semtok/semtok.go
@@ -173,7 +173,7 @@ func Encode(
x := make([]uint32, 5*len(tokens))
var j int
var last Token
- for i := 0; i < len(tokens); i++ {
+ for i := range tokens {
item := tokens[i]
typ, ok := typeMap[item.Type]
if !ok {
diff --git a/gopls/internal/protocol/tsprotocol.go b/gopls/internal/protocol/tsprotocol.go
index 444e51e0717..7306f62a7ad 100644
--- a/gopls/internal/protocol/tsprotocol.go
+++ b/gopls/internal/protocol/tsprotocol.go
@@ -55,7 +55,7 @@ type ApplyWorkspaceEditResult struct {
// Depending on the client's failure handling strategy `failedChange` might
// contain the index of the change that failed. This property is only available
// if the client signals a `failureHandlingStrategy` in its client capabilities.
- FailedChange uint32 `json:"failedChange,omitempty"`
+ FailedChange uint32 `json:"failedChange"`
}
// A base for all symbol information.
@@ -2377,12 +2377,12 @@ type FoldingRange struct {
// To be valid, the end must be zero or larger and smaller than the number of lines in the document.
StartLine uint32 `json:"startLine"`
// The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line.
- StartCharacter uint32 `json:"startCharacter,omitempty"`
+ StartCharacter uint32 `json:"startCharacter"`
// The zero-based end line of the range to fold. The folded area ends with the line's last character.
// To be valid, the end must be zero or larger and smaller than the number of lines in the document.
EndLine uint32 `json:"endLine"`
// The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line.
- EndCharacter uint32 `json:"endCharacter,omitempty"`
+ EndCharacter uint32 `json:"endCharacter"`
// Describes the kind of the folding range such as 'comment' or 'region'. The kind
// is used to categorize folding ranges and used by commands like 'Fold all comments'.
// See {@link FoldingRangeKind} for an enumeration of standardized kinds.
@@ -2405,7 +2405,7 @@ type FoldingRangeClientCapabilities struct {
// The maximum number of folding ranges that the client prefers to receive
// per document. The value serves as a hint, servers are free to follow the
// limit.
- RangeLimit uint32 `json:"rangeLimit,omitempty"`
+ RangeLimit uint32 `json:"rangeLimit"`
// If set, the client signals that it only supports folding complete lines.
// If set, client will ignore specified `startCharacter` and `endCharacter`
// properties in a FoldingRange.
@@ -4148,7 +4148,7 @@ type PublishDiagnosticsParams struct {
// Optional the version number of the document the diagnostics are published for.
//
// @since 3.15.0
- Version int32 `json:"version,omitempty"`
+ Version int32 `json:"version"`
// An array of diagnostic information items.
Diagnostics []Diagnostic `json:"diagnostics"`
}
@@ -4907,7 +4907,7 @@ type SignatureHelp struct {
//
// In future version of the protocol this property might become
// mandatory to better express this.
- ActiveSignature uint32 `json:"activeSignature,omitempty"`
+ ActiveSignature uint32 `json:"activeSignature"`
// The active parameter of the active signature.
//
// If `null`, no parameter of the signature is active (for example a named
@@ -4924,7 +4924,7 @@ type SignatureHelp struct {
// In future version of the protocol this property might become
// mandatory (but still nullable) to better express the active parameter if
// the active signature does have any.
- ActiveParameter uint32 `json:"activeParameter,omitempty"`
+ ActiveParameter uint32 `json:"activeParameter"`
}
// Client Capabilities for a {@link SignatureHelpRequest}.
@@ -5036,7 +5036,7 @@ type SignatureInformation struct {
// `SignatureHelp.activeParameter`.
//
// @since 3.16.0
- ActiveParameter uint32 `json:"activeParameter,omitempty"`
+ ActiveParameter uint32 `json:"activeParameter"`
}
// An interactive text edit.
@@ -5261,7 +5261,7 @@ type TextDocumentContentChangePartial struct {
// The optional length of the range that got replaced.
//
// @deprecated use range instead.
- RangeLength uint32 `json:"rangeLength,omitempty"`
+ RangeLength uint32 `json:"rangeLength"`
// The new text for the provided range.
Text string `json:"text"`
}
@@ -5764,7 +5764,7 @@ type WorkDoneProgressBegin struct {
//
// The value should be steadily rising. Clients are free to ignore values
// that are not following this rule. The value range is [0, 100].
- Percentage uint32 `json:"percentage,omitempty"`
+ Percentage uint32 `json:"percentage"`
}
// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#workDoneProgressCancelParams
@@ -5824,7 +5824,7 @@ type WorkDoneProgressReport struct {
//
// The value should be steadily rising. Clients are free to ignore values
// that are not following this rule. The value range is [0, 100]
- Percentage uint32 `json:"percentage,omitempty"`
+ Percentage uint32 `json:"percentage"`
}
// Workspace specific client capabilities.
diff --git a/gopls/internal/protocol/uri.go b/gopls/internal/protocol/uri.go
index 4105bd041f8..491d767805f 100644
--- a/gopls/internal/protocol/uri.go
+++ b/gopls/internal/protocol/uri.go
@@ -67,6 +67,11 @@ func (uri *DocumentURI) UnmarshalText(data []byte) (err error) {
return
}
+// Clean returns the cleaned uri by triggering filepath.Clean underlying.
+func Clean(uri DocumentURI) DocumentURI {
+ return URIFromPath(filepath.Clean(uri.Path()))
+}
+
// Path returns the file path for the given URI.
//
// DocumentURI("").Path() returns the empty string.
diff --git a/gopls/internal/server/call_hierarchy.go b/gopls/internal/server/call_hierarchy.go
index 671d4f8c81c..1887767250c 100644
--- a/gopls/internal/server/call_hierarchy.go
+++ b/gopls/internal/server/call_hierarchy.go
@@ -14,7 +14,7 @@ import (
)
func (s *server) PrepareCallHierarchy(ctx context.Context, params *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) {
- ctx, done := event.Start(ctx, "lsp.Server.prepareCallHierarchy")
+ ctx, done := event.Start(ctx, "server.PrepareCallHierarchy")
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
@@ -22,14 +22,15 @@ func (s *server) PrepareCallHierarchy(ctx context.Context, params *protocol.Call
return nil, err
}
defer release()
- if snapshot.FileKind(fh) != file.Go {
- return nil, nil // empty result
+ switch snapshot.FileKind(fh) {
+ case file.Go:
+ return golang.PrepareCallHierarchy(ctx, snapshot, fh, params.Position)
}
- return golang.PrepareCallHierarchy(ctx, snapshot, fh, params.Position)
+ return nil, nil // empty result
}
func (s *server) IncomingCalls(ctx context.Context, params *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) {
- ctx, done := event.Start(ctx, "lsp.Server.incomingCalls")
+ ctx, done := event.Start(ctx, "server.IncomingCalls")
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.Item.URI)
@@ -37,14 +38,15 @@ func (s *server) IncomingCalls(ctx context.Context, params *protocol.CallHierarc
return nil, err
}
defer release()
- if snapshot.FileKind(fh) != file.Go {
- return nil, nil // empty result
+ switch snapshot.FileKind(fh) {
+ case file.Go:
+ return golang.IncomingCalls(ctx, snapshot, fh, params.Item.Range.Start)
}
- return golang.IncomingCalls(ctx, snapshot, fh, params.Item.Range.Start)
+ return nil, nil // empty result
}
func (s *server) OutgoingCalls(ctx context.Context, params *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) {
- ctx, done := event.Start(ctx, "lsp.Server.outgoingCalls")
+ ctx, done := event.Start(ctx, "server.OutgoingCalls")
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.Item.URI)
@@ -52,8 +54,9 @@ func (s *server) OutgoingCalls(ctx context.Context, params *protocol.CallHierarc
return nil, err
}
defer release()
- if snapshot.FileKind(fh) != file.Go {
- return nil, nil // empty result
+ switch snapshot.FileKind(fh) {
+ case file.Go:
+ return golang.OutgoingCalls(ctx, snapshot, fh, params.Item.Range.Start)
}
- return golang.OutgoingCalls(ctx, snapshot, fh, params.Item.Range.Start)
+ return nil, nil // empty result
}
diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go
index c36e7c33f94..9fa2bf54459 100644
--- a/gopls/internal/server/code_action.go
+++ b/gopls/internal/server/code_action.go
@@ -8,7 +8,6 @@ import (
"context"
"fmt"
"slices"
- "sort"
"strings"
"golang.org/x/tools/gopls/internal/cache"
@@ -22,7 +21,7 @@ import (
)
func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
- ctx, done := event.Start(ctx, "lsp.Server.codeAction")
+ ctx, done := event.Start(ctx, "server.CodeAction")
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
@@ -225,7 +224,7 @@ func triggerKind(params *protocol.CodeActionParams) protocol.CodeActionTriggerKi
// This feature allows capable clients to preview and selectively apply the diff
// instead of applying the whole thing unconditionally through workspace/applyEdit.
func (s *server) ResolveCodeAction(ctx context.Context, ca *protocol.CodeAction) (*protocol.CodeAction, error) {
- ctx, done := event.Start(ctx, "lsp.Server.resolveCodeAction")
+ ctx, done := event.Start(ctx, "server.ResolveCodeAction")
defer done()
// Only resolve the code action if there is Data provided.
@@ -354,9 +353,7 @@ func (s *server) getSupportedCodeActions() []protocol.CodeActionKind {
for kind := range allCodeActionKinds {
result = append(result, kind)
}
- sort.Slice(result, func(i, j int) bool {
- return result[i] < result[j]
- })
+ slices.Sort(result)
return result
}
diff --git a/gopls/internal/server/code_lens.go b/gopls/internal/server/code_lens.go
index 67b359e866c..2509452f0b5 100644
--- a/gopls/internal/server/code_lens.go
+++ b/gopls/internal/server/code_lens.go
@@ -22,7 +22,7 @@ import (
// CodeLens reports the set of available CodeLenses
// (range-associated commands) in the given file.
func (s *server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
- ctx, done := event.Start(ctx, "lsp.Server.codeLens", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.CodeLens", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go
index 2b5c282a28f..a3345d33a1d 100644
--- a/gopls/internal/server/command.go
+++ b/gopls/internal/server/command.go
@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"log"
+ "maps"
"os"
"path/filepath"
"regexp"
@@ -22,6 +23,7 @@ import (
"strings"
"sync"
+ "github.com/fatih/gomodifytags/modifytags"
"golang.org/x/mod/modfile"
"golang.org/x/telemetry/counter"
"golang.org/x/tools/go/ast/astutil"
@@ -46,8 +48,8 @@ import (
"golang.org/x/tools/internal/xcontext"
)
-func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
- ctx, done := event.Start(ctx, "lsp.Server.executeCommand")
+func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (any, error) {
+ ctx, done := event.Start(ctx, "server.ExecuteCommand")
defer done()
// For test synchronization, always create a progress notification.
@@ -59,14 +61,7 @@ func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCom
defer work.End(ctx, "Done.")
}
- var found bool
- for _, name := range s.Options().SupportedCommands {
- if name == params.Command {
- found = true
- break
- }
- }
- if !found {
+ if !slices.Contains(s.Options().SupportedCommands, params.Command) {
return nil, fmt.Errorf("%s is not a supported command", params.Command)
}
@@ -1202,9 +1197,7 @@ func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.U
}
}
// Overwrite if there is any govulncheck-based result.
- for modfile, result := range deps.snapshot.Vulnerabilities() {
- ret[modfile] = result
- }
+ maps.Copy(ret, deps.snapshot.Vulnerabilities())
return nil
})
return ret, err
@@ -1652,7 +1645,7 @@ func (c *commandHandler) DiagnoseFiles(ctx context.Context, args command.Diagnos
// Though note that implementing pull diagnostics may cause some servers to
// request diagnostics in an ad-hoc manner, and break our intentional pacing.
- ctx, done := event.Start(ctx, "lsp.server.DiagnoseFiles")
+ ctx, done := event.Start(ctx, "commandHandler.DiagnoseFiles")
defer done()
snapshots := make(map[*cache.Snapshot]bool)
@@ -1671,7 +1664,6 @@ func (c *commandHandler) DiagnoseFiles(ctx context.Context, args command.Diagnos
var wg sync.WaitGroup
for snapshot := range snapshots {
- snapshot := snapshot
wg.Add(1)
go func() {
defer wg.Done()
@@ -1741,6 +1733,10 @@ func (c *commandHandler) PackageSymbols(ctx context.Context, args command.Packag
err := c.run(ctx, commandConfig{
forURI: args.URI,
}, func(ctx context.Context, deps commandDeps) error {
+ if deps.snapshot.FileKind(deps.fh) != file.Go {
+ // golang/vscode-go#3681: fail silently, to avoid spurious error popups.
+ return nil
+ }
res, err := golang.PackageSymbols(ctx, deps.snapshot, args.URI)
if err != nil {
return err
@@ -1760,3 +1756,88 @@ func (c *commandHandler) PackageSymbols(ctx context.Context, args command.Packag
return result, err
}
+
+// optionsStringToMap transforms comma-separated options of the form
+// "foo=bar,baz=quux" to a go map. Returns nil if any options are malformed.
+func optionsStringToMap(options string) (map[string][]string, error) {
+ optionsMap := make(map[string][]string)
+ for item := range strings.SplitSeq(options, ",") {
+ key, option, found := strings.Cut(item, "=")
+ if !found {
+ return nil, fmt.Errorf("invalid option %q", item)
+ }
+ optionsMap[key] = append(optionsMap[key], option)
+ }
+ return optionsMap, nil
+}
+
+func (c *commandHandler) ModifyTags(ctx context.Context, args command.ModifyTagsArgs) error {
+ return c.run(ctx, commandConfig{
+ progress: "Modifying tags",
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ m := &modifytags.Modification{
+ Clear: args.Clear,
+ ClearOptions: args.ClearOptions,
+ ValueFormat: args.ValueFormat,
+ Overwrite: args.Overwrite,
+ }
+
+ transform, err := parseTransform(args.Transform)
+ if err != nil {
+ return err
+ }
+ m.Transform = transform
+
+ if args.Add != "" {
+ m.Add = strings.Split(args.Add, ",")
+ }
+ if args.AddOptions != "" {
+ if options, err := optionsStringToMap(args.AddOptions); err != nil {
+ return err
+ } else {
+ m.AddOptions = options
+ }
+ }
+ if args.Remove != "" {
+ m.Remove = strings.Split(args.Remove, ",")
+ }
+ if args.RemoveOptions != "" {
+ if options, err := optionsStringToMap(args.RemoveOptions); err != nil {
+ return err
+ } else {
+ m.RemoveOptions = options
+ }
+ }
+ fh, err := deps.snapshot.ReadFile(ctx, args.URI)
+ if err != nil {
+ return err
+ }
+ changes, err := golang.ModifyTags(ctx, deps.snapshot, fh, args, m)
+ if err != nil {
+ return err
+ }
+ return applyChanges(ctx, c.s.client, changes)
+ })
+}
+
+func parseTransform(input string) (modifytags.Transform, error) {
+ switch input {
+ case "camelcase":
+ return modifytags.CamelCase, nil
+ case "lispcase":
+ return modifytags.LispCase, nil
+ case "pascalcase":
+ return modifytags.PascalCase, nil
+ case "titlecase":
+ return modifytags.TitleCase, nil
+ case "keep":
+ return modifytags.Keep, nil
+ case "":
+ fallthrough
+ case "snakecase":
+ return modifytags.SnakeCase, nil
+ default:
+ return modifytags.SnakeCase, fmt.Errorf("invalid Transform value")
+ }
+}
diff --git a/gopls/internal/server/completion.go b/gopls/internal/server/completion.go
index 6c185e93717..02604b2f710 100644
--- a/gopls/internal/server/completion.go
+++ b/gopls/internal/server/completion.go
@@ -27,7 +27,7 @@ func (s *server) Completion(ctx context.Context, params *protocol.CompletionPara
recordLatency(ctx, rerr)
}()
- ctx, done := event.Start(ctx, "lsp.Server.completion", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.Completion", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
@@ -102,6 +102,8 @@ func (s *server) saveLastCompletion(uri protocol.DocumentURI, version int32, ite
s.efficacyItems = items
}
+// toProtocolCompletionItems converts the candidates to the protocol completion items,
+// the candidates must be sorted based on score as it will be respected by client side.
func toProtocolCompletionItems(candidates []completion.CompletionItem, surrounding *completion.Selection, options *settings.Options) ([]protocol.CompletionItem, error) {
replaceRng, err := surrounding.Range()
if err != nil {
diff --git a/gopls/internal/server/definition.go b/gopls/internal/server/definition.go
index 7b4df3c7c07..8b9d42413be 100644
--- a/gopls/internal/server/definition.go
+++ b/gopls/internal/server/definition.go
@@ -9,6 +9,7 @@ import (
"fmt"
"golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/goasm"
"golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/label"
"golang.org/x/tools/gopls/internal/protocol"
@@ -23,7 +24,7 @@ func (s *server) Definition(ctx context.Context, params *protocol.DefinitionPara
recordLatency(ctx, rerr)
}()
- ctx, done := event.Start(ctx, "lsp.Server.definition", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.Definition", label.URI.Of(params.TextDocument.URI))
defer done()
// TODO(rfindley): definition requests should be multiplexed across all views.
@@ -37,13 +38,15 @@ func (s *server) Definition(ctx context.Context, params *protocol.DefinitionPara
return template.Definition(snapshot, fh, params.Position)
case file.Go:
return golang.Definition(ctx, snapshot, fh, params.Position)
+ case file.Asm:
+ return goasm.Definition(ctx, snapshot, fh, params.Position)
default:
return nil, fmt.Errorf("can't find definitions for file type %s", kind)
}
}
func (s *server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) {
- ctx, done := event.Start(ctx, "lsp.Server.typeDefinition", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.TypeDefinition", label.URI.Of(params.TextDocument.URI))
defer done()
// TODO(rfindley): type definition requests should be multiplexed across all views.
diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go
index b4e764b1233..dbffc58fd99 100644
--- a/gopls/internal/server/diagnostics.go
+++ b/gopls/internal/server/diagnostics.go
@@ -128,7 +128,6 @@ func (s *server) diagnoseChangedViews(ctx context.Context, modID uint64, lastCha
// Diagnose views concurrently.
var wg sync.WaitGroup
for _, v := range needsDiagnosis {
- v := v
snapshot, release, err := v.Snapshot()
if err != nil {
s.modificationMu.Lock()
@@ -200,7 +199,7 @@ func (s *server) diagnoseChangedViews(ctx context.Context, modID uint64, lastCha
// snapshot (or a subsequent snapshot in the same View) is eventually
// diagnosed.
func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot, changedURIs []protocol.DocumentURI, delay time.Duration) {
- ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", snapshot.Labels()...)
+ ctx, done := event.Start(ctx, "server.diagnoseSnapshot", snapshot.Labels()...)
defer done()
if delay > 0 {
@@ -241,7 +240,7 @@ func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot,
}
func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snapshot, uris []protocol.DocumentURI) (diagMap, error) {
- ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles", snapshot.Labels()...)
+ ctx, done := event.Start(ctx, "server.diagnoseChangedFiles", snapshot.Labels()...)
defer done()
toDiagnose := make(map[metadata.PackageID]*metadata.Package)
@@ -311,7 +310,7 @@ func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snaps
}
func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMap, error) {
- ctx, done := event.Start(ctx, "Server.diagnose", snapshot.Labels()...)
+ ctx, done := event.Start(ctx, "server.diagnose", snapshot.Labels()...)
defer done()
// Wait for a free diagnostics slot.
@@ -640,7 +639,7 @@ func (s *server) updateCriticalErrorStatus(ctx context.Context, snapshot *cache.
// updateDiagnostics records the result of diagnosing a snapshot, and publishes
// any diagnostics that need to be updated on the client.
func (s *server) updateDiagnostics(ctx context.Context, snapshot *cache.Snapshot, diagnostics diagMap, final bool) {
- ctx, done := event.Start(ctx, "Server.publishDiagnostics")
+ ctx, done := event.Start(ctx, "server.publishDiagnostics")
defer done()
s.diagnosticsMu.Lock()
diff --git a/gopls/internal/server/folding_range.go b/gopls/internal/server/folding_range.go
index 95b2ffc0744..5dbfd697db4 100644
--- a/gopls/internal/server/folding_range.go
+++ b/gopls/internal/server/folding_range.go
@@ -15,7 +15,7 @@ import (
)
func (s *server) FoldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
- ctx, done := event.Start(ctx, "lsp.Server.foldingRange", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.FoldingRange", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
@@ -26,24 +26,5 @@ func (s *server) FoldingRange(ctx context.Context, params *protocol.FoldingRange
if snapshot.FileKind(fh) != file.Go {
return nil, nil // empty result
}
- ranges, err := golang.FoldingRange(ctx, snapshot, fh, snapshot.Options().LineFoldingOnly)
- if err != nil {
- return nil, err
- }
- return toProtocolFoldingRanges(ranges)
-}
-
-func toProtocolFoldingRanges(ranges []*golang.FoldingRangeInfo) ([]protocol.FoldingRange, error) {
- result := make([]protocol.FoldingRange, 0, len(ranges))
- for _, info := range ranges {
- rng := info.Range
- result = append(result, protocol.FoldingRange{
- StartLine: rng.Start.Line,
- StartCharacter: rng.Start.Character,
- EndLine: rng.End.Line,
- EndCharacter: rng.End.Character,
- Kind: string(info.Kind),
- })
- }
- return result, nil
+ return golang.FoldingRange(ctx, snapshot, fh, snapshot.Options().LineFoldingOnly)
}
diff --git a/gopls/internal/server/format.go b/gopls/internal/server/format.go
index 1e6344dcff4..6abbb96d5b6 100644
--- a/gopls/internal/server/format.go
+++ b/gopls/internal/server/format.go
@@ -17,7 +17,7 @@ import (
)
func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
- ctx, done := event.Start(ctx, "lsp.Server.formatting", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.Formatting", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go
index 35614945f9d..6ce1f788dba 100644
--- a/gopls/internal/server/general.go
+++ b/gopls/internal/server/general.go
@@ -28,6 +28,7 @@ import (
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/semtok"
"golang.org/x/tools/gopls/internal/settings"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/goversion"
"golang.org/x/tools/gopls/internal/util/moremaps"
@@ -37,7 +38,7 @@ import (
)
func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
- ctx, done := event.Start(ctx, "lsp.Server.initialize")
+ ctx, done := event.Start(ctx, "server.Initialize")
defer done()
var clientName string
@@ -74,7 +75,11 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ
// TODO(rfindley): eliminate this defer.
defer func() { s.SetOptions(options) }()
- s.handleOptionErrors(ctx, options.Set(params.InitializationOptions))
+ // Process initialization options.
+ {
+ res, errs := options.Set(params.InitializationOptions)
+ s.handleOptionResult(ctx, res, errs)
+ }
options.ForClientCapabilities(params.ClientInfo, params.Capabilities)
if options.ShowBugReports {
@@ -99,7 +104,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ
}
s.pendingFolders = append(s.pendingFolders, folders...)
- var codeActionProvider interface{} = true
+ var codeActionProvider any = true
if ca := params.Capabilities.TextDocument.CodeAction; len(ca.CodeActionLiteralSupport.CodeActionKind.ValueSet) > 0 {
// If the client has specified CodeActionLiteralSupport,
// send the code actions we support.
@@ -121,7 +126,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ
}
}
- var renameOpts interface{} = true
+ var renameOpts any = true
if r := params.Capabilities.TextDocument.Rename; r != nil && r.PrepareSupport {
renameOpts = protocol.RenameOptions{
PrepareProvider: r.PrepareSupport,
@@ -179,11 +184,21 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ
IncludeText: false,
},
},
+ TypeHierarchyProvider: &protocol.Or_ServerCapabilities_typeHierarchyProvider{Value: true},
Workspace: &protocol.WorkspaceOptions{
WorkspaceFolders: &protocol.WorkspaceFolders5Gn{
Supported: true,
ChangeNotifications: "workspace/didChangeWorkspaceFolders",
},
+ FileOperations: &protocol.FileOperationOptions{
+ DidCreate: &protocol.FileOperationRegistrationOptions{
+ Filters: []protocol.FileOperationFilter{{
+ Scheme: "file",
+ // gopls is only interested with files in .go extension.
+ Pattern: protocol.FileOperationPattern{Glob: "**/*.go"},
+ }},
+ },
+ },
},
},
ServerInfo: &protocol.ServerInfo{
@@ -194,7 +209,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ
}
func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
- ctx, done := event.Start(ctx, "lsp.Server.initialized")
+ ctx, done := event.Start(ctx, "server.Initialized")
defer done()
s.stateMu.Lock()
@@ -298,10 +313,15 @@ func (s *server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol
// but the list can grow over time.
var filtered []protocol.WorkspaceFolder
for _, f := range folders {
- if _, err := protocol.ParseDocumentURI(f.URI); err != nil {
+ uri, err := protocol.ParseDocumentURI(f.URI)
+ if err != nil {
debuglog.Warning.Logf(ctx, "skip adding virtual folder %q - invalid folder URI: %v", f.Name, err)
continue
}
+ if s.session.HasView(uri) {
+ debuglog.Warning.Logf(ctx, "skip adding the already added folder %q - its view has been created before", f.Name)
+ continue
+ }
filtered = append(filtered, f)
}
folders = filtered
@@ -541,7 +561,8 @@ func (s *server) fetchFolderOptions(ctx context.Context, folder protocol.Documen
opts = opts.Clone()
for _, config := range configs {
- s.handleOptionErrors(ctx, opts.Set(config))
+ res, errs := opts.Set(config)
+ s.handleOptionResult(ctx, res, errs)
}
return opts, nil
}
@@ -555,7 +576,12 @@ func (s *server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMe
s.notifications = append(s.notifications, msg)
}
-func (s *server) handleOptionErrors(ctx context.Context, optionErrors []error) {
+func (s *server) handleOptionResult(ctx context.Context, applied []telemetry.CounterPath, optionErrors []error) {
+ for _, path := range applied {
+ path = append(settings.CounterPath{"gopls", "setting"}, path...)
+ counter.Inc(path.FullName())
+ }
+
var warnings, errs []string
for _, err := range optionErrors {
if err == nil {
@@ -615,7 +641,7 @@ func (s *server) fileOf(ctx context.Context, uri protocol.DocumentURI) (file.Han
// Shutdown implements the 'shutdown' LSP handler. It releases resources
// associated with the server and waits for all ongoing work to complete.
func (s *server) Shutdown(ctx context.Context) error {
- ctx, done := event.Start(ctx, "lsp.Server.shutdown")
+ ctx, done := event.Start(ctx, "server.Shutdown")
defer done()
s.stateMu.Lock()
@@ -642,7 +668,7 @@ func (s *server) Shutdown(ctx context.Context) error {
}
func (s *server) Exit(ctx context.Context) error {
- ctx, done := event.Start(ctx, "lsp.Server.exit")
+ ctx, done := event.Start(ctx, "server.Exit")
defer done()
s.stateMu.Lock()
diff --git a/gopls/internal/server/highlight.go b/gopls/internal/server/highlight.go
index 35ffc2db2f5..04ebbfa25ec 100644
--- a/gopls/internal/server/highlight.go
+++ b/gopls/internal/server/highlight.go
@@ -16,7 +16,7 @@ import (
)
func (s *server) DocumentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) {
- ctx, done := event.Start(ctx, "lsp.Server.documentHighlight", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.DocumentHighlight", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/hover.go b/gopls/internal/server/hover.go
index 80c35c09565..ed70ce493ba 100644
--- a/gopls/internal/server/hover.go
+++ b/gopls/internal/server/hover.go
@@ -25,7 +25,7 @@ func (s *server) Hover(ctx context.Context, params *protocol.HoverParams) (_ *pr
recordLatency(ctx, rerr)
}()
- ctx, done := event.Start(ctx, "lsp.Server.hover", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.Hover", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/implementation.go b/gopls/internal/server/implementation.go
index 9e61ebc4d88..9b2c103b2c3 100644
--- a/gopls/internal/server/implementation.go
+++ b/gopls/internal/server/implementation.go
@@ -21,7 +21,7 @@ func (s *server) Implementation(ctx context.Context, params *protocol.Implementa
recordLatency(ctx, rerr)
}()
- ctx, done := event.Start(ctx, "lsp.Server.implementation", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.Implementation", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/inlay_hint.go b/gopls/internal/server/inlay_hint.go
index fca8bcbc1c8..a11ab4c313a 100644
--- a/gopls/internal/server/inlay_hint.go
+++ b/gopls/internal/server/inlay_hint.go
@@ -16,7 +16,7 @@ import (
)
func (s *server) InlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) {
- ctx, done := event.Start(ctx, "lsp.Server.inlayHint", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.InlayHint", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/link.go b/gopls/internal/server/link.go
index 13097d89887..75c717dbe8e 100644
--- a/gopls/internal/server/link.go
+++ b/gopls/internal/server/link.go
@@ -11,11 +11,13 @@ import (
"go/ast"
"go/token"
"net/url"
+ "path/filepath"
"regexp"
"strings"
"sync"
"golang.org/x/mod/modfile"
+ "golang.org/x/mod/module"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/cache/parsego"
@@ -29,7 +31,7 @@ import (
)
func (s *server) DocumentLink(ctx context.Context, params *protocol.DocumentLinkParams) (links []protocol.DocumentLink, err error) {
- ctx, done := event.Start(ctx, "lsp.Server.documentLink")
+ ctx, done := event.Start(ctx, "server.DocumentLink")
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
@@ -59,6 +61,30 @@ func modLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]
}
var links []protocol.DocumentLink
+ for _, rep := range pm.File.Replace {
+ if modfile.IsDirectoryPath(rep.New.Path) {
+ // Have local replacement, such as 'replace A => ../'.
+ dep := []byte(rep.New.Path)
+ start, end := rep.Syntax.Start.Byte, rep.Syntax.End.Byte
+ i := bytes.Index(pm.Mapper.Content[start:end], dep)
+ if i < 0 {
+ continue
+ }
+ path := rep.New.Path
+ if !filepath.IsAbs(path) {
+ path = filepath.Join(fh.URI().DirPath(), path)
+ }
+ // jump to the go.mod file of replaced module.
+ path = filepath.Join(filepath.Clean(path), "go.mod")
+ l, err := toProtocolLink(pm.Mapper, protocol.URIFromPath(path).Path(), start+i, start+i+len(dep))
+ if err != nil {
+ return nil, err
+ }
+ links = append(links, l)
+ continue
+ }
+ }
+
for _, req := range pm.File.Require {
if req.Syntax == nil {
continue
@@ -73,9 +99,21 @@ func modLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]
if i == -1 {
continue
}
+
+ mod := req.Mod
+ // respect the repalcement when constructing a module link.
+ if m, ok := pm.ReplaceMap[req.Mod]; ok {
+ // Have: 'replace A v1.2.3 => A vx.x.x' or 'replace A v1.2.3 => B vx.x.x'.
+ mod = m
+ } else if m, ok := pm.ReplaceMap[module.Version{Path: req.Mod.Path}]; ok &&
+ !modfile.IsDirectoryPath(m.Path) { // exclude local replacement.
+ // Have: 'replace A => A vx.x.x' or 'replace A => B vx.x.x'.
+ mod = m
+ }
+
// Shift the start position to the location of the
// dependency within the require statement.
- target := cache.BuildLink(snapshot.Options().LinkTarget, "mod/"+req.Mod.String(), "")
+ target := cache.BuildLink(snapshot.Options().LinkTarget, "mod/"+mod.String(), "")
l, err := toProtocolLink(pm.Mapper, target, start+i, start+i+len(dep))
if err != nil {
return nil, err
@@ -142,8 +180,8 @@ func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]p
urlPath := string(importPath)
// For pkg.go.dev, append module version suffix to package import path.
- if mp := snapshot.Metadata(depsByImpPath[importPath]); mp != nil && mp.Module != nil && mp.Module.Path != "" && mp.Module.Version != "" {
- urlPath = strings.Replace(urlPath, mp.Module.Path, mp.Module.Path+"@"+mp.Module.Version, 1)
+ if mp := snapshot.Metadata(depsByImpPath[importPath]); mp != nil && mp.Module != nil && cache.ResolvedPath(mp.Module) != "" && cache.ResolvedVersion(mp.Module) != "" {
+ urlPath = strings.Replace(urlPath, mp.Module.Path, cache.ResolvedString(mp.Module), 1)
}
start, end, err := safetoken.Offsets(pgf.Tok, imp.Path.Pos(), imp.Path.End())
@@ -164,17 +202,15 @@ func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]p
// Gather links found in string literals.
var str []*ast.BasicLit
- ast.Inspect(pgf.File, func(node ast.Node) bool {
- switch n := node.(type) {
- case *ast.ImportSpec:
- return false // don't process import strings again
- case *ast.BasicLit:
- if n.Kind == token.STRING {
- str = append(str, n)
+ for curLit := range pgf.Cursor.Preorder((*ast.BasicLit)(nil)) {
+ lit := curLit.Node().(*ast.BasicLit)
+ if lit.Kind == token.STRING {
+ if _, ok := curLit.Parent().Node().(*ast.ImportSpec); ok {
+ continue // ignore import strings
}
+ str = append(str, lit)
}
- return true
- })
+ }
for _, s := range str {
strOffset, err := safetoken.Offset(pgf.Tok, s.Pos())
if err != nil {
@@ -213,7 +249,7 @@ var acceptedSchemes = map[string]bool{
"https": true,
}
-// urlRegexp is the user-supplied regular expression to match URL.
+// findLinksInString is the user-supplied regular expression to match URL.
// srcOffset is the start offset of 'src' within m's file.
func findLinksInString(urlRegexp *regexp.Regexp, src string, srcOffset int, m *protocol.Mapper) ([]protocol.DocumentLink, error) {
var links []protocol.DocumentLink
diff --git a/gopls/internal/server/prompt.go b/gopls/internal/server/prompt.go
index 37f591487a6..f8895358942 100644
--- a/gopls/internal/server/prompt.go
+++ b/gopls/internal/server/prompt.go
@@ -283,7 +283,7 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
attempts++
}
- pendingContent := []byte(fmt.Sprintf("%s %d %d %d", state, attempts, creationTime, token))
+ pendingContent := fmt.Appendf(nil, "%s %d %d %d", state, attempts, creationTime, token)
if err := os.WriteFile(promptFile, pendingContent, 0666); err != nil {
errorf("writing pending state: %v", err)
return
@@ -351,7 +351,7 @@ Would you like to enable Go telemetry?
message(protocol.Error, fmt.Sprintf("Unrecognized response %q", item.Title))
}
}
- resultContent := []byte(fmt.Sprintf("%s %d %d %d", result, attempts, creationTime, token))
+ resultContent := fmt.Appendf(nil, "%s %d %d %d", result, attempts, creationTime, token)
if err := os.WriteFile(promptFile, resultContent, 0666); err != nil {
errorf("error writing result state to prompt file: %v", err)
}
diff --git a/gopls/internal/server/prompt_test.go b/gopls/internal/server/prompt_test.go
index f4484cb6437..6af5b98eab7 100644
--- a/gopls/internal/server/prompt_test.go
+++ b/gopls/internal/server/prompt_test.go
@@ -27,7 +27,6 @@ func TestAcquireFileLock(t *testing.T) {
var wg sync.WaitGroup
for i := range releasers {
- i := i
wg.Add(1)
go func() {
defer wg.Done()
diff --git a/gopls/internal/server/references.go b/gopls/internal/server/references.go
index f5019693946..8a01e96498b 100644
--- a/gopls/internal/server/references.go
+++ b/gopls/internal/server/references.go
@@ -22,7 +22,7 @@ func (s *server) References(ctx context.Context, params *protocol.ReferenceParam
recordLatency(ctx, rerr)
}()
- ctx, done := event.Start(ctx, "lsp.Server.references", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.References", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/rename.go b/gopls/internal/server/rename.go
index b6fac8ba219..218740bd679 100644
--- a/gopls/internal/server/rename.go
+++ b/gopls/internal/server/rename.go
@@ -17,7 +17,7 @@ import (
)
func (s *server) Rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) {
- ctx, done := event.Start(ctx, "lsp.Server.rename", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.Rename", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
@@ -68,7 +68,7 @@ func (s *server) Rename(ctx context.Context, params *protocol.RenameParams) (*pr
// TODO(rfindley): why wouldn't we want to show an error to the user, if the
// user initiated a rename request at the cursor?
func (s *server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.PrepareRenamePlaceholder, error) {
- ctx, done := event.Start(ctx, "lsp.Server.prepareRename", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.PrepareRename", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/selection_range.go b/gopls/internal/server/selection_range.go
index 484e1cf67ab..afc878b1544 100644
--- a/gopls/internal/server/selection_range.go
+++ b/gopls/internal/server/selection_range.go
@@ -27,7 +27,7 @@ import (
// returned for each cursor to avoid multiple round-trips when the user is
// likely to issue this command multiple times in quick succession.
func (s *server) SelectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) {
- ctx, done := event.Start(ctx, "lsp.Server.selectionRange")
+ ctx, done := event.Start(ctx, "server.SelectionRange")
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/semantic.go b/gopls/internal/server/semantic.go
index f746593a3dd..f0a2e11dd98 100644
--- a/gopls/internal/server/semantic.go
+++ b/gopls/internal/server/semantic.go
@@ -24,7 +24,7 @@ func (s *server) SemanticTokensRange(ctx context.Context, params *protocol.Seman
}
func (s *server) semanticTokens(ctx context.Context, td protocol.TextDocumentIdentifier, rng *protocol.Range) (*protocol.SemanticTokens, error) {
- ctx, done := event.Start(ctx, "lsp.Server.semanticTokens", label.URI.Of(td.URI))
+ ctx, done := event.Start(ctx, "server.semanticTokens", label.URI.Of(td.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, td.URI)
diff --git a/gopls/internal/server/server.go b/gopls/internal/server/server.go
index d9090250a66..c22e8f19750 100644
--- a/gopls/internal/server/server.go
+++ b/gopls/internal/server/server.go
@@ -181,7 +181,7 @@ type server struct {
}
func (s *server) WorkDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error {
- ctx, done := event.Start(ctx, "lsp.Server.workDoneProgressCancel")
+ ctx, done := event.Start(ctx, "server.WorkDoneProgressCancel")
defer done()
return s.progress.Cancel(params.Token)
@@ -447,12 +447,7 @@ func (s *server) initWeb() (*web, error) {
pkg := pkgs[0]
// Produce report.
- html, err := golang.AssemblyHTML(ctx, snapshot, pkg, symbol, web)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Write(html)
+ golang.AssemblyHTML(ctx, snapshot, w, pkg, symbol, web)
})
return web, nil
diff --git a/gopls/internal/server/signature_help.go b/gopls/internal/server/signature_help.go
index addcfe1e262..eb464c48e27 100644
--- a/gopls/internal/server/signature_help.go
+++ b/gopls/internal/server/signature_help.go
@@ -15,7 +15,7 @@ import (
)
func (s *server) SignatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) {
- ctx, done := event.Start(ctx, "lsp.Server.signatureHelp", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.SignatureHelp", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/symbols.go b/gopls/internal/server/symbols.go
index e35b2c75451..40df7369f51 100644
--- a/gopls/internal/server/symbols.go
+++ b/gopls/internal/server/symbols.go
@@ -16,7 +16,7 @@ import (
)
func (s *server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]any, error) {
- ctx, done := event.Start(ctx, "lsp.Server.documentSymbol", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.DocumentSymbol", label.URI.Of(params.TextDocument.URI))
defer done()
fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
diff --git a/gopls/internal/server/text_synchronization.go b/gopls/internal/server/text_synchronization.go
index ad1266d783e..ad8554d9302 100644
--- a/gopls/internal/server/text_synchronization.go
+++ b/gopls/internal/server/text_synchronization.go
@@ -92,7 +92,7 @@ func (m ModificationSource) String() string {
}
func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
- ctx, done := event.Start(ctx, "lsp.Server.didOpen", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.DidOpen", label.URI.Of(params.TextDocument.URI))
defer done()
uri := params.TextDocument.URI
@@ -121,7 +121,7 @@ func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocume
}
func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
- ctx, done := event.Start(ctx, "lsp.Server.didChange", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.DidChange", label.URI.Of(params.TextDocument.URI))
defer done()
uri := params.TextDocument.URI
@@ -174,7 +174,7 @@ func (s *server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri proto
}
func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
- ctx, done := event.Start(ctx, "lsp.Server.didChangeWatchedFiles")
+ ctx, done := event.Start(ctx, "server.DidChangeWatchedFiles")
defer done()
var modifications []file.Modification
@@ -190,7 +190,7 @@ func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.Did
}
func (s *server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
- ctx, done := event.Start(ctx, "lsp.Server.didSave", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.DidSave", label.URI.Of(params.TextDocument.URI))
defer done()
c := file.Modification{
@@ -204,7 +204,7 @@ func (s *server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocume
}
func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
- ctx, done := event.Start(ctx, "lsp.Server.didClose", label.URI.Of(params.TextDocument.URI))
+ ctx, done := event.Start(ctx, "server.DidClose", label.URI.Of(params.TextDocument.URI))
defer done()
return s.didModifyFiles(ctx, []file.Modification{
diff --git a/gopls/internal/server/type_hierarchy.go b/gopls/internal/server/type_hierarchy.go
new file mode 100644
index 00000000000..5f40ed3c0c2
--- /dev/null
+++ b/gopls/internal/server/type_hierarchy.go
@@ -0,0 +1,63 @@
+// Copyright 2025 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 server
+
+import (
+ "context"
+ "fmt"
+
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/internal/event"
+)
+
+func (s *server) PrepareTypeHierarchy(ctx context.Context, params *protocol.TypeHierarchyPrepareParams) ([]protocol.TypeHierarchyItem, error) {
+ ctx, done := event.Start(ctx, "server.PrepareTypeHierarchy")
+ defer done()
+
+ fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI)
+ if err != nil {
+ return nil, err
+ }
+ defer release()
+ switch snapshot.FileKind(fh) {
+ case file.Go:
+ return golang.PrepareTypeHierarchy(ctx, snapshot, fh, params.Position)
+ }
+ return nil, fmt.Errorf("unsupported file type: %v", fh)
+}
+
+func (s *server) Subtypes(ctx context.Context, params *protocol.TypeHierarchySubtypesParams) ([]protocol.TypeHierarchyItem, error) {
+ ctx, done := event.Start(ctx, "server.Subtypes")
+ defer done()
+
+ fh, snapshot, release, err := s.fileOf(ctx, params.Item.URI)
+ if err != nil {
+ return nil, err
+ }
+ defer release()
+ switch snapshot.FileKind(fh) {
+ case file.Go:
+ return golang.Subtypes(ctx, snapshot, fh, params.Item)
+ }
+ return nil, fmt.Errorf("unsupported file type: %v", fh)
+}
+
+func (s *server) Supertypes(ctx context.Context, params *protocol.TypeHierarchySupertypesParams) ([]protocol.TypeHierarchyItem, error) {
+ ctx, done := event.Start(ctx, "server.Supertypes")
+ defer done()
+
+ fh, snapshot, release, err := s.fileOf(ctx, params.Item.URI)
+ if err != nil {
+ return nil, err
+ }
+ defer release()
+ switch snapshot.FileKind(fh) {
+ case file.Go:
+ return golang.Supertypes(ctx, snapshot, fh, params.Item)
+ }
+ return nil, fmt.Errorf("unsupported file type: %v", fh)
+}
diff --git a/gopls/internal/server/unimplemented.go b/gopls/internal/server/unimplemented.go
index 470a7cbb0ee..bd12b25f610 100644
--- a/gopls/internal/server/unimplemented.go
+++ b/gopls/internal/server/unimplemented.go
@@ -34,10 +34,6 @@ func (s *server) DidCloseNotebookDocument(context.Context, *protocol.DidCloseNot
return notImplemented("DidCloseNotebookDocument")
}
-func (s *server) DidCreateFiles(context.Context, *protocol.CreateFilesParams) error {
- return notImplemented("DidCreateFiles")
-}
-
func (s *server) DidDeleteFiles(context.Context, *protocol.DeleteFilesParams) error {
return notImplemented("DidDeleteFiles")
}
@@ -78,10 +74,6 @@ func (s *server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeForma
return nil, notImplemented("OnTypeFormatting")
}
-func (s *server) PrepareTypeHierarchy(context.Context, *protocol.TypeHierarchyPrepareParams) ([]protocol.TypeHierarchyItem, error) {
- return nil, notImplemented("PrepareTypeHierarchy")
-}
-
func (s *server) Progress(context.Context, *protocol.ProgressParams) error {
return notImplemented("Progress")
}
@@ -114,7 +106,7 @@ func (s *server) ResolveWorkspaceSymbol(context.Context, *protocol.WorkspaceSymb
return nil, notImplemented("ResolveWorkspaceSymbol")
}
-func (s *server) SemanticTokensFullDelta(context.Context, *protocol.SemanticTokensDeltaParams) (interface{}, error) {
+func (s *server) SemanticTokensFullDelta(context.Context, *protocol.SemanticTokensDeltaParams) (any, error) {
return nil, notImplemented("SemanticTokensFullDelta")
}
@@ -122,14 +114,6 @@ func (s *server) SetTrace(context.Context, *protocol.SetTraceParams) error {
return notImplemented("SetTrace")
}
-func (s *server) Subtypes(context.Context, *protocol.TypeHierarchySubtypesParams) ([]protocol.TypeHierarchyItem, error) {
- return nil, notImplemented("Subtypes")
-}
-
-func (s *server) Supertypes(context.Context, *protocol.TypeHierarchySupertypesParams) ([]protocol.TypeHierarchyItem, error) {
- return nil, notImplemented("Supertypes")
-}
-
func (s *server) WillCreateFiles(context.Context, *protocol.CreateFilesParams) (*protocol.WorkspaceEdit, error) {
return nil, notImplemented("WillCreateFiles")
}
diff --git a/gopls/internal/server/workspace.go b/gopls/internal/server/workspace.go
index 84e663c1049..ced5656c6ac 100644
--- a/gopls/internal/server/workspace.go
+++ b/gopls/internal/server/workspace.go
@@ -12,6 +12,8 @@ import (
"sync"
"golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/golang/completion"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/event"
@@ -59,7 +61,7 @@ func (s *server) addView(ctx context.Context, name string, dir protocol.Document
}
func (s *server) DidChangeConfiguration(ctx context.Context, _ *protocol.DidChangeConfigurationParams) error {
- ctx, done := event.Start(ctx, "lsp.Server.didChangeConfiguration")
+ ctx, done := event.Start(ctx, "server.DidChangeConfiguration")
defer done()
var wg sync.WaitGroup
@@ -139,3 +141,31 @@ func (s *server) DidChangeConfiguration(ctx context.Context, _ *protocol.DidChan
return nil
}
+
+func (s *server) DidCreateFiles(ctx context.Context, params *protocol.CreateFilesParams) error {
+ ctx, done := event.Start(ctx, "server.DidCreateFiles")
+ defer done()
+
+ var allChanges []protocol.DocumentChange
+ for _, createdFile := range params.Files {
+ uri := protocol.DocumentURI(createdFile.URI)
+ fh, snapshot, release, err := s.fileOf(ctx, uri)
+ if err != nil {
+ event.Error(ctx, "fail to call fileOf", err)
+ continue
+ }
+ defer release()
+
+ switch snapshot.FileKind(fh) {
+ case file.Go:
+ change, err := completion.NewFile(ctx, snapshot, fh)
+ if err != nil {
+ continue
+ }
+ allChanges = append(allChanges, *change)
+ default:
+ }
+ }
+
+ return applyChanges(ctx, s.client, allChanges)
+}
diff --git a/gopls/internal/server/workspace_symbol.go b/gopls/internal/server/workspace_symbol.go
index 9eafeb015ad..f34e76f7937 100644
--- a/gopls/internal/server/workspace_symbol.go
+++ b/gopls/internal/server/workspace_symbol.go
@@ -20,7 +20,7 @@ func (s *server) Symbol(ctx context.Context, params *protocol.WorkspaceSymbolPar
recordLatency(ctx, rerr)
}()
- ctx, done := event.Start(ctx, "lsp.Server.symbol")
+ ctx, done := event.Start(ctx, "server.Symbol")
defer done()
views := s.session.Views()
diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go
index 5ba8bdd06b0..99b55cc6b24 100644
--- a/gopls/internal/settings/analysis.go
+++ b/gopls/internal/settings/analysis.go
@@ -5,6 +5,8 @@
package settings
import (
+ "slices"
+
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/appends"
"golang.org/x/tools/go/analysis/passes/asmdecl"
@@ -21,6 +23,7 @@ import (
"golang.org/x/tools/go/analysis/passes/directive"
"golang.org/x/tools/go/analysis/passes/errorsas"
"golang.org/x/tools/go/analysis/passes/framepointer"
+ "golang.org/x/tools/go/analysis/passes/hostport"
"golang.org/x/tools/go/analysis/passes/httpresponse"
"golang.org/x/tools/go/analysis/passes/ifaceassert"
"golang.org/x/tools/go/analysis/passes/loopclosure"
@@ -49,8 +52,6 @@ import (
"golang.org/x/tools/gopls/internal/analysis/deprecated"
"golang.org/x/tools/gopls/internal/analysis/embeddirective"
"golang.org/x/tools/gopls/internal/analysis/fillreturns"
- "golang.org/x/tools/gopls/internal/analysis/gofix"
- "golang.org/x/tools/gopls/internal/analysis/hostport"
"golang.org/x/tools/gopls/internal/analysis/infertypeargs"
"golang.org/x/tools/gopls/internal/analysis/modernize"
"golang.org/x/tools/gopls/internal/analysis/nonewvars"
@@ -63,14 +64,19 @@ import (
"golang.org/x/tools/gopls/internal/analysis/unusedvariable"
"golang.org/x/tools/gopls/internal/analysis/yield"
"golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/internal/gofix"
+ "honnef.co/go/tools/analysis/lint"
)
-// Analyzer augments a [analysis.Analyzer] with additional LSP configuration.
+var AllAnalyzers = slices.Concat(DefaultAnalyzers, StaticcheckAnalyzers)
+
+// Analyzer augments an [analysis.Analyzer] with additional LSP configuration.
//
// Analyzers are immutable, since they are shared across multiple LSP sessions.
type Analyzer struct {
analyzer *analysis.Analyzer
- nonDefault bool
+ staticcheck *lint.RawDocumentation // only for staticcheck analyzers
+ nonDefault bool // (sense is negated so we can mostly omit it)
actionKinds []protocol.CodeActionKind
severity protocol.DiagnosticSeverity
tags []protocol.DiagnosticTag
@@ -79,9 +85,28 @@ type Analyzer struct {
// Analyzer returns the [analysis.Analyzer] that this Analyzer wraps.
func (a *Analyzer) Analyzer() *analysis.Analyzer { return a.analyzer }
-// EnabledByDefault reports whether the analyzer is enabled by default for all sessions.
+// Enabled reports whether the analyzer is enabled by the options.
// This value can be configured per-analysis in user settings.
-func (a *Analyzer) EnabledByDefault() bool { return !a.nonDefault }
+func (a *Analyzer) Enabled(o *Options) bool {
+ // An explicit setting by name takes precedence.
+ if v, found := o.Analyses[a.Analyzer().Name]; found {
+ return v
+ }
+ if a.staticcheck != nil {
+ // An explicit staticcheck={true,false} setting
+ // enables/disables all staticcheck analyzers.
+ if o.StaticcheckProvided {
+ return o.Staticcheck
+ }
+ // Respect staticcheck's off-by-default options too.
+ // (This applies to only a handful of analyzers.)
+ if a.staticcheck.NonDefault {
+ return false
+ }
+ }
+ // Respect gopls' default setting.
+ return !a.nonDefault
+}
// ActionKinds is the set of kinds of code action this analyzer produces.
//
@@ -126,108 +151,105 @@ func (a *Analyzer) Tags() []protocol.DiagnosticTag { return a.tags }
// String returns the name of this analyzer.
func (a *Analyzer) String() string { return a.analyzer.String() }
-// DefaultAnalyzers holds the set of Analyzers available to all gopls sessions,
-// independent of build version, keyed by analyzer name.
-//
-// It is the source from which gopls/doc/analyzers.md is generated.
-var DefaultAnalyzers = make(map[string]*Analyzer) // initialized below
-
-func init() {
+// DefaultAnalyzers holds the list of Analyzers available to all gopls
+// sessions, independent of build version. It is the source from which
+// gopls/doc/analyzers.md is generated.
+var DefaultAnalyzers = []*Analyzer{
// See [Analyzer.Severity] for guidance on setting analyzer severity below.
- analyzers := []*Analyzer{
- // The traditional vet suite:
- {analyzer: appends.Analyzer},
- {analyzer: asmdecl.Analyzer},
- {analyzer: assign.Analyzer},
- {analyzer: atomic.Analyzer},
- {analyzer: bools.Analyzer},
- {analyzer: buildtag.Analyzer},
- {analyzer: cgocall.Analyzer},
- {analyzer: composite.Analyzer},
- {analyzer: copylock.Analyzer},
- {analyzer: defers.Analyzer},
- {analyzer: deprecated.Analyzer, severity: protocol.SeverityHint, tags: []protocol.DiagnosticTag{protocol.Deprecated}},
- {analyzer: directive.Analyzer},
- {analyzer: errorsas.Analyzer},
- {analyzer: framepointer.Analyzer},
- {analyzer: httpresponse.Analyzer},
- {analyzer: ifaceassert.Analyzer},
- {analyzer: loopclosure.Analyzer},
- {analyzer: lostcancel.Analyzer},
- {analyzer: nilfunc.Analyzer},
- {analyzer: printf.Analyzer},
- {analyzer: shift.Analyzer},
- {analyzer: sigchanyzer.Analyzer},
- {analyzer: slog.Analyzer},
- {analyzer: stdmethods.Analyzer},
- {analyzer: stdversion.Analyzer},
- {analyzer: stringintconv.Analyzer},
- {analyzer: structtag.Analyzer},
- {analyzer: testinggoroutine.Analyzer},
- {analyzer: tests.Analyzer},
- {analyzer: timeformat.Analyzer},
- {analyzer: unmarshal.Analyzer},
- {analyzer: unreachable.Analyzer},
- {analyzer: unsafeptr.Analyzer},
- {analyzer: unusedresult.Analyzer},
-
- // not suitable for vet:
- // - some (nilness, yield) use go/ssa; see #59714.
- // - others don't meet the "frequency" criterion;
- // see GOROOT/src/cmd/vet/README.
- {analyzer: atomicalign.Analyzer},
- {analyzer: deepequalerrors.Analyzer},
- {analyzer: nilness.Analyzer}, // uses go/ssa
- {analyzer: yield.Analyzer}, // uses go/ssa
- {analyzer: sortslice.Analyzer},
- {analyzer: embeddirective.Analyzer},
- {analyzer: waitgroup.Analyzer}, // to appear in cmd/vet@go1.25
- {analyzer: hostport.Analyzer}, // to appear in cmd/vet@go1.25
-
- // disabled due to high false positives
- {analyzer: shadow.Analyzer, nonDefault: true}, // very noisy
- // fieldalignment is not even off-by-default; see #67762.
-
- // simplifiers and modernizers
- //
- // These analyzers offer mere style fixes on correct code,
- // thus they will never appear in cmd/vet and
- // their severity level is "information".
- //
- // gofmt -s suite
- {
- analyzer: simplifycompositelit.Analyzer,
- actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
- severity: protocol.SeverityInformation,
- },
- {
- analyzer: simplifyrange.Analyzer,
- actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
- severity: protocol.SeverityInformation,
- },
- {
- analyzer: simplifyslice.Analyzer,
- actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
- severity: protocol.SeverityInformation,
- },
- // other simplifiers
- {analyzer: gofix.Analyzer, severity: protocol.SeverityHint},
- {analyzer: infertypeargs.Analyzer, severity: protocol.SeverityInformation},
- {analyzer: unusedparams.Analyzer, severity: protocol.SeverityInformation},
- {analyzer: unusedfunc.Analyzer, severity: protocol.SeverityInformation},
- {analyzer: unusedwrite.Analyzer, severity: protocol.SeverityInformation}, // uses go/ssa
- {analyzer: modernize.Analyzer, severity: protocol.SeverityHint},
-
- // type-error analyzers
- // These analyzers enrich go/types errors with suggested fixes.
- // Since they exist only to attach their fixes to type errors, their
- // severity is irrelevant.
- {analyzer: fillreturns.Analyzer},
- {analyzer: nonewvars.Analyzer},
- {analyzer: noresultvalues.Analyzer},
- {analyzer: unusedvariable.Analyzer},
- }
- for _, analyzer := range analyzers {
- DefaultAnalyzers[analyzer.analyzer.Name] = analyzer
- }
+
+ // The traditional vet suite:
+ {analyzer: appends.Analyzer},
+ {analyzer: asmdecl.Analyzer},
+ {analyzer: assign.Analyzer},
+ {analyzer: atomic.Analyzer},
+ {analyzer: bools.Analyzer},
+ {analyzer: buildtag.Analyzer},
+ {analyzer: cgocall.Analyzer},
+ {analyzer: composite.Analyzer},
+ {analyzer: copylock.Analyzer},
+ {analyzer: defers.Analyzer},
+ {
+ analyzer: deprecated.Analyzer,
+ severity: protocol.SeverityHint,
+ tags: []protocol.DiagnosticTag{protocol.Deprecated},
+ },
+ {analyzer: directive.Analyzer},
+ {analyzer: errorsas.Analyzer},
+ {analyzer: framepointer.Analyzer},
+ {analyzer: httpresponse.Analyzer},
+ {analyzer: ifaceassert.Analyzer},
+ {analyzer: loopclosure.Analyzer},
+ {analyzer: lostcancel.Analyzer},
+ {analyzer: nilfunc.Analyzer},
+ {analyzer: printf.Analyzer},
+ {analyzer: shift.Analyzer},
+ {analyzer: sigchanyzer.Analyzer},
+ {analyzer: slog.Analyzer},
+ {analyzer: stdmethods.Analyzer},
+ {analyzer: stdversion.Analyzer},
+ {analyzer: stringintconv.Analyzer},
+ {analyzer: structtag.Analyzer},
+ {analyzer: testinggoroutine.Analyzer},
+ {analyzer: tests.Analyzer},
+ {analyzer: timeformat.Analyzer},
+ {analyzer: unmarshal.Analyzer},
+ {analyzer: unreachable.Analyzer},
+ {analyzer: unsafeptr.Analyzer},
+ {analyzer: unusedresult.Analyzer},
+
+ // not suitable for vet:
+ // - some (nilness, yield) use go/ssa; see #59714.
+ // - others don't meet the "frequency" criterion;
+ // see GOROOT/src/cmd/vet/README.
+ {analyzer: atomicalign.Analyzer},
+ {analyzer: deepequalerrors.Analyzer},
+ {analyzer: nilness.Analyzer}, // uses go/ssa
+ {analyzer: yield.Analyzer}, // uses go/ssa
+ {analyzer: sortslice.Analyzer},
+ {analyzer: embeddirective.Analyzer},
+ {analyzer: waitgroup.Analyzer}, // to appear in cmd/vet@go1.25
+ {analyzer: hostport.Analyzer}, // to appear in cmd/vet@go1.25
+
+ // disabled due to high false positives
+ {analyzer: shadow.Analyzer, nonDefault: true}, // very noisy
+ // fieldalignment is not even off-by-default; see #67762.
+
+ // simplifiers and modernizers
+ //
+ // These analyzers offer mere style fixes on correct code,
+ // thus they will never appear in cmd/vet and
+ // their severity level is "information".
+ //
+ // gofmt -s suite
+ {
+ analyzer: simplifycompositelit.Analyzer,
+ actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
+ severity: protocol.SeverityInformation,
+ },
+ {
+ analyzer: simplifyrange.Analyzer,
+ actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
+ severity: protocol.SeverityInformation,
+ },
+ {
+ analyzer: simplifyslice.Analyzer,
+ actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
+ severity: protocol.SeverityInformation,
+ },
+ // other simplifiers
+ {analyzer: gofix.Analyzer, severity: protocol.SeverityHint},
+ {analyzer: infertypeargs.Analyzer, severity: protocol.SeverityInformation},
+ {analyzer: unusedparams.Analyzer, severity: protocol.SeverityInformation},
+ {analyzer: unusedfunc.Analyzer, severity: protocol.SeverityInformation},
+ {analyzer: unusedwrite.Analyzer, severity: protocol.SeverityInformation}, // uses go/ssa
+ {analyzer: modernize.Analyzer, severity: protocol.SeverityHint},
+
+ // type-error analyzers
+ // These analyzers enrich go/types errors with suggested fixes.
+ // Since they exist only to attach their fixes to type errors, their
+ // severity is irrelevant.
+ {analyzer: fillreturns.Analyzer},
+ {analyzer: nonewvars.Analyzer},
+ {analyzer: noresultvalues.Analyzer},
+ {analyzer: unusedvariable.Analyzer},
}
diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go
index fcce7cd2682..ebe9606adab 100644
--- a/gopls/internal/settings/codeactionkind.go
+++ b/gopls/internal/settings/codeactionkind.go
@@ -81,20 +81,24 @@ const (
GoTest protocol.CodeActionKind = "source.test"
GoToggleCompilerOptDetails protocol.CodeActionKind = "source.toggleCompilerOptDetails"
AddTest protocol.CodeActionKind = "source.addTest"
+ OrganizeImports protocol.CodeActionKind = "source.organizeImports"
// gopls
GoplsDocFeatures protocol.CodeActionKind = "gopls.doc.features"
// refactor.rewrite
- RefactorRewriteChangeQuote protocol.CodeActionKind = "refactor.rewrite.changeQuote"
- RefactorRewriteFillStruct protocol.CodeActionKind = "refactor.rewrite.fillStruct"
- RefactorRewriteFillSwitch protocol.CodeActionKind = "refactor.rewrite.fillSwitch"
- RefactorRewriteInvertIf protocol.CodeActionKind = "refactor.rewrite.invertIf"
- RefactorRewriteJoinLines protocol.CodeActionKind = "refactor.rewrite.joinLines"
- RefactorRewriteRemoveUnusedParam protocol.CodeActionKind = "refactor.rewrite.removeUnusedParam"
- RefactorRewriteMoveParamLeft protocol.CodeActionKind = "refactor.rewrite.moveParamLeft"
- RefactorRewriteMoveParamRight protocol.CodeActionKind = "refactor.rewrite.moveParamRight"
- RefactorRewriteSplitLines protocol.CodeActionKind = "refactor.rewrite.splitLines"
+ RefactorRewriteChangeQuote protocol.CodeActionKind = "refactor.rewrite.changeQuote"
+ RefactorRewriteFillStruct protocol.CodeActionKind = "refactor.rewrite.fillStruct"
+ RefactorRewriteFillSwitch protocol.CodeActionKind = "refactor.rewrite.fillSwitch"
+ RefactorRewriteInvertIf protocol.CodeActionKind = "refactor.rewrite.invertIf"
+ RefactorRewriteJoinLines protocol.CodeActionKind = "refactor.rewrite.joinLines"
+ RefactorRewriteRemoveUnusedParam protocol.CodeActionKind = "refactor.rewrite.removeUnusedParam"
+ RefactorRewriteMoveParamLeft protocol.CodeActionKind = "refactor.rewrite.moveParamLeft"
+ RefactorRewriteMoveParamRight protocol.CodeActionKind = "refactor.rewrite.moveParamRight"
+ RefactorRewriteSplitLines protocol.CodeActionKind = "refactor.rewrite.splitLines"
+ RefactorRewriteEliminateDotImport protocol.CodeActionKind = "refactor.rewrite.eliminateDotImport"
+ RefactorRewriteAddTags protocol.CodeActionKind = "refactor.rewrite.addTags"
+ RefactorRewriteRemoveTags protocol.CodeActionKind = "refactor.rewrite.removeTags"
// refactor.inline
RefactorInlineCall protocol.CodeActionKind = "refactor.inline.call"
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go
index ebb3f1ccfae..aa81640f3e8 100644
--- a/gopls/internal/settings/default.go
+++ b/gopls/internal/settings/default.go
@@ -91,6 +91,12 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
},
UIOptions: UIOptions{
DiagnosticOptions: DiagnosticOptions{
+ Annotations: map[Annotation]bool{
+ Bounds: true,
+ Escape: true,
+ Inline: true,
+ Nil: true,
+ },
Vulncheck: ModeVulncheckOff,
DiagnosticsDelay: 1 * time.Second,
DiagnosticsTrigger: DiagnosticsOnEdit,
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go
index 8f33bdae96b..8a694854edd 100644
--- a/gopls/internal/settings/settings.go
+++ b/gopls/internal/settings/settings.go
@@ -14,9 +14,27 @@ import (
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/semtok"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/util/frob"
)
+// An Annotation is a category of Go compiler optimization diagnostic.
+type Annotation string
+
+const (
+ // Nil controls nil checks.
+ Nil Annotation = "nil"
+
+ // Escape controls diagnostics about escape choices.
+ Escape Annotation = "escape"
+
+ // Inline controls diagnostics about inlining choices.
+ Inline Annotation = "inline"
+
+ // Bounds controls bounds checking diagnostics.
+ Bounds Annotation = "bounds"
+)
+
// Options holds various configuration that affects Gopls execution, organized
// by the nature or origin of the settings.
//
@@ -251,6 +269,8 @@ const (
// computes the set of functions reachable within your application, including
// dependencies; queries a database of known security vulnerabilities; and
// reports any potential problems it finds.
+ //
+ //gopls:status experimental
CodeLensVulncheck CodeLensSource = "vulncheck"
// Run govulncheck (legacy)
@@ -262,6 +282,8 @@ const (
// computes the set of functions reachable within your application, including
// dependencies; queries a database of known security vulnerabilities; and
// reports any potential problems it finds.
+ //
+ //gopls:status experimental
CodeLensRunGovulncheck CodeLensSource = "run_govulncheck"
// Run tests and benchmarks
@@ -411,7 +433,8 @@ type FormattingOptions struct {
Gofumpt bool
}
-// Note: DiagnosticOptions must be comparable with reflect.DeepEqual.
+// Note: DiagnosticOptions must be comparable with reflect.DeepEqual,
+// and frob-encodable (no interfaces).
type DiagnosticOptions struct {
// Analyses specify analyses that the user would like to enable or disable.
// A map of the names of analysis passes that should be enabled/disabled.
@@ -430,10 +453,34 @@ type DiagnosticOptions struct {
// ```
Analyses map[string]bool
- // Staticcheck enables additional analyses from staticcheck.io.
+ // Staticcheck configures the default set of analyses staticcheck.io.
// These analyses are documented on
// [Staticcheck's website](https://staticcheck.io/docs/checks/).
- Staticcheck bool `status:"experimental"`
+ //
+ // The "staticcheck" option has three values:
+ // - false: disable all staticcheck analyzers
+ // - true: enable all staticcheck analyzers
+ // - unset: enable a subset of staticcheck analyzers
+ // selected by gopls maintainers for runtime efficiency
+ // and analytic precision.
+ //
+ // Regardless of this setting, individual analyzers can be
+ // selectively enabled or disabled using the `analyses` setting.
+ Staticcheck bool `status:"experimental"`
+ StaticcheckProvided bool `status:"experimental"` // = "staticcheck" was explicitly provided
+
+ // Annotations specifies the various kinds of compiler
+ // optimization details that should be reported as diagnostics
+ // when enabled for a package by the "Toggle compiler
+ // optimization details" (`gopls.gc_details`) command.
+ //
+ // (Some users care only about one kind of annotation in their
+ // profiling efforts. More importantly, in large packages, the
+ // number of annotations can sometimes overwhelm the user
+ // interface and exceed the per-file diagnostic limit.)
+ //
+ // TODO(adonovan): rename this field to CompilerOptDetail.
+ Annotations map[Annotation]bool
// Vulncheck enables vulnerability scanning.
Vulncheck VulncheckMode `status:"experimental"`
@@ -739,8 +786,8 @@ type ImportsSourceEnum string
const (
ImportsSourceOff ImportsSourceEnum = "off"
- ImportsSourceGopls = "gopls"
- ImportsSourceGoimports = "goimports"
+ ImportsSourceGopls ImportsSourceEnum = "gopls"
+ ImportsSourceGoimports ImportsSourceEnum = "goimports"
)
type Matcher string
@@ -797,6 +844,11 @@ const (
NoDocumentation HoverKind = "NoDocumentation"
SynopsisDocumentation HoverKind = "SynopsisDocumentation"
FullDocumentation HoverKind = "FullDocumentation"
+
+ // Structured is a misguided experimental setting that returns a JSON
+ // hover format. This setting should not be used, as it will be removed in a
+ // future release of gopls.
+ Structured HoverKind = "Structured"
)
type VulncheckMode string
@@ -822,10 +874,18 @@ const (
// TODO: support "Manual"?
)
-// Set updates *options based on the provided JSON value:
+type CounterPath = telemetry.CounterPath
+
+// Set updates *Options based on the provided JSON value:
// null, bool, string, number, array, or object.
+//
+// The applied result describes settings that were applied. Each CounterPath
+// contains at least the name of the setting, but may also include sub-setting
+// names for settings that are themselves maps, and/or a non-empty bucket name
+// when bucketing is desirable.
+//
// On failure, it returns one or more non-nil errors.
-func (o *Options) Set(value any) (errors []error) {
+func (o *Options) Set(value any) (applied []CounterPath, errs []error) {
switch value := value.(type) {
case nil:
case map[string]any:
@@ -840,19 +900,32 @@ func (o *Options) Set(value any) (errors []error) {
name = split[len(split)-1]
if _, ok := seen[name]; ok {
- errors = append(errors, fmt.Errorf("duplicate value for %s", name))
+ errs = append(errs, fmt.Errorf("duplicate value for %s", name))
}
seen[name] = struct{}{}
- if err := o.setOne(name, value); err != nil {
+ paths, err := o.setOne(name, value)
+ if err != nil {
err := fmt.Errorf("setting option %q: %w", name, err)
- errors = append(errors, err)
+ errs = append(errs, err)
+ }
+ _, soft := err.(*SoftError)
+ if err == nil || soft {
+ if len(paths) == 0 {
+ path := CounterPath{name, ""}
+ applied = append(applied, path)
+ } else {
+ for _, subpath := range paths {
+ path := append(CounterPath{name}, subpath...)
+ applied = append(applied, path)
+ }
+ }
}
}
default:
- errors = append(errors, fmt.Errorf("invalid options type %T (want JSON null or object)", value))
+ errs = append(errs, fmt.Errorf("invalid options type %T (want JSON null or object)", value))
}
- return errors
+ return applied, errs
}
func (o *Options) ForClientCapabilities(clientInfo *protocol.ClientInfo, caps protocol.ClientCapabilities) {
@@ -945,7 +1018,7 @@ func validateDirectoryFilter(ifilter string) (string, error) {
if seg != "**" {
for _, op := range unsupportedOps {
if strings.Contains(seg, op) {
- return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue.", filter, op)
+ return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue", filter, op)
}
}
}
@@ -955,14 +1028,26 @@ func validateDirectoryFilter(ifilter string) (string, error) {
}
// setOne updates a field of o based on the name and value.
+//
+// The applied result describes the counter values to be updated as a result of
+// the applied setting. If the result is nil, the default counter for this
+// setting should be updated.
+//
+// For example, if the setting name is "foo",
+// - If applied is nil, update the count for "foo".
+// - If applied is []CounterPath{{"bucket"}}, update the count for
+// foo:bucket.
+// - If applied is []CounterPath{{"a","b"}, {"c","d"}}, update foo/a:b and
+// foo/c:d.
+//
// It returns an error if the value was invalid or duplicate.
// It is the caller's responsibility to augment the error with 'name'.
-func (o *Options) setOne(name string, value any) error {
+func (o *Options) setOne(name string, value any) (applied []CounterPath, _ error) {
switch name {
case "env":
env, ok := value.(map[string]any)
if !ok {
- return fmt.Errorf("invalid type %T (want JSON object)", value)
+ return nil, fmt.Errorf("invalid type %T (want JSON object)", value)
}
if o.Env == nil {
o.Env = make(map[string]string)
@@ -973,30 +1058,32 @@ func (o *Options) setOne(name string, value any) error {
case string, int:
o.Env[k] = fmt.Sprint(v)
default:
- return fmt.Errorf("invalid map value %T (want string)", v)
+ return nil, fmt.Errorf("invalid map value %T (want string)", v)
}
}
+ return nil, nil
case "buildFlags":
- return setStringSlice(&o.BuildFlags, value)
+ return nil, setStringSlice(&o.BuildFlags, value)
case "directoryFilters":
filterStrings, err := asStringSlice(value)
if err != nil {
- return err
+ return nil, err
}
var filters []string
for _, filterStr := range filterStrings {
filter, err := validateDirectoryFilter(filterStr)
if err != nil {
- return err
+ return nil, err
}
filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/"))
}
o.DirectoryFilters = filters
+ return nil, nil
case "workspaceFiles":
- return setStringSlice(&o.WorkspaceFiles, value)
+ return nil, setStringSlice(&o.WorkspaceFiles, value)
case "completionDocumentation":
return setBool(&o.CompletionDocumentation, value)
case "usePlaceholders":
@@ -1006,7 +1093,7 @@ func (o *Options) setOne(name string, value any) error {
case "completeUnimported":
return setBool(&o.CompleteUnimported, value)
case "completionBudget":
- return setDuration(&o.CompletionBudget, value)
+ return nil, setDuration(&o.CompletionBudget, value)
case "importsSource":
return setEnum(&o.ImportsSource, value,
ImportsSourceOff,
@@ -1037,17 +1124,18 @@ func (o *Options) setOne(name string, value any) error {
AllSymbolScope)
case "hoverKind":
- if s, ok := value.(string); ok && strings.EqualFold(s, "structured") {
- return deprecatedError("the experimental hoverKind='structured' setting was removed in gopls/v0.18.0 (https://go.dev/issue/70233)")
- }
+ // TODO(rfindley): reinstate the deprecation of Structured hover by making
+ // it a warning in gopls v0.N+1, and removing it in gopls v0.N+2.
return setEnum(&o.HoverKind, value,
NoDocumentation,
SingleLine,
SynopsisDocumentation,
- FullDocumentation)
+ FullDocumentation,
+ Structured,
+ )
case "linkTarget":
- return setString(&o.LinkTarget, value)
+ return nil, setString(&o.LinkTarget, value)
case "linksInHover":
switch value {
@@ -1058,9 +1146,9 @@ func (o *Options) setOne(name string, value any) error {
case "gopls":
o.LinksInHover = LinksInHover_Gopls
default:
- return fmt.Errorf(`invalid value %s; expect false, true, or "gopls"`,
- value)
+ return nil, fmt.Errorf(`invalid value %s; expect false, true, or "gopls"`, value)
}
+ return nil, nil
case "importShortcut":
return setEnum(&o.ImportShortcut, value,
@@ -1069,18 +1157,20 @@ func (o *Options) setOne(name string, value any) error {
DefinitionShortcut)
case "analyses":
- if err := setBoolMap(&o.Analyses, value); err != nil {
- return err
+ counts, err := setBoolMap(&o.Analyses, value)
+ if err != nil {
+ return nil, err
}
if o.Analyses["fieldalignment"] {
- return deprecatedError("the 'fieldalignment' analyzer was removed in gopls/v0.17.0; instead, hover over struct fields to see size/offset information (https://go.dev/issue/66861)")
+ return counts, &SoftError{"the 'fieldalignment' analyzer was removed in gopls/v0.17.0; instead, hover over struct fields to see size/offset information (https://go.dev/issue/66861)"}
}
+ return counts, nil
case "hints":
return setBoolMap(&o.Hints, value)
case "annotations":
- return deprecatedError("the 'annotations' setting was removed in gopls/v0.18.0; all compiler optimization details are now shown")
+ return setAnnotationMap(&o.Annotations, value)
case "vulncheck":
return setEnum(&o.Vulncheck, value,
@@ -1090,7 +1180,7 @@ func (o *Options) setOne(name string, value any) error {
case "codelenses", "codelens":
lensOverrides, err := asBoolMap[CodeLensSource](value)
if err != nil {
- return err
+ return nil, err
}
if o.Codelenses == nil {
o.Codelenses = make(map[CodeLensSource]bool)
@@ -1098,15 +1188,22 @@ func (o *Options) setOne(name string, value any) error {
o.Codelenses = maps.Clone(o.Codelenses)
maps.Copy(o.Codelenses, lensOverrides)
+ var counts []CounterPath
+ for k, v := range lensOverrides {
+ counts = append(counts, CounterPath{string(k), fmt.Sprint(v)})
+ }
+
if name == "codelens" {
- return deprecatedError("codelenses")
+ return counts, deprecatedError("codelenses")
}
+ return counts, nil
case "staticcheck":
+ o.StaticcheckProvided = true
return setBool(&o.Staticcheck, value)
case "local":
- return setString(&o.Local, value)
+ return nil, setString(&o.Local, value)
case "verboseOutput":
return setBool(&o.VerboseOutput, value)
@@ -1128,16 +1225,18 @@ func (o *Options) setOne(name string, value any) error {
// TODO(hxjiang): deprecate noSemanticString and noSemanticNumber.
case "noSemanticString":
- if err := setBool(&o.NoSemanticString, value); err != nil {
- return err
+ counts, err := setBool(&o.NoSemanticString, value)
+ if err != nil {
+ return nil, err
}
- return &SoftError{fmt.Sprintf("noSemanticString setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being).")}
+ return counts, &SoftError{"noSemanticString setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being)."}
case "noSemanticNumber":
- if err := setBool(&o.NoSemanticNumber, value); err != nil {
- return nil
+ counts, err := setBool(&o.NoSemanticNumber, value)
+ if err != nil {
+ return nil, err
}
- return &SoftError{fmt.Sprintf("noSemanticNumber setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being).")}
+ return counts, &SoftError{"noSemanticNumber setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being)."}
case "semanticTokenTypes":
return setBoolMap(&o.SemanticTokenTypes, value)
@@ -1157,15 +1256,16 @@ func (o *Options) setOne(name string, value any) error {
case "templateExtensions":
switch value := value.(type) {
case []any:
- return setStringSlice(&o.TemplateExtensions, value)
+ return nil, setStringSlice(&o.TemplateExtensions, value)
case nil:
o.TemplateExtensions = nil
default:
- return fmt.Errorf("unexpected type %T (want JSON array of string)", value)
+ return nil, fmt.Errorf("unexpected type %T (want JSON array of string)", value)
}
+ return nil, nil
case "diagnosticsDelay":
- return setDuration(&o.DiagnosticsDelay, value)
+ return nil, setDuration(&o.DiagnosticsDelay, value)
case "diagnosticsTrigger":
return setEnum(&o.DiagnosticsTrigger, value,
@@ -1175,11 +1275,8 @@ func (o *Options) setOne(name string, value any) error {
case "analysisProgressReporting":
return setBool(&o.AnalysisProgressReporting, value)
- case "allowImplicitNetworkAccess":
- return deprecatedError("")
-
case "standaloneTags":
- return setStringSlice(&o.StandaloneTags, value)
+ return nil, setStringSlice(&o.StandaloneTags, value)
case "subdirWatchPatterns":
return setEnum(&o.SubdirWatchPatterns, value,
@@ -1188,7 +1285,7 @@ func (o *Options) setOne(name string, value any) error {
SubdirWatchPatternsAuto)
case "reportAnalysisProgressAfter":
- return setDuration(&o.ReportAnalysisProgressAfter, value)
+ return nil, setDuration(&o.ReportAnalysisProgressAfter, value)
case "telemetryPrompt":
return setBool(&o.TelemetryPrompt, value)
@@ -1213,50 +1310,54 @@ func (o *Options) setOne(name string, value any) error {
// renamed
case "experimentalDisabledAnalyses":
- return deprecatedError("analyses")
+ return nil, deprecatedError("analyses")
case "disableDeepCompletion":
- return deprecatedError("deepCompletion")
+ return nil, deprecatedError("deepCompletion")
case "disableFuzzyMatching":
- return deprecatedError("fuzzyMatching")
+ return nil, deprecatedError("fuzzyMatching")
case "wantCompletionDocumentation":
- return deprecatedError("completionDocumentation")
+ return nil, deprecatedError("completionDocumentation")
case "wantUnimportedCompletions":
- return deprecatedError("completeUnimported")
+ return nil, deprecatedError("completeUnimported")
case "fuzzyMatching":
- return deprecatedError("matcher")
+ return nil, deprecatedError("matcher")
case "caseSensitiveCompletion":
- return deprecatedError("matcher")
+ return nil, deprecatedError("matcher")
case "experimentalDiagnosticsDelay":
- return deprecatedError("diagnosticsDelay")
+ return nil, deprecatedError("diagnosticsDelay")
// deprecated
+
+ case "allowImplicitNetworkAccess":
+ return nil, deprecatedError("")
+
case "memoryMode":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "tempModFile":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "experimentalWorkspaceModule":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "experimentalTemplateSupport":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "experimentalWatchedFileDelay":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "experimentalPackageCacheKey":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "allowModfileModifications":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "allExperiments":
// golang/go#65548: this setting is a no-op, but we fail don't report it as
@@ -1265,29 +1366,29 @@ func (o *Options) setOne(name string, value any) error {
// If, in the future, VS Code stops injecting this, we could theoretically
// report an error here, but it also seems harmless to keep ignoring this
// setting forever.
+ return nil, nil
case "experimentalUseInvalidMetadata":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "newDiff":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "wantSuggestedFixes":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "noIncrementalSync":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "watchFileChanges":
- return deprecatedError("")
+ return nil, deprecatedError("")
case "go-diff":
- return deprecatedError("")
+ return nil, deprecatedError("")
default:
- return fmt.Errorf("unexpected setting")
+ return nil, fmt.Errorf("unexpected setting")
}
- return nil
}
// EnabledSemanticTokenModifiers returns a map of modifiers to boolean.
@@ -1299,10 +1400,10 @@ func (o *Options) EnabledSemanticTokenModifiers() map[semtok.Modifier]bool {
return copy
}
-// EncodeSemanticTokenTypes returns a map of types to boolean.
+// EnabledSemanticTokenTypes returns a map of types to boolean.
func (o *Options) EnabledSemanticTokenTypes() map[semtok.Type]bool {
copy := make(map[semtok.Type]bool, len(o.SemanticTokenTypes))
- for k, v := range o.SemanticTokenModifiers {
+ for k, v := range o.SemanticTokenTypes {
copy[semtok.Type(k)] = v
}
if o.NoSemanticString {
@@ -1323,15 +1424,10 @@ func (e *SoftError) Error() string {
return e.msg
}
-// softErrorf reports a soft error related to the current option.
-func softErrorf(format string, args ...any) error {
- return &SoftError{fmt.Sprintf(format, args...)}
-}
-
// deprecatedError reports the current setting as deprecated.
// The optional replacement is suggested to the user.
func deprecatedError(replacement string) error {
- msg := fmt.Sprintf("this setting is deprecated")
+ msg := "this setting is deprecated"
if replacement != "" {
msg = fmt.Sprintf("%s, use %q instead", msg, replacement)
}
@@ -1341,13 +1437,13 @@ func deprecatedError(replacement string) error {
// setT() and asT() helpers: the setT forms write to the 'dest *T'
// variable only on success, to reduce boilerplate in Option.set.
-func setBool(dest *bool, value any) error {
+func setBool(dest *bool, value any) ([]CounterPath, error) {
b, err := asBool(value)
if err != nil {
- return err
+ return nil, err
}
*dest = b
- return nil
+ return []CounterPath{{fmt.Sprint(b)}}, nil
}
func asBool(value any) (bool, error) {
@@ -1371,13 +1467,62 @@ func setDuration(dest *time.Duration, value any) error {
return nil
}
-func setBoolMap[K ~string](dest *map[K]bool, value any) error {
+func setAnnotationMap(dest *map[Annotation]bool, value any) ([]CounterPath, error) {
+ all, err := asBoolMap[string](value)
+ if err != nil {
+ return nil, err
+ }
+ var counters []CounterPath
+ // Default to everything enabled by default.
+ m := make(map[Annotation]bool)
+ for k, enabled := range all {
+ var a Annotation
+ cnts, err := setEnum(&a, k,
+ Nil,
+ Escape,
+ Inline,
+ Bounds)
+ if err != nil {
+ // In case of an error, process any legacy values.
+ switch k {
+ case "noEscape":
+ m[Escape] = false
+ return nil, fmt.Errorf(`"noEscape" is deprecated, set "Escape: false" instead`)
+
+ case "noNilcheck":
+ m[Nil] = false
+ return nil, fmt.Errorf(`"noNilcheck" is deprecated, set "Nil: false" instead`)
+
+ case "noInline":
+ m[Inline] = false
+ return nil, fmt.Errorf(`"noInline" is deprecated, set "Inline: false" instead`)
+
+ case "noBounds":
+ m[Bounds] = false
+ return nil, fmt.Errorf(`"noBounds" is deprecated, set "Bounds: false" instead`)
+
+ default:
+ return nil, err
+ }
+ }
+ counters = append(counters, cnts...)
+ m[a] = enabled
+ }
+ *dest = m
+ return counters, nil
+}
+
+func setBoolMap[K ~string](dest *map[K]bool, value any) ([]CounterPath, error) {
m, err := asBoolMap[K](value)
if err != nil {
- return err
+ return nil, err
}
*dest = m
- return nil
+ var counts []CounterPath
+ for k, v := range m {
+ counts = append(counts, CounterPath{string(k), fmt.Sprint(v)})
+ }
+ return counts, nil
}
func asBoolMap[K ~string](value any) (map[K]bool, error) {
@@ -1438,13 +1583,13 @@ func asStringSlice(value any) ([]string, error) {
return slice, nil
}
-func setEnum[S ~string](dest *S, value any, options ...S) error {
+func setEnum[S ~string](dest *S, value any, options ...S) ([]CounterPath, error) {
enum, err := asEnum(value, options...)
if err != nil {
- return err
+ return nil, err
}
*dest = enum
- return nil
+ return []CounterPath{{string(enum)}}, nil
}
func asEnum[S ~string](value any, options ...S) (S, error) {
diff --git a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go
index 63b4aded8bd..d7a032e1938 100644
--- a/gopls/internal/settings/settings_test.go
+++ b/gopls/internal/settings/settings_test.go
@@ -91,19 +91,19 @@ func TestOptions_Set(t *testing.T) {
},
},
{
- name: "hoverKind",
- value: "Structured",
- wantError: true,
+ name: "hoverKind",
+ value: "Structured",
+ // wantError: true, // TODO(rfindley): reinstate this error
check: func(o Options) bool {
- return o.HoverKind == FullDocumentation
+ return o.HoverKind == Structured
},
},
{
- name: "ui.documentation.hoverKind",
- value: "Structured",
- wantError: true,
+ name: "ui.documentation.hoverKind",
+ value: "Structured",
+ // wantError: true, // TODO(rfindley): reinstate this error
check: func(o Options) bool {
- return o.HoverKind == FullDocumentation
+ return o.HoverKind == Structured
},
},
{
@@ -180,6 +180,17 @@ func TestOptions_Set(t *testing.T) {
return len(o.DirectoryFilters) == 0
},
},
+ {
+ name: "annotations",
+ value: map[string]any{
+ "Nil": false,
+ "noBounds": true,
+ },
+ wantError: true,
+ check: func(o Options) bool {
+ return !o.Annotations[Nil] && !o.Annotations[Bounds]
+ },
+ },
{
name: "vulncheck",
value: []any{"invalid"},
@@ -206,7 +217,7 @@ func TestOptions_Set(t *testing.T) {
for _, test := range tests {
var opts Options
- err := opts.Set(map[string]any{test.name: test.value})
+ _, err := opts.Set(map[string]any{test.name: test.value})
if err != nil {
if !test.wantError {
t.Errorf("Options.set(%q, %v) failed: %v",
diff --git a/gopls/internal/settings/staticcheck.go b/gopls/internal/settings/staticcheck.go
index 6e06e0b44ea..68e48819cfc 100644
--- a/gopls/internal/settings/staticcheck.go
+++ b/gopls/internal/settings/staticcheck.go
@@ -5,19 +5,183 @@
package settings
import (
+ "fmt"
+ "log"
+
+ "golang.org/x/tools/go/analysis"
"golang.org/x/tools/gopls/internal/protocol"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/quickfix"
+ "honnef.co/go/tools/quickfix/qf1001"
+ "honnef.co/go/tools/quickfix/qf1002"
+ "honnef.co/go/tools/quickfix/qf1003"
+ "honnef.co/go/tools/quickfix/qf1004"
+ "honnef.co/go/tools/quickfix/qf1005"
+ "honnef.co/go/tools/quickfix/qf1006"
+ "honnef.co/go/tools/quickfix/qf1007"
+ "honnef.co/go/tools/quickfix/qf1008"
+ "honnef.co/go/tools/quickfix/qf1009"
+ "honnef.co/go/tools/quickfix/qf1010"
+ "honnef.co/go/tools/quickfix/qf1011"
+ "honnef.co/go/tools/quickfix/qf1012"
"honnef.co/go/tools/simple"
+ "honnef.co/go/tools/simple/s1000"
+ "honnef.co/go/tools/simple/s1001"
+ "honnef.co/go/tools/simple/s1002"
+ "honnef.co/go/tools/simple/s1003"
+ "honnef.co/go/tools/simple/s1004"
+ "honnef.co/go/tools/simple/s1005"
+ "honnef.co/go/tools/simple/s1006"
+ "honnef.co/go/tools/simple/s1007"
+ "honnef.co/go/tools/simple/s1008"
+ "honnef.co/go/tools/simple/s1009"
+ "honnef.co/go/tools/simple/s1010"
+ "honnef.co/go/tools/simple/s1011"
+ "honnef.co/go/tools/simple/s1012"
+ "honnef.co/go/tools/simple/s1016"
+ "honnef.co/go/tools/simple/s1017"
+ "honnef.co/go/tools/simple/s1018"
+ "honnef.co/go/tools/simple/s1019"
+ "honnef.co/go/tools/simple/s1020"
+ "honnef.co/go/tools/simple/s1021"
+ "honnef.co/go/tools/simple/s1023"
+ "honnef.co/go/tools/simple/s1024"
+ "honnef.co/go/tools/simple/s1025"
+ "honnef.co/go/tools/simple/s1028"
+ "honnef.co/go/tools/simple/s1029"
+ "honnef.co/go/tools/simple/s1030"
+ "honnef.co/go/tools/simple/s1031"
+ "honnef.co/go/tools/simple/s1032"
+ "honnef.co/go/tools/simple/s1033"
+ "honnef.co/go/tools/simple/s1034"
+ "honnef.co/go/tools/simple/s1035"
+ "honnef.co/go/tools/simple/s1036"
+ "honnef.co/go/tools/simple/s1037"
+ "honnef.co/go/tools/simple/s1038"
+ "honnef.co/go/tools/simple/s1039"
+ "honnef.co/go/tools/simple/s1040"
"honnef.co/go/tools/staticcheck"
+ "honnef.co/go/tools/staticcheck/sa1000"
+ "honnef.co/go/tools/staticcheck/sa1001"
+ "honnef.co/go/tools/staticcheck/sa1002"
+ "honnef.co/go/tools/staticcheck/sa1003"
+ "honnef.co/go/tools/staticcheck/sa1004"
+ "honnef.co/go/tools/staticcheck/sa1005"
+ "honnef.co/go/tools/staticcheck/sa1006"
+ "honnef.co/go/tools/staticcheck/sa1007"
+ "honnef.co/go/tools/staticcheck/sa1008"
+ "honnef.co/go/tools/staticcheck/sa1010"
+ "honnef.co/go/tools/staticcheck/sa1011"
+ "honnef.co/go/tools/staticcheck/sa1012"
+ "honnef.co/go/tools/staticcheck/sa1013"
+ "honnef.co/go/tools/staticcheck/sa1014"
+ "honnef.co/go/tools/staticcheck/sa1015"
+ "honnef.co/go/tools/staticcheck/sa1016"
+ "honnef.co/go/tools/staticcheck/sa1017"
+ "honnef.co/go/tools/staticcheck/sa1018"
+ "honnef.co/go/tools/staticcheck/sa1019"
+ "honnef.co/go/tools/staticcheck/sa1020"
+ "honnef.co/go/tools/staticcheck/sa1021"
+ "honnef.co/go/tools/staticcheck/sa1023"
+ "honnef.co/go/tools/staticcheck/sa1024"
+ "honnef.co/go/tools/staticcheck/sa1025"
+ "honnef.co/go/tools/staticcheck/sa1026"
+ "honnef.co/go/tools/staticcheck/sa1027"
+ "honnef.co/go/tools/staticcheck/sa1028"
+ "honnef.co/go/tools/staticcheck/sa1029"
+ "honnef.co/go/tools/staticcheck/sa1030"
+ "honnef.co/go/tools/staticcheck/sa1031"
+ "honnef.co/go/tools/staticcheck/sa1032"
+ "honnef.co/go/tools/staticcheck/sa2000"
+ "honnef.co/go/tools/staticcheck/sa2001"
+ "honnef.co/go/tools/staticcheck/sa2002"
+ "honnef.co/go/tools/staticcheck/sa2003"
+ "honnef.co/go/tools/staticcheck/sa3000"
+ "honnef.co/go/tools/staticcheck/sa3001"
+ "honnef.co/go/tools/staticcheck/sa4000"
+ "honnef.co/go/tools/staticcheck/sa4001"
+ "honnef.co/go/tools/staticcheck/sa4003"
+ "honnef.co/go/tools/staticcheck/sa4004"
+ "honnef.co/go/tools/staticcheck/sa4005"
+ "honnef.co/go/tools/staticcheck/sa4006"
+ "honnef.co/go/tools/staticcheck/sa4008"
+ "honnef.co/go/tools/staticcheck/sa4009"
+ "honnef.co/go/tools/staticcheck/sa4010"
+ "honnef.co/go/tools/staticcheck/sa4011"
+ "honnef.co/go/tools/staticcheck/sa4012"
+ "honnef.co/go/tools/staticcheck/sa4013"
+ "honnef.co/go/tools/staticcheck/sa4014"
+ "honnef.co/go/tools/staticcheck/sa4015"
+ "honnef.co/go/tools/staticcheck/sa4016"
+ "honnef.co/go/tools/staticcheck/sa4017"
+ "honnef.co/go/tools/staticcheck/sa4018"
+ "honnef.co/go/tools/staticcheck/sa4019"
+ "honnef.co/go/tools/staticcheck/sa4020"
+ "honnef.co/go/tools/staticcheck/sa4021"
+ "honnef.co/go/tools/staticcheck/sa4022"
+ "honnef.co/go/tools/staticcheck/sa4023"
+ "honnef.co/go/tools/staticcheck/sa4024"
+ "honnef.co/go/tools/staticcheck/sa4025"
+ "honnef.co/go/tools/staticcheck/sa4026"
+ "honnef.co/go/tools/staticcheck/sa4027"
+ "honnef.co/go/tools/staticcheck/sa4028"
+ "honnef.co/go/tools/staticcheck/sa4029"
+ "honnef.co/go/tools/staticcheck/sa4030"
+ "honnef.co/go/tools/staticcheck/sa4031"
+ "honnef.co/go/tools/staticcheck/sa4032"
+ "honnef.co/go/tools/staticcheck/sa5000"
+ "honnef.co/go/tools/staticcheck/sa5001"
+ "honnef.co/go/tools/staticcheck/sa5002"
+ "honnef.co/go/tools/staticcheck/sa5003"
+ "honnef.co/go/tools/staticcheck/sa5004"
+ "honnef.co/go/tools/staticcheck/sa5005"
+ "honnef.co/go/tools/staticcheck/sa5007"
+ "honnef.co/go/tools/staticcheck/sa5008"
+ "honnef.co/go/tools/staticcheck/sa5009"
+ "honnef.co/go/tools/staticcheck/sa5010"
+ "honnef.co/go/tools/staticcheck/sa5011"
+ "honnef.co/go/tools/staticcheck/sa5012"
+ "honnef.co/go/tools/staticcheck/sa6000"
+ "honnef.co/go/tools/staticcheck/sa6001"
+ "honnef.co/go/tools/staticcheck/sa6002"
+ "honnef.co/go/tools/staticcheck/sa6003"
+ "honnef.co/go/tools/staticcheck/sa6005"
+ "honnef.co/go/tools/staticcheck/sa6006"
+ "honnef.co/go/tools/staticcheck/sa9001"
+ "honnef.co/go/tools/staticcheck/sa9002"
+ "honnef.co/go/tools/staticcheck/sa9003"
+ "honnef.co/go/tools/staticcheck/sa9004"
+ "honnef.co/go/tools/staticcheck/sa9005"
+ "honnef.co/go/tools/staticcheck/sa9006"
+ "honnef.co/go/tools/staticcheck/sa9007"
+ "honnef.co/go/tools/staticcheck/sa9008"
+ "honnef.co/go/tools/staticcheck/sa9009"
"honnef.co/go/tools/stylecheck"
+ "honnef.co/go/tools/stylecheck/st1000"
+ "honnef.co/go/tools/stylecheck/st1001"
+ "honnef.co/go/tools/stylecheck/st1003"
+ "honnef.co/go/tools/stylecheck/st1005"
+ "honnef.co/go/tools/stylecheck/st1006"
+ "honnef.co/go/tools/stylecheck/st1008"
+ "honnef.co/go/tools/stylecheck/st1011"
+ "honnef.co/go/tools/stylecheck/st1012"
+ "honnef.co/go/tools/stylecheck/st1013"
+ "honnef.co/go/tools/stylecheck/st1015"
+ "honnef.co/go/tools/stylecheck/st1016"
+ "honnef.co/go/tools/stylecheck/st1017"
+ "honnef.co/go/tools/stylecheck/st1018"
+ "honnef.co/go/tools/stylecheck/st1019"
+ "honnef.co/go/tools/stylecheck/st1020"
+ "honnef.co/go/tools/stylecheck/st1021"
+ "honnef.co/go/tools/stylecheck/st1022"
+ "honnef.co/go/tools/stylecheck/st1023"
)
-// StaticcheckAnalzyers describes available Staticcheck analyzers, keyed by
-// analyzer name.
-var StaticcheckAnalyzers = make(map[string]*Analyzer) // written by analysis_.go
+// StaticcheckAnalyzers lists available Staticcheck analyzers.
+var StaticcheckAnalyzers = initStaticcheckAnalyzers()
+
+func initStaticcheckAnalyzers() (res []*Analyzer) {
-func init() {
mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity {
switch severity {
case lint.SeverityError:
@@ -36,28 +200,251 @@ func init() {
return protocol.SeverityWarning
}
}
- add := func(analyzers []*lint.Analyzer, skip map[string]struct{}) {
- for _, a := range analyzers {
- if _, ok := skip[a.Analyzer.Name]; ok {
- continue
+
+ // We can't import buildir.Analyzer directly, so grab it from another analyzer.
+ buildir := sa1000.SCAnalyzer.Analyzer.Requires[0]
+ if buildir.Name != "buildir" {
+ panic("sa1000.Requires[0] is not buildir")
+ }
+
+ add := func(a *lint.Analyzer, dflt bool) {
+ // Assert that no analyzer that requires "buildir",
+ // even indirectly, is enabled by default.
+ if dflt {
+ var visit func(aa *analysis.Analyzer)
+ visit = func(aa *analysis.Analyzer) {
+ if aa == buildir {
+ log.Fatalf("%s requires buildir (perhaps indirectly) yet is enabled by default", a.Analyzer.Name)
+ }
+ for _, req := range aa.Requires {
+ visit(req)
+ }
}
+ visit(a.Analyzer)
+ }
+ res = append(res, &Analyzer{
+ analyzer: a.Analyzer,
+ staticcheck: a.Doc,
+ nonDefault: !dflt,
+ severity: mapSeverity(a.Doc.Severity),
+ })
+ }
- StaticcheckAnalyzers[a.Analyzer.Name] = &Analyzer{
- analyzer: a.Analyzer,
- nonDefault: a.Doc.NonDefault,
- severity: mapSeverity(a.Doc.Severity),
+ type M = map[*lint.Analyzer]any // value = true|false|nil
+
+ addAll := func(suite string, upstream []*lint.Analyzer, config M) {
+ for _, a := range upstream {
+ v, ok := config[a]
+ if !ok {
+ panic(fmt.Sprintf("%s.Analyzers includes %s but config mapping does not; settings audit required", suite, a.Analyzer.Name))
+ }
+ if v != nil {
+ add(a, v.(bool))
}
}
}
- add(simple.Analyzers, nil)
- add(staticcheck.Analyzers, map[string]struct{}{
- // This check conflicts with the vet printf check (golang/go#34494).
- "SA5009": {},
- // This check relies on facts from dependencies, which
- // we don't currently compute.
- "SA5011": {},
+ // For each analyzer in the four suites provided by
+ // staticcheck, we provide a complete configuration, mapping
+ // it to a boolean, indicating whether it should be on by
+ // default in gopls, or nil to indicate explicitly that it has
+ // been excluded (e.g. because it is redundant with an
+ // existing vet analyzer such as printf, waitgroup, appends).
+ //
+ // This approach ensures that as suites grow, we make an
+ // affirmative decision, positive or negative, about adding
+ // new items.
+ //
+ // An analyzer may be off by default if:
+ // - it requires, even indirectly, "buildir", which is like
+ // buildssa but uses facts, making it expensive;
+ // - it has significant false positives;
+ // - it reports on non-problematic style issues;
+ // - its fixes are lossy (e.g. of comments) or not always sound;
+ // - it reports "maybes", not "definites" (e.g. sa9001).
+ // - it reports on harmless stylistic choices that may have
+ // been chosen deliberately for clarity or emphasis (e.g. s1005).
+ // - it makes deductions from build tags that are not true
+ // for all configurations.
+
+ addAll("simple", simple.Analyzers, M{
+ s1000.SCAnalyzer: true,
+ s1001.SCAnalyzer: true,
+ s1002.SCAnalyzer: false, // makes unsound deductions from build tags
+ s1003.SCAnalyzer: true,
+ s1004.SCAnalyzer: true,
+ s1005.SCAnalyzer: false, // not a correctness/style issue
+ s1006.SCAnalyzer: false, // makes unsound deductions from build tags
+ s1007.SCAnalyzer: true,
+ s1008.SCAnalyzer: false, // may lose important comments
+ s1009.SCAnalyzer: true,
+ s1010.SCAnalyzer: true,
+ s1011.SCAnalyzer: false, // requires buildir
+ s1012.SCAnalyzer: true,
+ s1016.SCAnalyzer: false, // may rely on coincidental structural subtyping
+ s1017.SCAnalyzer: true,
+ s1018.SCAnalyzer: true,
+ s1019.SCAnalyzer: true,
+ s1020.SCAnalyzer: true,
+ s1021.SCAnalyzer: false, // may lose important comments
+ s1023.SCAnalyzer: true,
+ s1024.SCAnalyzer: true,
+ s1025.SCAnalyzer: false, // requires buildir
+ s1028.SCAnalyzer: true,
+ s1029.SCAnalyzer: false, // requires buildir
+ s1030.SCAnalyzer: true, // (tentative: see docs,
+ s1031.SCAnalyzer: true,
+ s1032.SCAnalyzer: true,
+ s1033.SCAnalyzer: true,
+ s1034.SCAnalyzer: true,
+ s1035.SCAnalyzer: true,
+ s1036.SCAnalyzer: true,
+ s1037.SCAnalyzer: true,
+ s1038.SCAnalyzer: true,
+ s1039.SCAnalyzer: true,
+ s1040.SCAnalyzer: true,
+ })
+
+ addAll("stylecheck", stylecheck.Analyzers, M{
+ // These are all slightly too opinionated to be on by default.
+ st1000.SCAnalyzer: false,
+ st1001.SCAnalyzer: false,
+ st1003.SCAnalyzer: false,
+ st1005.SCAnalyzer: false,
+ st1006.SCAnalyzer: false,
+ st1008.SCAnalyzer: false,
+ st1011.SCAnalyzer: false,
+ st1012.SCAnalyzer: false,
+ st1013.SCAnalyzer: false,
+ st1015.SCAnalyzer: false,
+ st1016.SCAnalyzer: false,
+ st1017.SCAnalyzer: false,
+ st1018.SCAnalyzer: false,
+ st1019.SCAnalyzer: false,
+ st1020.SCAnalyzer: false,
+ st1021.SCAnalyzer: false,
+ st1022.SCAnalyzer: false,
+ st1023.SCAnalyzer: false,
+ })
+
+ // These are not bug fixes but code transformations: some
+ // reversible and value-neutral, of the kind typically listed
+ // on the VS Code's Refactor/Source Action/Quick Fix menus.
+ //
+ // TODO(adonovan): plumb these to the appropriate menu,
+ // as we do for code actions such as split/join lines.
+ addAll("quickfix", quickfix.Analyzers, M{
+ qf1001.SCAnalyzer: false, // not always a style improvement
+ qf1002.SCAnalyzer: true,
+ qf1003.SCAnalyzer: true,
+ qf1004.SCAnalyzer: true,
+ qf1005.SCAnalyzer: false, // not always a style improvement
+ qf1006.SCAnalyzer: false, // may lose important comments
+ qf1007.SCAnalyzer: false, // may lose important comments
+ qf1008.SCAnalyzer: false, // not always a style improvement
+ qf1009.SCAnalyzer: true,
+ qf1010.SCAnalyzer: true,
+ qf1011.SCAnalyzer: false, // not always a style improvement
+ qf1012.SCAnalyzer: true,
+ })
+
+ addAll("staticcheck", staticcheck.Analyzers, M{
+ sa1000.SCAnalyzer: false, // requires buildir
+ sa1001.SCAnalyzer: true,
+ sa1002.SCAnalyzer: false, // requires buildir
+ sa1003.SCAnalyzer: false, // requires buildir
+ sa1004.SCAnalyzer: true,
+ sa1005.SCAnalyzer: true,
+ sa1006.SCAnalyzer: nil, // redundant wrt 'printf'
+ sa1007.SCAnalyzer: false, // requires buildir
+ sa1008.SCAnalyzer: true,
+ sa1010.SCAnalyzer: false, // requires buildir
+ sa1011.SCAnalyzer: false, // requires buildir
+ sa1012.SCAnalyzer: true,
+ sa1013.SCAnalyzer: true,
+ sa1014.SCAnalyzer: false, // requires buildir
+ sa1015.SCAnalyzer: false, // requires buildir
+ sa1016.SCAnalyzer: true,
+ sa1017.SCAnalyzer: false, // requires buildir
+ sa1018.SCAnalyzer: false, // requires buildir
+ sa1019.SCAnalyzer: nil, // redundant wrt 'deprecated'
+ sa1020.SCAnalyzer: false, // requires buildir
+ sa1021.SCAnalyzer: false, // requires buildir
+ sa1023.SCAnalyzer: false, // requires buildir
+ sa1024.SCAnalyzer: false, // requires buildir
+ sa1025.SCAnalyzer: false, // requires buildir
+ sa1026.SCAnalyzer: false, // requires buildir
+ sa1027.SCAnalyzer: false, // requires buildir
+ sa1028.SCAnalyzer: false, // requires buildir
+ sa1029.SCAnalyzer: false, // requires buildir
+ sa1030.SCAnalyzer: false, // requires buildir
+ sa1031.SCAnalyzer: false, // requires buildir
+ sa1032.SCAnalyzer: false, // requires buildir
+ sa2000.SCAnalyzer: nil, // redundant wrt 'waitgroup'
+ sa2001.SCAnalyzer: true,
+ sa2002.SCAnalyzer: false, // requires buildir
+ sa2003.SCAnalyzer: false, // requires buildir
+ sa3000.SCAnalyzer: true,
+ sa3001.SCAnalyzer: true,
+ sa4000.SCAnalyzer: true,
+ sa4001.SCAnalyzer: true,
+ sa4003.SCAnalyzer: true,
+ sa4004.SCAnalyzer: true,
+ sa4005.SCAnalyzer: false, // requires buildir
+ sa4006.SCAnalyzer: false, // requires buildir
+ sa4008.SCAnalyzer: false, // requires buildir
+ sa4009.SCAnalyzer: false, // requires buildir
+ sa4010.SCAnalyzer: false, // requires buildir
+ sa4011.SCAnalyzer: true,
+ sa4012.SCAnalyzer: false, // requires buildir
+ sa4013.SCAnalyzer: true,
+ sa4014.SCAnalyzer: true,
+ sa4015.SCAnalyzer: false, // requires buildir
+ sa4016.SCAnalyzer: true,
+ sa4017.SCAnalyzer: false, // requires buildir
+ sa4018.SCAnalyzer: false, // requires buildir
+ sa4019.SCAnalyzer: true,
+ sa4020.SCAnalyzer: true,
+ sa4021.SCAnalyzer: nil, // redundant wrt 'appends'
+ sa4022.SCAnalyzer: true,
+ sa4023.SCAnalyzer: false, // requires buildir
+ sa4024.SCAnalyzer: true,
+ sa4025.SCAnalyzer: true,
+ sa4026.SCAnalyzer: true,
+ sa4027.SCAnalyzer: true,
+ sa4028.SCAnalyzer: true,
+ sa4029.SCAnalyzer: true,
+ sa4030.SCAnalyzer: true,
+ sa4031.SCAnalyzer: false, // requires buildir
+ sa4032.SCAnalyzer: true,
+ sa5000.SCAnalyzer: false, // requires buildir
+ sa5001.SCAnalyzer: true,
+ sa5002.SCAnalyzer: false, // makes unsound deductions from build tags
+ sa5003.SCAnalyzer: true,
+ sa5004.SCAnalyzer: true,
+ sa5005.SCAnalyzer: false, // requires buildir
+ sa5007.SCAnalyzer: false, // requires buildir
+ sa5008.SCAnalyzer: true,
+ sa5009.SCAnalyzer: nil, // requires buildir; redundant wrt 'printf' (#34494,
+ sa5010.SCAnalyzer: false, // requires buildir
+ sa5011.SCAnalyzer: false, // requires buildir
+ sa5012.SCAnalyzer: false, // requires buildir
+ sa6000.SCAnalyzer: false, // requires buildir
+ sa6001.SCAnalyzer: false, // requires buildir
+ sa6002.SCAnalyzer: false, // requires buildir
+ sa6003.SCAnalyzer: false, // requires buildir
+ sa6005.SCAnalyzer: true,
+ sa6006.SCAnalyzer: true,
+ sa9001.SCAnalyzer: false, // reports a "maybe" bug (low signal/noise,
+ sa9002.SCAnalyzer: true,
+ sa9003.SCAnalyzer: false, // requires buildir; NonDefault
+ sa9004.SCAnalyzer: true,
+ sa9005.SCAnalyzer: false, // requires buildir
+ sa9006.SCAnalyzer: true,
+ sa9007.SCAnalyzer: false, // requires buildir
+ sa9008.SCAnalyzer: false, // requires buildir
+ sa9009.SCAnalyzer: true,
})
- add(stylecheck.Analyzers, nil)
- add(quickfix.Analyzers, nil)
+
+ return res
}
diff --git a/gopls/internal/settings/vet_test.go b/gopls/internal/settings/vet_test.go
index 56daf678c43..f70b72e2151 100644
--- a/gopls/internal/settings/vet_test.go
+++ b/gopls/internal/settings/vet_test.go
@@ -41,7 +41,7 @@ func TestVetSuite(t *testing.T) {
out := fmt.Sprint(cmd.Stdout)
_, out, _ = strings.Cut(out, "Registered analyzers:\n\n")
out, _, _ = strings.Cut(out, "\n\n")
- for _, line := range strings.Split(out, "\n") {
+ for line := range strings.SplitSeq(out, "\n") {
name := strings.Fields(line)[0]
if !goplsAnalyzers[name] {
t.Errorf("gopls lacks vet analyzer %q", name)
diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go
index 7cb20012657..cb0a21b4ec2 100644
--- a/gopls/internal/telemetry/cmd/stacks/stacks.go
+++ b/gopls/internal/telemetry/cmd/stacks/stacks.go
@@ -479,11 +479,20 @@ func parsePredicate(s string) (func(string) bool, error) {
if err != nil {
return err
}
- // The literal should match complete words. It may match multiple words,
- // if it contains non-word runes like whitespace; but it must match word
- // boundaries at each end.
+ // The end of the literal (usually "symbol",
+ // "pkg.symbol", or "pkg.symbol:+1") must
+ // match a word boundary. However, the start
+ // of the literal need not: an input line such
+ // as "domain.name/dir/pkg.symbol:+1" should
+ // match literal "pkg.symbol", but the slash
+ // is not a word boundary (witness:
+ // https://go.dev/play/p/w-8ev_VUBSq).
+ //
+ // It may match multiple words if it contains
+ // non-word runes like whitespace.
+ //
// The constructed regular expression is always valid.
- literalRegexps[e] = regexp.MustCompile(`\b` + regexp.QuoteMeta(lit) + `\b`)
+ literalRegexps[e] = regexp.MustCompile(regexp.QuoteMeta(lit) + `\b`)
default:
return fmt.Errorf("syntax error (%T)", e)
@@ -520,7 +529,7 @@ func parsePredicate(s string) (func(string) bool, error) {
}, nil
}
-// claimStack maps each stack ID to its issue (if any).
+// claimStacks maps each stack ID to its issue (if any).
//
// It returns a map of stack text to the issue that claimed it.
//
@@ -732,7 +741,7 @@ func newIssue(pcfg ProgramConfig, stack, id, jsonURL string, counts map[Info]int
// lines around the PC in this symbol.
var symbol string
outer:
- for _, line := range strings.Split(stack, "\n") {
+ for line := range strings.SplitSeq(stack, "\n") {
for _, s := range pcfg.IgnoreSymbolContains {
if strings.Contains(line, s) {
continue outer // not interesting
@@ -805,7 +814,7 @@ func writeStackComment(body *bytes.Buffer, stack, id string, jsonURL string, cou
}
// Parse the stack and get the symbol names out.
- for _, frame := range strings.Split(stack, "\n") {
+ for frame := range strings.SplitSeq(stack, "\n") {
if url := frameURL(pclntab, info, frame); url != "" {
fmt.Fprintf(body, "- [`%s`](%s)\n", frame, url)
} else {
@@ -1084,6 +1093,8 @@ type Issue struct {
newStacks []string // new stacks to add to existing issue (comments and IDs)
}
+func (issue *Issue) String() string { return fmt.Sprintf("#%d", issue.Number) }
+
type User struct {
Login string
HTMLURL string `json:"html_url"`
diff --git a/gopls/internal/telemetry/cmd/stacks/stacks_test.go b/gopls/internal/telemetry/cmd/stacks/stacks_test.go
index 452113a1581..9f798aa43a3 100644
--- a/gopls/internal/telemetry/cmd/stacks/stacks_test.go
+++ b/gopls/internal/telemetry/cmd/stacks/stacks_test.go
@@ -85,13 +85,15 @@ func TestParsePredicate(t *testing.T) {
want bool
}{
{`"x"`, `"x"`, true},
- {`"x"`, `"axe"`, false}, // literals match whole words
+ {`"x"`, `"axe"`, false}, // literals must match word ends
+ {`"xe"`, `"axe"`, true},
{`"x"`, "val:x+5", true},
{`"fu+12"`, "x:fu+12,", true},
- {`"fu+12"`, "snafu+12,", false},
+ {`"fu+12"`, "snafu+12,", true}, // literals needn't match word start
{`"fu+12"`, "x:fu+123,", false},
- {`"a.*b"`, "a.*b", true}, // regexp metachars are escaped
- {`"a.*b"`, "axxb", false}, // ditto
+ {`"foo:+12"`, "dir/foo:+12,", true}, // literals needn't match word start
+ {`"a.*b"`, "a.*b", true}, // regexp metachars are escaped
+ {`"a.*b"`, "axxb", false}, // ditto
{`"x"`, `"y"`, false},
{`!"x"`, "x", false},
{`!"x"`, "y", true},
diff --git a/gopls/internal/telemetry/counterpath.go b/gopls/internal/telemetry/counterpath.go
new file mode 100644
index 00000000000..e6d9d84b531
--- /dev/null
+++ b/gopls/internal/telemetry/counterpath.go
@@ -0,0 +1,30 @@
+// Copyright 2025 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 telemetry
+
+import "strings"
+
+// A CounterPath represents the components of a telemetry counter name.
+//
+// By convention, counter names follow the format path/to/counter:bucket. The
+// CounterPath holds the '/'-separated components of this path, along with a
+// final element representing the bucket.
+//
+// CounterPaths may be used to build up counters incrementally, such as when a
+// set of observed counters shared a common prefix, to be controlled by the
+// caller.
+type CounterPath []string
+
+// FullName returns the counter name for the receiver.
+func (p CounterPath) FullName() string {
+ if len(p) == 0 {
+ return ""
+ }
+ name := strings.Join([]string(p[:len(p)-1]), "/")
+ if bucket := p[len(p)-1]; bucket != "" {
+ name += ":" + bucket
+ }
+ return name
+}
diff --git a/gopls/internal/telemetry/counterpath_test.go b/gopls/internal/telemetry/counterpath_test.go
new file mode 100644
index 00000000000..b6ac7478b72
--- /dev/null
+++ b/gopls/internal/telemetry/counterpath_test.go
@@ -0,0 +1,47 @@
+// Copyright 2025 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 telemetry
+
+import (
+ "testing"
+)
+
+// TestCounterPath tests the formatting of various counter paths.
+func TestCounterPath(t *testing.T) {
+ tests := []struct {
+ path CounterPath
+ want string
+ }{
+ {
+ path: CounterPath{},
+ want: "",
+ },
+ {
+ path: CounterPath{"counter"},
+ want: ":counter",
+ },
+ {
+ path: CounterPath{"counter", "bucket"},
+ want: "counter:bucket",
+ },
+ {
+ path: CounterPath{"path", "to", "counter"},
+ want: "path/to:counter",
+ },
+ {
+ path: CounterPath{"multi", "component", "path", "bucket"},
+ want: "multi/component/path:bucket",
+ },
+ {
+ path: CounterPath{"path", ""},
+ want: "path",
+ },
+ }
+ for _, tt := range tests {
+ if got := tt.path.FullName(); got != tt.want {
+ t.Errorf("CounterPath(%v).FullName() = %v, want %v", tt.path, got, tt.want)
+ }
+ }
+}
diff --git a/gopls/internal/telemetry/telemetry_test.go b/gopls/internal/telemetry/telemetry_test.go
index 7aaca41ab55..1e56012182f 100644
--- a/gopls/internal/telemetry/telemetry_test.go
+++ b/gopls/internal/telemetry/telemetry_test.go
@@ -119,12 +119,56 @@ func TestTelemetry(t *testing.T) {
}
}
+func TestSettingTelemetry(t *testing.T) {
+ // counters that should be incremented by each session
+ sessionCounters := []*counter.Counter{
+ counter.New("gopls/setting/diagnosticsDelay"),
+ counter.New("gopls/setting/staticcheck:true"),
+ counter.New("gopls/setting/noSemanticString:true"),
+ counter.New("gopls/setting/analyses/deprecated:false"),
+ }
+
+ initialCounts := make([]uint64, len(sessionCounters))
+ for i, c := range sessionCounters {
+ count, err := countertest.ReadCounter(c)
+ if err != nil {
+ continue // counter db not open, or counter not found
+ }
+ initialCounts[i] = count
+ }
+
+ // Run gopls.
+ WithOptions(
+ Modes(Default),
+ Settings{
+ "staticcheck": true,
+ "analyses": map[string]bool{
+ "deprecated": false,
+ },
+ "diagnosticsDelay": "0s",
+ "noSemanticString": true,
+ },
+ ).Run(t, "", func(_ *testing.T, env *Env) {
+ })
+
+ for i, c := range sessionCounters {
+ count, err := countertest.ReadCounter(c)
+ if err != nil {
+ t.Errorf("ReadCounter(%q) failed: %v", c.Name(), err)
+ continue
+ }
+ if count <= initialCounts[i] {
+ t.Errorf("ReadCounter(%q) = %d, want > %d", c.Name(), count, initialCounts[i])
+ }
+ }
+}
+
func addForwardedCounters(env *Env, names []string, values []int64) {
args, err := command.MarshalArgs(command.AddTelemetryCountersArgs{
Names: names, Values: values,
})
if err != nil {
- env.T.Fatal(err)
+ env.TB.Fatal(err)
}
var res error
env.ExecuteCommand(&protocol.ExecuteCommandParams{
@@ -132,7 +176,7 @@ func addForwardedCounters(env *Env, names []string, values []int64) {
Arguments: args,
}, &res)
if res != nil {
- env.T.Errorf("%v failed - %v", command.AddTelemetryCounters, res)
+ env.TB.Errorf("%v failed - %v", command.AddTelemetryCounters, res)
}
}
diff --git a/gopls/internal/template/highlight.go b/gopls/internal/template/highlight.go
index 39812cfd0ba..c6b0c0f778e 100644
--- a/gopls/internal/template/highlight.go
+++ b/gopls/internal/template/highlight.go
@@ -70,7 +70,7 @@ func markWordInToken(p *Parsed, wordAt string) ([]protocol.DocumentHighlight, er
}
for _, tok := range p.tokens {
got := pat.FindAllIndex(p.buf[tok.Start:tok.End], -1)
- for i := 0; i < len(got); i++ {
+ for i := range got {
ans = append(ans, protocol.DocumentHighlight{
Range: p.Range(got[i][0], got[i][1]-got[i][0]),
Kind: protocol.Text,
diff --git a/gopls/internal/template/parse.go b/gopls/internal/template/parse.go
index 448a5ab51e8..f1b26bbb14f 100644
--- a/gopls/internal/template/parse.go
+++ b/gopls/internal/template/parse.go
@@ -114,7 +114,7 @@ func parseBuffer(buf []byte) *Parsed {
matches := parseErrR.FindStringSubmatch(err.Error())
if len(matches) == 2 {
// suppress the error by giving it a function with the right name
- funcs[matches[1]] = func() interface{} { return nil }
+ funcs[matches[1]] = func() any { return nil }
t, err = template.New("").Funcs(funcs).Parse(string(ans.buf))
continue
}
diff --git a/gopls/internal/test/integration/bench/codeaction_test.go b/gopls/internal/test/integration/bench/codeaction_test.go
index 679f2d4cf3d..4bba9e6f317 100644
--- a/gopls/internal/test/integration/bench/codeaction_test.go
+++ b/gopls/internal/test/integration/bench/codeaction_test.go
@@ -28,7 +28,7 @@ func BenchmarkCodeAction(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
env.CodeActionForFile(test.file, nil)
}
})
@@ -52,7 +52,7 @@ func BenchmarkCodeActionFollowingEdit(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
edits := atomic.AddInt64(&editID, 1)
env.EditBuffer(test.file, protocol.TextEdit{
Range: protocol.Range{
diff --git a/gopls/internal/test/integration/bench/completion_test.go b/gopls/internal/test/integration/bench/completion_test.go
index bbbba0e3fd1..2140e30d123 100644
--- a/gopls/internal/test/integration/bench/completion_test.go
+++ b/gopls/internal/test/integration/bench/completion_test.go
@@ -53,7 +53,7 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
if options.beforeCompletion != nil {
options.beforeCompletion(env)
}
@@ -69,7 +69,7 @@ func endRangeInBuffer(env *Env, name string) protocol.Range {
m := protocol.NewMapper("", []byte(buffer))
rng, err := m.OffsetRange(len(buffer), len(buffer))
if err != nil {
- env.T.Fatal(err)
+ env.TB.Fatal(err)
}
return rng
}
@@ -282,7 +282,7 @@ func runCompletion(b *testing.B, test completionTest, followingEdit, completeUni
env := repo.newEnv(b, fake.EditorConfig{
Env: envvars,
- Settings: map[string]interface{}{
+ Settings: map[string]any{
"completeUnimported": completeUnimported,
"completionBudget": budget,
},
@@ -314,13 +314,11 @@ func runCompletion(b *testing.B, test completionTest, followingEdit, completeUni
}
}
- b.ResetTimer()
-
if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "completion")); stopAndRecord != nil {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
if followingEdit {
editPlaceholder()
}
diff --git a/gopls/internal/test/integration/bench/definition_test.go b/gopls/internal/test/integration/bench/definition_test.go
index b703378a27b..e456d5a7c87 100644
--- a/gopls/internal/test/integration/bench/definition_test.go
+++ b/gopls/internal/test/integration/bench/definition_test.go
@@ -38,7 +38,7 @@ func BenchmarkDefinition(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
env.GoToDefinition(loc) // pre-warm the query
}
})
diff --git a/gopls/internal/test/integration/bench/diagnostic_test.go b/gopls/internal/test/integration/bench/diagnostic_test.go
index ce8a84d9eb2..6dd00afd5d8 100644
--- a/gopls/internal/test/integration/bench/diagnostic_test.go
+++ b/gopls/internal/test/integration/bench/diagnostic_test.go
@@ -58,9 +58,7 @@ func BenchmarkDiagnosePackageFiles(b *testing.B) {
defer stopAndRecord()
}
- b.ResetTimer()
-
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
edit()
var wg sync.WaitGroup
for _, file := range files {
diff --git a/gopls/internal/test/integration/bench/didchange_test.go b/gopls/internal/test/integration/bench/didchange_test.go
index 57ed01bbcd6..aa87a4f9b0e 100644
--- a/gopls/internal/test/integration/bench/didchange_test.go
+++ b/gopls/internal/test/integration/bench/didchange_test.go
@@ -56,7 +56,7 @@ func BenchmarkDidChange(b *testing.B) {
}
b.ResetTimer()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
edit()
env.Await(env.StartedChange())
}
@@ -118,7 +118,7 @@ func runChangeDiagnosticsBenchmark(b *testing.B, test changeTest, save bool, ope
Env: map[string]string{
"GOPATH": sharedEnv.Sandbox.GOPATH(),
},
- Settings: map[string]interface{}{
+ Settings: map[string]any{
"diagnosticsDelay": "0s",
},
}
@@ -142,7 +142,7 @@ func runChangeDiagnosticsBenchmark(b *testing.B, test changeTest, save bool, ope
if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, operation)); stopAndRecord != nil {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
edits := atomic.AddInt64(&editID, 1)
env.EditBuffer(test.file, protocol.TextEdit{
Range: protocol.Range{
diff --git a/gopls/internal/test/integration/bench/hover_test.go b/gopls/internal/test/integration/bench/hover_test.go
index c3b0c6bc0cb..07a60c354f7 100644
--- a/gopls/internal/test/integration/bench/hover_test.go
+++ b/gopls/internal/test/integration/bench/hover_test.go
@@ -39,7 +39,7 @@ func BenchmarkHover(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
env.Hover(loc) // pre-warm the query
}
})
diff --git a/gopls/internal/test/integration/bench/implementations_test.go b/gopls/internal/test/integration/bench/implementations_test.go
index b7e08aa3141..0c3acca89b1 100644
--- a/gopls/internal/test/integration/bench/implementations_test.go
+++ b/gopls/internal/test/integration/bench/implementations_test.go
@@ -36,7 +36,7 @@ func BenchmarkImplementations(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
env.Implementations(loc)
}
})
diff --git a/gopls/internal/test/integration/bench/imports_test.go b/gopls/internal/test/integration/bench/imports_test.go
index 97419cb10c5..3f47a561681 100644
--- a/gopls/internal/test/integration/bench/imports_test.go
+++ b/gopls/internal/test/integration/bench/imports_test.go
@@ -29,9 +29,7 @@ func BenchmarkInitialGoimportsScan(b *testing.B) {
repo := getRepo(b, "tools") // since this a test of module cache scanning, any repo will do
- b.ResetTimer()
-
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
func() {
// Unfortunately we (intentionally) don't support resetting the module
// cache scan state, so in order to have an accurate benchmark we must
diff --git a/gopls/internal/test/integration/bench/iwl_test.go b/gopls/internal/test/integration/bench/iwl_test.go
index 09ccb301a58..0f94b6a3857 100644
--- a/gopls/internal/test/integration/bench/iwl_test.go
+++ b/gopls/internal/test/integration/bench/iwl_test.go
@@ -41,7 +41,7 @@ func BenchmarkInitialWorkspaceLoad(b *testing.B) {
sharedEnv := repo.sharedEnv(b)
b.ResetTimer()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
doIWL(b, sharedEnv.Sandbox.GOPATH(), repo, nil)
}
})
@@ -61,7 +61,7 @@ func BenchmarkInitialWorkspaceLoadOpenFiles(b *testing.B) {
sharedEnv := repo.sharedEnv(b)
b.ResetTimer()
- for range b.N {
+ for b.Loop() {
doIWL(b, sharedEnv.Sandbox.GOPATH(), repo, []string{t.file})
}
})
diff --git a/gopls/internal/test/integration/bench/references_test.go b/gopls/internal/test/integration/bench/references_test.go
index aeaba6f5683..7a4152a8b70 100644
--- a/gopls/internal/test/integration/bench/references_test.go
+++ b/gopls/internal/test/integration/bench/references_test.go
@@ -36,7 +36,7 @@ func BenchmarkReferences(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
env.References(loc)
}
})
diff --git a/gopls/internal/test/integration/bench/reload_test.go b/gopls/internal/test/integration/bench/reload_test.go
index b93b76f945d..1a40cc5eba1 100644
--- a/gopls/internal/test/integration/bench/reload_test.go
+++ b/gopls/internal/test/integration/bench/reload_test.go
@@ -48,7 +48,7 @@ func BenchmarkReload(b *testing.B) {
}
b.ResetTimer()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
// Mutate the file. This may result in cache hits, but that's OK: the
// goal is to ensure that we don't reload more than just the current
// package.
diff --git a/gopls/internal/test/integration/bench/rename_test.go b/gopls/internal/test/integration/bench/rename_test.go
index ca5ed5f4397..32cbace5faa 100644
--- a/gopls/internal/test/integration/bench/rename_test.go
+++ b/gopls/internal/test/integration/bench/rename_test.go
@@ -39,7 +39,7 @@ func BenchmarkRename(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
names++
newName := fmt.Sprintf("%s%d", test.baseName, names)
env.Rename(loc, newName)
diff --git a/gopls/internal/test/integration/bench/repo_test.go b/gopls/internal/test/integration/bench/repo_test.go
index 50370e73491..65728c00552 100644
--- a/gopls/internal/test/integration/bench/repo_test.go
+++ b/gopls/internal/test/integration/bench/repo_test.go
@@ -211,7 +211,7 @@ func (r *repo) sharedEnv(tb testing.TB) *Env {
})
return &Env{
- T: tb,
+ TB: tb,
Ctx: context.Background(),
Editor: r.editor,
Sandbox: r.sandbox,
@@ -238,7 +238,7 @@ func (r *repo) newEnv(tb testing.TB, config fake.EditorConfig, forOperation stri
}
return &Env{
- T: tb,
+ TB: tb,
Ctx: context.Background(),
Editor: editor,
Sandbox: sandbox,
diff --git a/gopls/internal/test/integration/bench/tests_test.go b/gopls/internal/test/integration/bench/tests_test.go
index 3bc69ef95e1..77ba88c7156 100644
--- a/gopls/internal/test/integration/bench/tests_test.go
+++ b/gopls/internal/test/integration/bench/tests_test.go
@@ -75,7 +75,7 @@ func BenchmarkPackagesCommand(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
executePackagesCmd(b, env, args)
}
})
diff --git a/gopls/internal/test/integration/bench/typing_test.go b/gopls/internal/test/integration/bench/typing_test.go
index 78bd16cef5b..b32e707858f 100644
--- a/gopls/internal/test/integration/bench/typing_test.go
+++ b/gopls/internal/test/integration/bench/typing_test.go
@@ -41,7 +41,7 @@ func BenchmarkTyping(b *testing.B) {
defer stopAndRecord()
}
ticker := time.NewTicker(delay)
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
edits := atomic.AddInt64(&editID, 1)
env.EditBuffer(test.file, protocol.TextEdit{
Range: protocol.Range{
diff --git a/gopls/internal/test/integration/bench/workspace_symbols_test.go b/gopls/internal/test/integration/bench/workspace_symbols_test.go
index d3e1d207b2d..fb914563191 100644
--- a/gopls/internal/test/integration/bench/workspace_symbols_test.go
+++ b/gopls/internal/test/integration/bench/workspace_symbols_test.go
@@ -35,7 +35,7 @@ func BenchmarkWorkspaceSymbols(b *testing.B) {
defer stopAndRecord()
}
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
env.Symbol(*symbolQuery)
}
})
diff --git a/gopls/internal/test/integration/completion/completion_test.go b/gopls/internal/test/integration/completion/completion_test.go
index 1d293fe9019..8fa03908c01 100644
--- a/gopls/internal/test/integration/completion/completion_test.go
+++ b/gopls/internal/test/integration/completion/completion_test.go
@@ -53,6 +53,10 @@ func TestPackageCompletion(t *testing.T) {
module mod.com
go 1.12
+-- cmd/main.go --
+package main
+-- cmd/testfile.go --
+package
-- fruits/apple.go --
package apple
@@ -95,6 +99,13 @@ package
want []string
editRegexp string
}{
+ {
+ name: "main package completion after package keyword",
+ filename: "cmd/testfile.go",
+ triggerRegexp: "package()",
+ want: []string{"package main", "package cmd", "package cmd_test"},
+ editRegexp: "package",
+ },
{
name: "package completion at valid position",
filename: "fruits/testfile.go",
@@ -1212,25 +1223,21 @@ func TestDoubleParamReturnCompletion(t *testing.T) {
Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
- compl := env.RegexpSearch("a.go", `DoubleWrap\[()\]\(\)`)
- result := env.Completion(compl)
-
- wantLabel := []string{"InterfaceA", "TypeA", "InterfaceB", "TypeB", "TypeC"}
-
- for i, item := range result.Items[:len(wantLabel)] {
- if diff := cmp.Diff(wantLabel[i], item.Label); diff != "" {
- t.Errorf("Completion: unexpected label mismatch (-want +got):\n%s", diff)
- }
+ tests := map[string][]string{
+ `DoubleWrap\[()\]\(\)`: {"InterfaceA", "TypeA", "InterfaceB", "TypeB", "TypeC"},
+ `DoubleWrap\[InterfaceA, (_)\]\(\)`: {"InterfaceB", "TypeB", "TypeX", "InterfaceA", "TypeA"},
}
- compl = env.RegexpSearch("a.go", `DoubleWrap\[InterfaceA, (_)\]\(\)`)
- result = env.Completion(compl)
-
- wantLabel = []string{"InterfaceB", "TypeB", "TypeX", "InterfaceA", "TypeA"}
-
- for i, item := range result.Items[:len(wantLabel)] {
- if diff := cmp.Diff(wantLabel[i], item.Label); diff != "" {
- t.Errorf("Completion: unexpected label mismatch (-want +got):\n%s", diff)
+ for re, wantLabels := range tests {
+ compl := env.RegexpSearch("a.go", re)
+ result := env.Completion(compl)
+ if len(result.Items) < len(wantLabels) {
+ t.Fatalf("Completion(%q) returned mismatching labels: got %v, want at least labels %v", re, result.Items, wantLabels)
+ }
+ for i, item := range result.Items[:len(wantLabels)] {
+ if diff := cmp.Diff(wantLabels[i], item.Label); diff != "" {
+ t.Errorf("Completion(%q): unexpected label mismatch (-want +got):\n%s", re, diff)
+ }
}
}
})
diff --git a/gopls/internal/test/integration/completion/fixedbugs_test.go b/gopls/internal/test/integration/completion/fixedbugs_test.go
index faa5324e138..ccec432904e 100644
--- a/gopls/internal/test/integration/completion/fixedbugs_test.go
+++ b/gopls/internal/test/integration/completion/fixedbugs_test.go
@@ -38,3 +38,20 @@ package
}
})
}
+
+func TestFixInitStatementCrash_Issue72026(t *testing.T) {
+ // This test checks that we don't crash when the if condition overflows the
+ // file (as is possible with a malformed struct type).
+
+ const files = `
+-- go.mod --
+module example.com
+
+go 1.18
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.CreateBuffer("p.go", "package p\nfunc _() {\n\tfor i := struct")
+ env.AfterChange()
+ })
+}
diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go
index c496f6464a3..5ef39a5f0c5 100644
--- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go
+++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go
@@ -542,27 +542,34 @@ var X = 0
// Tests golang/go#38467.
func TestNoSuggestedFixesForGeneratedFiles_Issue38467(t *testing.T) {
+ // This test ensures that gopls' CodeAction handler suppresses
+ // diagnostics in generated code. Beware that many analyzers
+ // themselves suppress diagnostics in generated files, in
+ // particular the low-status "simplifiers" (modernize,
+ // simplify{range,slice,compositelit}), so we use the hostport
+ // analyzer here.
const generated = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
+// Code generated by generator.go. DO NOT EDIT.
+
package main
-// Code generated by generator.go. DO NOT EDIT.
+import ("fmt"; "net")
func _() {
- for i, _ := range []string{} {
- _ = i
- }
+ addr := fmt.Sprintf("%s:%d", "localhost", 12345)
+ net.Dial("tcp", addr)
}
`
Run(t, generated, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
var d protocol.PublishDiagnosticsParams
env.AfterChange(
- Diagnostics(AtPosition("main.go", 5, 8)),
+ Diagnostics(AtPosition("main.go", 7, 21)),
ReadDiagnostics("main.go", &d),
)
if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 {
diff --git a/gopls/internal/test/integration/diagnostics/invalidation_test.go b/gopls/internal/test/integration/diagnostics/invalidation_test.go
index e8d39c3c38a..0ee23eda003 100644
--- a/gopls/internal/test/integration/diagnostics/invalidation_test.go
+++ b/gopls/internal/test/integration/diagnostics/invalidation_test.go
@@ -82,7 +82,7 @@ func _() {
}
msg := d.Diagnostics[0].Message
- for i := 0; i < 5; i++ {
+ for i := range 5 {
before := d.Version
env.RegexpReplace("main.go", "Irrelevant comment #.", fmt.Sprintf("Irrelevant comment #%d", i))
env.AfterChange(
diff --git a/gopls/internal/test/integration/diagnostics/undeclared_test.go b/gopls/internal/test/integration/diagnostics/undeclared_test.go
index 5579c0752d7..2b399f52f3c 100644
--- a/gopls/internal/test/integration/diagnostics/undeclared_test.go
+++ b/gopls/internal/test/integration/diagnostics/undeclared_test.go
@@ -5,6 +5,7 @@
package diagnostics
import (
+ "slices"
"testing"
"golang.org/x/tools/gopls/internal/protocol"
@@ -34,12 +35,7 @@ func _() int {
`
Run(t, src, func(t *testing.T, env *Env) {
isUnnecessary := func(diag protocol.Diagnostic) bool {
- for _, tag := range diag.Tags {
- if tag == protocol.Unnecessary {
- return true
- }
- }
- return false
+ return slices.Contains(diag.Tags, protocol.Unnecessary)
}
// 'x' is undeclared, but still necessary.
diff --git a/gopls/internal/test/integration/env.go b/gopls/internal/test/integration/env.go
index 64344d0d146..822120e8324 100644
--- a/gopls/internal/test/integration/env.go
+++ b/gopls/internal/test/integration/env.go
@@ -21,7 +21,7 @@ import (
// wrapper methods that hide the boilerplate of plumbing contexts and checking
// errors.
type Env struct {
- T testing.TB // TODO(rfindley): rename to TB
+ TB testing.TB
Ctx context.Context
// Most tests should not need to access the scratch area, editor, server, or
@@ -114,53 +114,16 @@ type workProgress struct {
complete bool // seen 'end'
}
-// This method, provided for debugging, accesses mutable fields without a lock,
-// so it must not be called concurrent with any State mutation.
-func (s State) String() string {
- var b strings.Builder
- b.WriteString("#### log messages (see RPC logs for full text):\n")
- for _, msg := range s.logs {
- summary := fmt.Sprintf("%v: %q", msg.Type, msg.Message)
- if len(summary) > 60 {
- summary = summary[:57] + "..."
- }
- // Some logs are quite long, and since they should be reproduced in the RPC
- // logs on any failure we include here just a short summary.
- fmt.Fprint(&b, "\t"+summary+"\n")
- }
- b.WriteString("\n")
- b.WriteString("#### diagnostics:\n")
- for name, params := range s.diagnostics {
- fmt.Fprintf(&b, "\t%s (version %d):\n", name, params.Version)
- for _, d := range params.Diagnostics {
- fmt.Fprintf(&b, "\t\t%d:%d [%s]: %s\n", d.Range.Start.Line, d.Range.Start.Character, d.Source, d.Message)
- }
- }
- b.WriteString("\n")
- b.WriteString("#### outstanding work:\n")
- for token, state := range s.work {
- if state.complete {
- continue
- }
- name := state.title
- if name == "" {
- name = fmt.Sprintf("!NO NAME(token: %s)", token)
- }
- fmt.Fprintf(&b, "\t%s: %.2f\n", name, state.percent)
- }
- b.WriteString("#### completed work:\n")
- for name, count := range s.completedWork {
- fmt.Fprintf(&b, "\t%s: %d\n", name, count)
- }
- return b.String()
+type awaitResult struct {
+ verdict Verdict
+ reason string
}
-// A condition is satisfied when all expectations are simultaneously
-// met. At that point, the 'met' channel is closed. On any failure, err is set
-// and the failed channel is closed.
+// A condition is satisfied when its expectation is [Met] or [Unmeetable]. The
+// result is sent on the verdict channel.
type condition struct {
- expectations []Expectation
- verdict chan Verdict
+ expectation Expectation
+ verdict chan awaitResult
}
func (a *Awaiter) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error {
@@ -282,7 +245,7 @@ func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) erro
if !ok {
panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m))
}
- v := m.Value.(map[string]interface{})
+ v := m.Value.(map[string]any)
switch kind := v["kind"]; kind {
case "begin":
work.title = v["title"].(string)
@@ -334,27 +297,13 @@ func (a *Awaiter) onUnregisterCapability(_ context.Context, m *protocol.Unregist
func (a *Awaiter) checkConditionsLocked() {
for id, condition := range a.waiters {
- if v, _ := checkExpectations(a.state, condition.expectations); v != Unmet {
+ if v, why := condition.expectation.Check(a.state); v != Unmet {
delete(a.waiters, id)
- condition.verdict <- v
+ condition.verdict <- awaitResult{v, why}
}
}
}
-// checkExpectations reports whether s meets all expectations.
-func checkExpectations(s State, expectations []Expectation) (Verdict, string) {
- finalVerdict := Met
- var summary strings.Builder
- for _, e := range expectations {
- v := e.Check(s)
- if v > finalVerdict {
- finalVerdict = v
- }
- fmt.Fprintf(&summary, "%v: %s\n", v, e.Description)
- }
- return finalVerdict, summary.String()
-}
-
// Await blocks until the given expectations are all simultaneously met.
//
// Generally speaking Await should be avoided because it blocks indefinitely if
@@ -362,39 +311,39 @@ func checkExpectations(s State, expectations []Expectation) (Verdict, string) {
// Use AfterChange or OnceMet instead, so that the runner knows when to stop
// waiting.
func (e *Env) Await(expectations ...Expectation) {
- e.T.Helper()
- if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil {
- e.T.Fatal(err)
+ e.TB.Helper()
+ if err := e.Awaiter.Await(e.Ctx, AllOf(expectations...)); err != nil {
+ e.TB.Fatal(err)
}
}
// OnceMet blocks until the precondition is met by the state or becomes
// unmeetable. If it was met, OnceMet checks that the state meets all
// expectations in mustMeets.
-func (e *Env) OnceMet(precondition Expectation, mustMeets ...Expectation) {
- e.T.Helper()
- e.Await(OnceMet(precondition, mustMeets...))
+func (e *Env) OnceMet(pre Expectation, mustMeets ...Expectation) {
+ e.TB.Helper()
+ e.Await(OnceMet(pre, AllOf(mustMeets...)))
}
// Await waits for all expectations to simultaneously be met. It should only be
// called from the main test goroutine.
-func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error {
+func (a *Awaiter) Await(ctx context.Context, expectation Expectation) error {
a.mu.Lock()
// Before adding the waiter, we check if the condition is currently met or
// failed to avoid a race where the condition was realized before Await was
// called.
- switch verdict, summary := checkExpectations(a.state, expectations); verdict {
+ switch verdict, why := expectation.Check(a.state); verdict {
case Met:
a.mu.Unlock()
return nil
case Unmeetable:
- err := fmt.Errorf("unmeetable expectations:\n%s\nstate:\n%v", summary, a.state)
+ err := fmt.Errorf("unmeetable expectation:\n%s\nreason:\n%s", indent(expectation.Description), indent(why))
a.mu.Unlock()
return err
}
cond := &condition{
- expectations: expectations,
- verdict: make(chan Verdict),
+ expectation: expectation,
+ verdict: make(chan awaitResult),
}
a.waiters[nextAwaiterRegistration.Add(1)] = cond
a.mu.Unlock()
@@ -403,19 +352,17 @@ func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error
select {
case <-ctx.Done():
err = ctx.Err()
- case v := <-cond.verdict:
- if v != Met {
- err = fmt.Errorf("condition has final verdict %v", v)
+ case res := <-cond.verdict:
+ if res.verdict != Met {
+ err = fmt.Errorf("the following condition is %s:\n%s\nreason:\n%s",
+ res.verdict, indent(expectation.Description), indent(res.reason))
}
}
- a.mu.Lock()
- defer a.mu.Unlock()
- _, summary := checkExpectations(a.state, expectations)
+ return err
+}
- // Debugging an unmet expectation can be tricky, so we put some effort into
- // nicely formatting the failure.
- if err != nil {
- return fmt.Errorf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, a.state)
- }
- return nil
+// indent indents all lines of msg, including the first.
+func indent(msg string) string {
+ const prefix = " "
+ return prefix + strings.ReplaceAll(msg, "\n", "\n"+prefix)
}
diff --git a/gopls/internal/test/integration/env_test.go b/gopls/internal/test/integration/env_test.go
index 32203f7cb83..1fa68676b5c 100644
--- a/gopls/internal/test/integration/env_test.go
+++ b/gopls/internal/test/integration/env_test.go
@@ -33,7 +33,7 @@ func TestProgressUpdating(t *testing.T) {
}
updates := []struct {
token string
- value interface{}
+ value any
}{
{"foo", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "foo work"}},
{"bar", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "bar work"}},
diff --git a/gopls/internal/test/integration/expectation.go b/gopls/internal/test/integration/expectation.go
index ad41423d098..98554ddccc3 100644
--- a/gopls/internal/test/integration/expectation.go
+++ b/gopls/internal/test/integration/expectation.go
@@ -5,14 +5,17 @@
package integration
import (
+ "bytes"
"fmt"
+ "maps"
"regexp"
- "sort"
+ "slices"
"strings"
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/server"
+ "golang.org/x/tools/gopls/internal/util/constraints"
)
var (
@@ -55,16 +58,11 @@ func (v Verdict) String() string {
//
// Expectations are combinators. By composing them, tests may express
// complex expectations in terms of simpler ones.
-//
-// TODO(rfindley): as expectations are combined, it becomes harder to identify
-// why they failed. A better signature for Check would be
-//
-// func(State) (Verdict, string)
-//
-// returning a reason for the verdict that can be composed similarly to
-// descriptions.
type Expectation struct {
- Check func(State) Verdict
+ // Check returns the verdict of this expectation for the given state.
+ // If the vertict is not [Met], the second result should return a reason
+ // that the verdict is not (yet) met.
+ Check func(State) (Verdict, string)
// Description holds a noun-phrase identifying what the expectation checks.
//
@@ -74,117 +72,117 @@ type Expectation struct {
// OnceMet returns an Expectation that, once the precondition is met, asserts
// that mustMeet is met.
-func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation {
- check := func(s State) Verdict {
- switch pre := precondition.Check(s); pre {
- case Unmeetable:
- return Unmeetable
+func OnceMet(pre, post Expectation) Expectation {
+ check := func(s State) (Verdict, string) {
+ switch v, why := pre.Check(s); v {
+ case Unmeetable, Unmet:
+ return v, fmt.Sprintf("precondition is %s: %s", v, why)
case Met:
- for _, mustMeet := range mustMeets {
- verdict := mustMeet.Check(s)
- if verdict != Met {
- return Unmeetable
- }
+ v, why := post.Check(s)
+ if v != Met {
+ return Unmeetable, fmt.Sprintf("postcondition is not met:\n%s", indent(why))
}
- return Met
+ return Met, ""
default:
- return Unmet
+ panic(fmt.Sprintf("unknown precondition verdict %s", v))
}
}
- description := describeExpectations(mustMeets...)
+ desc := fmt.Sprintf("once the following is met:\n%s\nmust have:\n%s",
+ indent(pre.Description), indent(post.Description))
return Expectation{
Check: check,
- Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description),
- }
-}
-
-func describeExpectations(expectations ...Expectation) string {
- var descriptions []string
- for _, e := range expectations {
- descriptions = append(descriptions, e.Description)
+ Description: desc,
}
- return strings.Join(descriptions, "\n")
}
// Not inverts the sense of an expectation: a met expectation is unmet, and an
// unmet expectation is met.
func Not(e Expectation) Expectation {
- check := func(s State) Verdict {
- switch v := e.Check(s); v {
+ check := func(s State) (Verdict, string) {
+ switch v, _ := e.Check(s); v {
case Met:
- return Unmet
+ return Unmet, "condition unexpectedly satisfied"
case Unmet, Unmeetable:
- return Met
+ return Met, ""
default:
panic(fmt.Sprintf("unexpected verdict %v", v))
}
}
- description := describeExpectations(e)
return Expectation{
Check: check,
- Description: fmt.Sprintf("not: %s", description),
+ Description: fmt.Sprintf("not: %s", e.Description),
}
}
// AnyOf returns an expectation that is satisfied when any of the given
// expectations is met.
func AnyOf(anyOf ...Expectation) Expectation {
- check := func(s State) Verdict {
+ if len(anyOf) == 1 {
+ return anyOf[0] // avoid unnecessary boilerplate
+ }
+ check := func(s State) (Verdict, string) {
for _, e := range anyOf {
- verdict := e.Check(s)
+ verdict, _ := e.Check(s)
if verdict == Met {
- return Met
+ return Met, ""
}
}
- return Unmet
+ return Unmet, "none of the expectations were met"
}
description := describeExpectations(anyOf...)
return Expectation{
Check: check,
- Description: fmt.Sprintf("Any of:\n%s", description),
+ Description: fmt.Sprintf("any of:\n%s", description),
}
}
// AllOf expects that all given expectations are met.
-//
-// TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf
-// and AllOf) is that we lose the information of *why* they failed: the Awaiter
-// is not smart enough to look inside.
-//
-// Refactor the API such that the Check function is responsible for explaining
-// why an expectation failed. This should allow us to significantly improve
-// test output: we won't need to summarize state at all, as the verdict
-// explanation itself should describe clearly why the expectation not met.
func AllOf(allOf ...Expectation) Expectation {
- check := func(s State) Verdict {
- verdict := Met
+ if len(allOf) == 1 {
+ return allOf[0] // avoid unnecessary boilerplate
+ }
+ check := func(s State) (Verdict, string) {
+ var (
+ verdict = Met
+ reason string
+ )
for _, e := range allOf {
- if v := e.Check(s); v > verdict {
+ v, why := e.Check(s)
+ if v > verdict {
verdict = v
+ reason = why
}
}
- return verdict
+ return verdict, reason
}
- description := describeExpectations(allOf...)
+ desc := describeExpectations(allOf...)
return Expectation{
Check: check,
- Description: fmt.Sprintf("All of:\n%s", description),
+ Description: fmt.Sprintf("all of:\n%s", indent(desc)),
}
}
+func describeExpectations(expectations ...Expectation) string {
+ var descriptions []string
+ for _, e := range expectations {
+ descriptions = append(descriptions, e.Description)
+ }
+ return strings.Join(descriptions, "\n")
+}
+
// ReadDiagnostics is an Expectation that stores the current diagnostics for
// fileName in into, whenever it is evaluated.
//
// It can be used in combination with OnceMet or AfterChange to capture the
// state of diagnostics when other expectations are satisfied.
func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
diags, ok := s.diagnostics[fileName]
if !ok {
- return Unmeetable
+ return Unmeetable, fmt.Sprintf("no diagnostics for %q", fileName)
}
*into = *diags
- return Met
+ return Met, ""
}
return Expectation{
Check: check,
@@ -198,13 +196,10 @@ func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) E
// It can be used in combination with OnceMet or AfterChange to capture the
// state of diagnostics when other expectations are satisfied.
func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Expectation {
- check := func(s State) Verdict {
- allDiags := make(map[string]*protocol.PublishDiagnosticsParams)
- for name, diags := range s.diagnostics {
- allDiags[name] = diags
- }
+ check := func(s State) (Verdict, string) {
+ allDiags := maps.Clone(s.diagnostics)
*into = allDiags
- return Met
+ return Met, ""
}
return Expectation{
Check: check,
@@ -215,13 +210,13 @@ func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Exp
// ShownDocument asserts that the client has received a
// ShowDocumentRequest for the given URI.
func ShownDocument(uri protocol.URI) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
for _, params := range s.showDocument {
if params.URI == uri {
- return Met
+ return Met, ""
}
}
- return Unmet
+ return Unmet, fmt.Sprintf("no ShowDocumentRequest received for %s", uri)
}
return Expectation{
Check: check,
@@ -236,9 +231,9 @@ func ShownDocument(uri protocol.URI) Expectation {
// capture the set of showDocument requests when other expectations
// are satisfied.
func ShownDocuments(into *[]*protocol.ShowDocumentParams) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
*into = append(*into, s.showDocument...)
- return Met
+ return Met, ""
}
return Expectation{
Check: check,
@@ -247,31 +242,39 @@ func ShownDocuments(into *[]*protocol.ShowDocumentParams) Expectation {
}
// NoShownMessage asserts that the editor has not received a ShowMessage.
-func NoShownMessage(subString string) Expectation {
- check := func(s State) Verdict {
+func NoShownMessage(containing string) Expectation {
+ check := func(s State) (Verdict, string) {
for _, m := range s.showMessage {
- if strings.Contains(m.Message, subString) {
- return Unmeetable
+ if strings.Contains(m.Message, containing) {
+ // Format the message (which may contain newlines) as a block quote.
+ msg := fmt.Sprintf("\"\"\"\n%s\n\"\"\"", strings.TrimSpace(m.Message))
+ return Unmeetable, fmt.Sprintf("observed the following message:\n%s", indent(msg))
}
}
- return Met
+ return Met, ""
+ }
+ var desc string
+ if containing != "" {
+ desc = fmt.Sprintf("received no ShowMessage containing %q", containing)
+ } else {
+ desc = "received no ShowMessage requests"
}
return Expectation{
Check: check,
- Description: fmt.Sprintf("no ShowMessage received containing %q", subString),
+ Description: desc,
}
}
// ShownMessage asserts that the editor has received a ShowMessageRequest
// containing the given substring.
func ShownMessage(containing string) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
for _, m := range s.showMessage {
if strings.Contains(m.Message, containing) {
- return Met
+ return Met, ""
}
}
- return Unmet
+ return Unmet, fmt.Sprintf("no ShowMessage containing %q", containing)
}
return Expectation{
Check: check,
@@ -281,22 +284,22 @@ func ShownMessage(containing string) Expectation {
// ShownMessageRequest asserts that the editor has received a
// ShowMessageRequest with message matching the given regular expression.
-func ShownMessageRequest(messageRegexp string) Expectation {
- msgRE := regexp.MustCompile(messageRegexp)
- check := func(s State) Verdict {
+func ShownMessageRequest(matchingRegexp string) Expectation {
+ msgRE := regexp.MustCompile(matchingRegexp)
+ check := func(s State) (Verdict, string) {
if len(s.showMessageRequest) == 0 {
- return Unmet
+ return Unmet, "no ShowMessageRequest have been received"
}
for _, m := range s.showMessageRequest {
if msgRE.MatchString(m.Message) {
- return Met
+ return Met, ""
}
}
- return Unmet
+ return Unmet, fmt.Sprintf("no ShowMessageRequest (out of %d) match %q", len(s.showMessageRequest), matchingRegexp)
}
return Expectation{
Check: check,
- Description: fmt.Sprintf("ShowMessageRequest matching %q", messageRegexp),
+ Description: fmt.Sprintf("ShowMessageRequest matching %q", matchingRegexp),
}
}
@@ -328,9 +331,7 @@ func (e *Env) DoneDiagnosingChanges() Expectation {
}
// Sort for stability.
- sort.Slice(expected, func(i, j int) bool {
- return expected[i] < expected[j]
- })
+ slices.Sort(expected)
var all []Expectation
for _, source := range expected {
@@ -351,7 +352,7 @@ func (e *Env) DoneDiagnosingChanges() Expectation {
// - workspace/didChangeWatchedFiles
// - workspace/didChangeConfiguration
func (e *Env) AfterChange(expectations ...Expectation) {
- e.T.Helper()
+ e.TB.Helper()
e.OnceMet(
e.DoneDiagnosingChanges(),
expectations...,
@@ -411,15 +412,16 @@ func (e *Env) DoneWithClose() Expectation {
//
// See CompletedWork.
func StartedWork(title string, atLeast uint64) Expectation {
- check := func(s State) Verdict {
- if s.startedWork[title] >= atLeast {
- return Met
+ check := func(s State) (Verdict, string) {
+ started := s.startedWork[title]
+ if started >= atLeast {
+ return Met, ""
}
- return Unmet
+ return Unmet, fmt.Sprintf("started work %d %s", started, pluralize("time", started))
}
return Expectation{
Check: check,
- Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast),
+ Description: fmt.Sprintf("started work %q at least %d %s", title, atLeast, pluralize("time", atLeast)),
}
}
@@ -428,16 +430,16 @@ func StartedWork(title string, atLeast uint64) Expectation {
// Since the Progress API doesn't include any hidden metadata, we must use the
// progress notification title to identify the work we expect to be completed.
func CompletedWork(title string, count uint64, atLeast bool) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
completed := s.completedWork[title]
if completed == count || atLeast && completed > count {
- return Met
+ return Met, ""
}
- return Unmet
+ return Unmet, fmt.Sprintf("completed %d %s", completed, pluralize("time", completed))
}
- desc := fmt.Sprintf("completed work %q %v times", title, count)
+ desc := fmt.Sprintf("completed work %q %v %s", title, count, pluralize("time", count))
if atLeast {
- desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count)
+ desc = fmt.Sprintf("completed work %q at least %d %s", title, count, pluralize("time", count))
}
return Expectation{
Check: check,
@@ -445,6 +447,14 @@ func CompletedWork(title string, count uint64, atLeast bool) Expectation {
}
}
+// pluralize adds an 's' suffix to name if n > 1.
+func pluralize[T constraints.Integer](name string, n T) string {
+ if n > 1 {
+ return name + "s"
+ }
+ return name
+}
+
type WorkStatus struct {
// Last seen message from either `begin` or `report` progress.
Msg string
@@ -459,24 +469,23 @@ type WorkStatus struct {
// If the token is not a progress token that the client has seen, this
// expectation is Unmeetable.
func CompletedProgressToken(token protocol.ProgressToken, into *WorkStatus) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
work, ok := s.work[token]
if !ok {
- return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result
+ return Unmeetable, "no matching work items"
}
if work.complete {
if into != nil {
into.Msg = work.msg
into.EndMsg = work.endMsg
}
- return Met
+ return Met, ""
}
- return Unmet
+ return Unmet, fmt.Sprintf("work is not complete; last message: %q", work.msg)
}
- desc := fmt.Sprintf("completed work for token %v", token)
return Expectation{
Check: check,
- Description: desc,
+ Description: fmt.Sprintf("completed work for token %v", token),
}
}
@@ -488,28 +497,27 @@ func CompletedProgressToken(token protocol.ProgressToken, into *WorkStatus) Expe
// This expectation is a vestige of older workarounds for asynchronous command
// execution.
func CompletedProgress(title string, into *WorkStatus) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
var work *workProgress
for _, w := range s.work {
if w.title == title {
if work != nil {
- // TODO(rfindley): refactor to allow the verdict to explain this result
- return Unmeetable // multiple matches
+ return Unmeetable, "multiple matching work items"
}
work = w
}
}
if work == nil {
- return Unmeetable // zero matches
+ return Unmeetable, "no matching work items"
}
if work.complete {
if into != nil {
into.Msg = work.msg
into.EndMsg = work.endMsg
}
- return Met
+ return Met, ""
}
- return Unmet
+ return Unmet, fmt.Sprintf("work is not complete; last message: %q", work.msg)
}
desc := fmt.Sprintf("exactly 1 completed workDoneProgress with title %v", title)
return Expectation{
@@ -522,16 +530,16 @@ func CompletedProgress(title string, into *WorkStatus) Expectation {
// be an exact match, whereas the given msg must only be contained in the work
// item's message.
func OutstandingWork(title, msg string) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
for _, work := range s.work {
if work.complete {
continue
}
if work.title == title && strings.Contains(work.msg, msg) {
- return Met
+ return Met, ""
}
}
- return Unmet
+ return Unmet, "no matching work"
}
return Expectation{
Check: check,
@@ -548,7 +556,7 @@ func OutstandingWork(title, msg string) Expectation {
// TODO(rfindley): consider refactoring to treat outstanding work the same way
// we treat diagnostics: with an algebra of filters.
func NoOutstandingWork(ignore func(title, msg string) bool) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
for _, w := range s.work {
if w.complete {
continue
@@ -563,9 +571,9 @@ func NoOutstandingWork(ignore func(title, msg string) bool) Expectation {
if ignore != nil && ignore(w.title, w.msg) {
continue
}
- return Unmet
+ return Unmet, fmt.Sprintf("found outstanding work %q: %q", w.title, w.msg)
}
- return Met
+ return Met, ""
}
return Expectation{
Check: check,
@@ -600,7 +608,7 @@ func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) E
if err != nil {
panic(err)
}
- check := func(state State) Verdict {
+ check := func(state State) (Verdict, string) {
var found int
for _, msg := range state.logs {
if msg.Type == typ && rec.Match([]byte(msg.Message)) {
@@ -609,14 +617,15 @@ func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) E
}
// Check for an exact or "at least" match.
if found == count || (found >= count && atLeast) {
- return Met
+ return Met, ""
}
// If we require an exact count, and have received more than expected, the
// expectation can never be met.
+ verdict := Unmet
if found > count && !atLeast {
- return Unmeetable
+ verdict = Unmeetable
}
- return Unmet
+ return verdict, fmt.Sprintf("found %d matching logs", found)
}
desc := fmt.Sprintf("log message matching %q expected %v times", re, count)
if atLeast {
@@ -640,20 +649,24 @@ func NoLogMatching(typ protocol.MessageType, re string) Expectation {
panic(err)
}
}
- check := func(state State) Verdict {
+ check := func(state State) (Verdict, string) {
for _, msg := range state.logs {
if msg.Type != typ {
continue
}
if r == nil || r.Match([]byte(msg.Message)) {
- return Unmeetable
+ return Unmeetable, fmt.Sprintf("found matching log %q", msg.Message)
}
}
- return Met
+ return Met, ""
+ }
+ desc := fmt.Sprintf("no %s log messages", typ)
+ if re != "" {
+ desc += fmt.Sprintf(" matching %q", re)
}
return Expectation{
Check: check,
- Description: fmt.Sprintf("no log message matching %q", re),
+ Description: desc,
}
}
@@ -673,18 +686,18 @@ func NoFileWatchMatching(re string) Expectation {
}
}
-func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict {
+func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) (Verdict, string) {
rec := regexp.MustCompile(re)
- return func(s State) Verdict {
+ return func(s State) (Verdict, string) {
r := s.registeredCapabilities["workspace/didChangeWatchedFiles"]
- watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{})
+ watchers := jsonProperty(r.RegisterOptions, "watchers").([]any)
for _, watcher := range watchers {
pattern := jsonProperty(watcher, "globPattern").(string)
if rec.MatchString(pattern) {
- return onMatch
+ return onMatch, fmt.Sprintf("matches watcher pattern %q", pattern)
}
}
- return onNoMatch
+ return onNoMatch, "no matching watchers"
}
}
@@ -699,18 +712,22 @@ func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict {
// }
//
// Then jsonProperty(obj, "foo", "bar") will be 3.
-func jsonProperty(obj interface{}, path ...string) interface{} {
+func jsonProperty(obj any, path ...string) any {
if len(path) == 0 || obj == nil {
return obj
}
- m := obj.(map[string]interface{})
+ m := obj.(map[string]any)
return jsonProperty(m[path[0]], path[1:]...)
}
+func formatDiagnostic(d protocol.Diagnostic) string {
+ return fmt.Sprintf("%d:%d [%s]: %s\n", d.Range.Start.Line, d.Range.Start.Character, d.Source, d.Message)
+}
+
// Diagnostics asserts that there is at least one diagnostic matching the given
// filters.
func Diagnostics(filters ...DiagnosticFilter) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
diags := flattenDiagnostics(s)
for _, filter := range filters {
var filtered []flatDiagnostic
@@ -720,14 +737,22 @@ func Diagnostics(filters ...DiagnosticFilter) Expectation {
}
}
if len(filtered) == 0 {
- // TODO(rfindley): if/when expectations describe their own failure, we
- // can provide more useful information here as to which filter caused
- // the failure.
- return Unmet
+ // Reprinting the description of the filters is too verbose.
+ //
+ // We can probably do better here, but for now just format the
+ // diagnostics.
+ var b bytes.Buffer
+ for name, params := range s.diagnostics {
+ fmt.Fprintf(&b, "\t%s (version %d):\n", name, params.Version)
+ for _, d := range params.Diagnostics {
+ fmt.Fprintf(&b, "\t\t%s", formatDiagnostic(d))
+ }
+ }
+ return Unmet, fmt.Sprintf("diagnostics:\n%s", b.String())
}
diags = filtered
}
- return Met
+ return Met, ""
}
var descs []string
for _, filter := range filters {
@@ -743,7 +768,7 @@ func Diagnostics(filters ...DiagnosticFilter) Expectation {
// filters. Notably, if no filters are supplied this assertion checks that
// there are no diagnostics at all, for any file.
func NoDiagnostics(filters ...DiagnosticFilter) Expectation {
- check := func(s State) Verdict {
+ check := func(s State) (Verdict, string) {
diags := flattenDiagnostics(s)
for _, filter := range filters {
var filtered []flatDiagnostic
@@ -755,9 +780,11 @@ func NoDiagnostics(filters ...DiagnosticFilter) Expectation {
diags = filtered
}
if len(diags) > 0 {
- return Unmet
+ d := diags[0]
+ why := fmt.Sprintf("have diagnostic: %s: %v", d.name, formatDiagnostic(d.diag))
+ return Unmet, why
}
- return Met
+ return Met, ""
}
var descs []string
for _, filter := range filters {
diff --git a/gopls/internal/test/integration/fake/client.go b/gopls/internal/test/integration/fake/client.go
index 93eeab4a8af..aee6c1cfc3e 100644
--- a/gopls/internal/test/integration/fake/client.go
+++ b/gopls/internal/test/integration/fake/client.go
@@ -103,7 +103,7 @@ func (c *Client) LogMessage(ctx context.Context, params *protocol.LogMessagePara
return nil
}
-func (c *Client) Event(ctx context.Context, event *interface{}) error {
+func (c *Client) Event(ctx context.Context, event *any) error {
return nil
}
@@ -118,8 +118,8 @@ func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder,
return []protocol.WorkspaceFolder{}, nil
}
-func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
- results := make([]interface{}, len(p.Items))
+func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]any, error) {
+ results := make([]any, len(p.Items))
for i, item := range p.Items {
if item.ScopeURI != nil && *item.ScopeURI == "" {
return nil, fmt.Errorf(`malformed ScopeURI ""`)
diff --git a/gopls/internal/test/integration/fake/edit_test.go b/gopls/internal/test/integration/fake/edit_test.go
index 0d7ac18c414..f0a44846d31 100644
--- a/gopls/internal/test/integration/fake/edit_test.go
+++ b/gopls/internal/test/integration/fake/edit_test.go
@@ -79,7 +79,6 @@ func TestApplyEdits(t *testing.T) {
}
for _, test := range tests {
- test := test
t.Run(test.label, func(t *testing.T) {
got, err := applyEdits(protocol.NewMapper("", []byte(test.content)), test.edits, false)
if (err != nil) != test.wantErr {
diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go
index adc9df6c17d..a2dabf61c46 100644
--- a/gopls/internal/test/integration/fake/editor.go
+++ b/gopls/internal/test/integration/fake/editor.go
@@ -10,6 +10,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "maps"
"math/rand/v2"
"os"
"path"
@@ -108,16 +109,25 @@ type EditorConfig struct {
// To explicitly send no workspace folders, use an empty (non-nil) slice.
WorkspaceFolders []string
+ // NoDefaultWorkspaceFiles is used to specify whether the fake editor
+ // should give a default workspace folder when WorkspaceFolders is nil.
+ // When it's true, the editor will pass original WorkspaceFolders as is to the LSP server.
+ NoDefaultWorkspaceFiles bool
+
+ // RelRootPath is the root path which will be converted to rootUri to configure on the LSP server.
+ RelRootPath string
+
// Whether to edit files with windows line endings.
WindowsLineEndings bool
// Map of language ID -> regexp to match, used to set the file type of new
// buffers. Applied as an overlay on top of the following defaults:
- // "go" -> ".*\.go"
+ // "go" -> ".*\.go"
// "go.mod" -> "go\.mod"
// "go.sum" -> "go\.sum"
// "gotmpl" -> ".*tmpl"
- FileAssociations map[string]string
+ // "go.s" -> ".*\.s"
+ FileAssociations map[protocol.LanguageKind]string
// Settings holds user-provided configuration for the LSP server.
Settings map[string]any
@@ -252,12 +262,8 @@ func (e *Editor) Client() *Client {
// makeSettings builds the settings map for use in LSP settings RPCs.
func makeSettings(sandbox *Sandbox, config EditorConfig, scopeURI *protocol.URI) map[string]any {
env := make(map[string]string)
- for k, v := range sandbox.GoEnv() {
- env[k] = v
- }
- for k, v := range config.Env {
- env[k] = v
- }
+ maps.Copy(env, sandbox.GoEnv())
+ maps.Copy(env, config.Env)
for k, v := range env {
v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", sandbox.Workdir.RootURI().Path())
env[k] = v
@@ -298,9 +304,7 @@ func makeSettings(sandbox *Sandbox, config EditorConfig, scopeURI *protocol.URI)
}
}
if closestSettings != nil {
- for k, v := range closestSettings {
- settings[k] = v
- }
+ maps.Copy(settings, closestSettings)
}
}
@@ -321,8 +325,9 @@ func (e *Editor) initialize(ctx context.Context) error {
Version: "v1.0.0",
}
params.InitializationOptions = makeSettings(e.sandbox, config, nil)
- params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
+ params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders, config.NoDefaultWorkspaceFiles)
+ params.RootURI = protocol.DocumentURI(makeRootURI(e.sandbox, config.RelRootPath))
capabilities, err := clientCapabilities(config)
if err != nil {
return fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err)
@@ -433,12 +438,7 @@ func marshalUnmarshal[T any](v any) (T, error) {
// HasCommand reports whether the connected server supports the command with the given ID.
func (e *Editor) HasCommand(cmd command.Command) bool {
- for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands {
- if command == cmd.String() {
- return true
- }
- }
- return false
+ return slices.Contains(e.serverCapabilities.ExecuteCommandProvider.Commands, cmd.String())
}
// Examples: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
@@ -446,7 +446,10 @@ var uriRE = regexp.MustCompile(`^[a-z][a-z0-9+\-.]*://\S+`)
// makeWorkspaceFolders creates a slice of workspace folders to use for
// this editing session, based on the editor configuration.
-func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.WorkspaceFolder) {
+func makeWorkspaceFolders(sandbox *Sandbox, paths []string, useEmpty bool) (folders []protocol.WorkspaceFolder) {
+ if len(paths) == 0 && useEmpty {
+ return nil
+ }
if len(paths) == 0 {
paths = []string{string(sandbox.Workdir.RelativeTo)}
}
@@ -465,6 +468,14 @@ func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.
return folders
}
+func makeRootURI(sandbox *Sandbox, path string) string {
+ uri := path
+ if !uriRE.MatchString(path) { // relative file path
+ uri = string(sandbox.Workdir.URI(path))
+ }
+ return uri
+}
+
// onFileChanges is registered to be called by the Workdir on any writes that
// go through the Workdir API. It is called synchronously by the Workdir.
func (e *Editor) onFileChanges(ctx context.Context, evts []protocol.FileEvent) {
@@ -619,27 +630,28 @@ func (e *Editor) sendDidOpen(ctx context.Context, item protocol.TextDocumentItem
return nil
}
-var defaultFileAssociations = map[string]*regexp.Regexp{
+var defaultFileAssociations = map[protocol.LanguageKind]*regexp.Regexp{
"go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl!
"go.mod": regexp.MustCompile(`^go\.mod$`),
"go.sum": regexp.MustCompile(`^go(\.work)?\.sum$`),
"go.work": regexp.MustCompile(`^go\.work$`),
"gotmpl": regexp.MustCompile(`^.*tmpl$`),
+ "go.s": regexp.MustCompile(`\.s$`),
}
// languageID returns the language identifier for the path p given the user
// configured fileAssociations.
-func languageID(p string, fileAssociations map[string]string) protocol.LanguageKind {
+func languageID(p string, fileAssociations map[protocol.LanguageKind]string) protocol.LanguageKind {
base := path.Base(p)
for lang, re := range fileAssociations {
re := regexp.MustCompile(re)
if re.MatchString(base) {
- return protocol.LanguageKind(lang)
+ return lang
}
}
for lang, re := range defaultFileAssociations {
if re.MatchString(base) {
- return protocol.LanguageKind(lang)
+ return lang
}
}
return ""
@@ -1157,11 +1169,8 @@ func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCom
var match bool
if e.serverCapabilities.ExecuteCommandProvider != nil {
// Ensure that this command was actually listed as a supported command.
- for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands {
- if command == params.Command {
- match = true
- break
- }
+ if slices.Contains(e.serverCapabilities.ExecuteCommandProvider.Commands, params.Command) {
+ match = true
}
}
if !match {
@@ -1307,6 +1316,19 @@ func (e *Editor) Completion(ctx context.Context, loc protocol.Location) (*protoc
return completions, nil
}
+func (e *Editor) DidCreateFiles(ctx context.Context, files ...protocol.DocumentURI) error {
+ if e.Server == nil {
+ return nil
+ }
+ params := &protocol.CreateFilesParams{}
+ for _, file := range files {
+ params.Files = append(params.Files, protocol.FileCreate{
+ URI: string(file),
+ })
+ }
+ return e.Server.DidCreateFiles(ctx, params)
+}
+
func (e *Editor) SetSuggestionInsertReplaceMode(_ context.Context, useReplaceMode bool) {
e.mu.Lock()
defer e.mu.Unlock()
@@ -1630,8 +1652,8 @@ func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) e
config := e.Config()
// capture existing folders so that we can compute the change.
- oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
- newFolders := makeWorkspaceFolders(e.sandbox, folders)
+ oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders, config.NoDefaultWorkspaceFiles)
+ newFolders := makeWorkspaceFolders(e.sandbox, folders, config.NoDefaultWorkspaceFiles)
config.WorkspaceFolders = folders
e.SetConfig(config)
diff --git a/gopls/internal/test/integration/fake/glob/glob.go b/gopls/internal/test/integration/fake/glob/glob.go
index a540ebefac5..3bda93bee6d 100644
--- a/gopls/internal/test/integration/fake/glob/glob.go
+++ b/gopls/internal/test/integration/fake/glob/glob.go
@@ -217,7 +217,7 @@ func (g *Glob) Match(input string) bool {
}
func match(elems []element, input string) (ok bool) {
- var elem interface{}
+ var elem any
for len(elems) > 0 {
elem, elems = elems[0], elems[1:]
switch elem := elem.(type) {
diff --git a/gopls/internal/test/integration/fake/sandbox.go b/gopls/internal/test/integration/fake/sandbox.go
index 7adf3e3e4a9..1d8918babd4 100644
--- a/gopls/internal/test/integration/fake/sandbox.go
+++ b/gopls/internal/test/integration/fake/sandbox.go
@@ -208,6 +208,7 @@ func (sb *Sandbox) GoEnv() map[string]string {
"GO111MODULE": "",
"GOSUMDB": "off",
"GOPACKAGESDRIVER": "off",
+ "GOTOOLCHAIN": "local", // tests should not download toolchains
}
if testenv.Go1Point() >= 5 {
vars["GOMODCACHE"] = ""
diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go
index c62a3898e9b..d9c83186d69 100644
--- a/gopls/internal/test/integration/misc/codeactions_test.go
+++ b/gopls/internal/test/integration/misc/codeactions_test.go
@@ -35,25 +35,28 @@ package a
func f() { g() }
func g() {}
+
+-- issue72742/a.go --
+package main
+
+func main(){
+ fmt.Println("helloworld")
+}
`
Run(t, src, func(t *testing.T, env *Env) {
- check := func(filename string, wantKind ...protocol.CodeActionKind) {
+ check := func(filename string, re string, want []protocol.CodeActionKind) {
env.OpenFile(filename)
- loc := env.RegexpSearch(filename, `g\(\)`)
+ loc := env.RegexpSearch(filename, re)
actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger)
if err != nil {
t.Fatal(err)
}
- type kinds = map[protocol.CodeActionKind]bool
- got := make(kinds)
+ type kinds = []protocol.CodeActionKind
+ got := make(kinds, 0)
for _, act := range actions {
- got[act.Kind] = true
- }
- want := make(kinds)
- for _, kind := range wantKind {
- want[kind] = true
+ got = append(got, act.Kind)
}
if diff := cmp.Diff(want, got); diff != "" {
@@ -63,20 +66,33 @@ func g() {}
}
}
- check("src/a.go",
+ check("src/a.go", `g\(\)`, []protocol.CodeActionKind{
settings.AddTest,
settings.GoAssembly,
settings.GoDoc,
settings.GoFreeSymbols,
settings.GoToggleCompilerOptDetails,
+ settings.RefactorInlineCall,
settings.GoplsDocFeatures,
- settings.RefactorInlineCall)
- check("gen/a.go",
+ })
+
+ check("gen/a.go", `g\(\)`, []protocol.CodeActionKind{
settings.GoAssembly,
settings.GoDoc,
settings.GoFreeSymbols,
settings.GoToggleCompilerOptDetails,
- settings.GoplsDocFeatures)
+ settings.GoplsDocFeatures,
+ })
+
+ check("issue72742/a.go", `fmt`, []protocol.CodeActionKind{
+ settings.OrganizeImports,
+ settings.AddTest,
+ settings.GoAssembly,
+ settings.GoDoc,
+ settings.GoFreeSymbols,
+ settings.GoToggleCompilerOptDetails,
+ settings.GoplsDocFeatures,
+ })
})
}
diff --git a/gopls/internal/test/integration/misc/compileropt_test.go b/gopls/internal/test/integration/misc/compileropt_test.go
index 175ec640042..a02a5dddebd 100644
--- a/gopls/internal/test/integration/misc/compileropt_test.go
+++ b/gopls/internal/test/integration/misc/compileropt_test.go
@@ -5,6 +5,7 @@
package misc
import (
+ "fmt"
"runtime"
"testing"
@@ -44,6 +45,7 @@ func main() {
if err != nil {
t.Fatal(err)
}
+
params := &protocol.ExecuteCommandParams{
Command: docAction.Command.Command,
Arguments: docAction.Command.Arguments,
@@ -166,3 +168,76 @@ func H(x int) any { return &x }
)
})
}
+
+// TestCompilerOptDetails_config exercises that the "want optimization
+// details" flag honors the "annotation" configuration setting.
+func TestCompilerOptDetails_config(t *testing.T) {
+ if runtime.GOOS == "android" {
+ t.Skipf("the compiler optimization details code action doesn't work on Android")
+ }
+
+ const mod = `
+-- go.mod --
+module mod.com
+go 1.18
+
+-- a/a.go --
+package a
+
+func F(x int) any { return &x } // escape(x escapes to heap)
+func G() { defer func(){} () } // cannotInlineFunction(unhandled op DEFER)
+`
+
+ for _, escape := range []bool{true, false} {
+ WithOptions(
+ Settings{"annotations": map[string]any{"inline": true, "escape": escape}},
+ ).Run(t, mod, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+ actions := env.CodeActionForFile("a/a.go", nil)
+
+ docAction, err := codeActionByKind(actions, settings.GoToggleCompilerOptDetails)
+ if err != nil {
+ t.Fatal(err)
+ }
+ params := &protocol.ExecuteCommandParams{
+ Command: docAction.Command.Command,
+ Arguments: docAction.Command.Arguments,
+ }
+ env.ExecuteCommand(params, nil)
+
+ env.OnceMet(
+ CompletedWork(server.DiagnosticWorkTitle(server.FromToggleCompilerOptDetails), 1, true),
+ cond(escape, Diagnostics, NoDiagnostics)(
+ ForFile("a/a.go"),
+ AtPosition("a/a.go", 2, 7),
+ WithMessage("x escapes to heap"),
+ WithSeverityTags("optimizer details", protocol.SeverityInformation, nil),
+ ),
+ Diagnostics(
+ ForFile("a/a.go"),
+ AtPosition("a/a.go", 3, 5),
+ WithMessage("cannotInlineFunction(unhandled op DEFER)"),
+ WithSeverityTags("optimizer details", protocol.SeverityInformation, nil),
+ ),
+ )
+ })
+ }
+}
+
+func cond[T any](cond bool, x, y T) T {
+ if cond {
+ return x
+ } else {
+ return y
+ }
+}
+
+// codeActionByKind returns the first action of (exactly) the specified kind, or an error.
+func codeActionByKind(actions []protocol.CodeAction, kind protocol.CodeActionKind) (*protocol.CodeAction, error) {
+ for _, act := range actions {
+ if act.Kind == kind {
+ return &act, nil
+ }
+ }
+ return nil, fmt.Errorf("can't find action with kind %s, only %#v", kind, actions)
+}
diff --git a/gopls/internal/test/integration/misc/failures_test.go b/gopls/internal/test/integration/misc/failures_test.go
index 81fa17deb9b..543e36a9e44 100644
--- a/gopls/internal/test/integration/misc/failures_test.go
+++ b/gopls/internal/test/integration/misc/failures_test.go
@@ -7,8 +7,8 @@ package misc
import (
"testing"
- . "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/compare"
+ . "golang.org/x/tools/gopls/internal/test/integration"
)
// This is a slight variant of TestHoverOnError in definition_test.go
diff --git a/gopls/internal/test/integration/misc/highlight_test.go b/gopls/internal/test/integration/misc/highlight_test.go
index e4da558e5d0..36bddf25057 100644
--- a/gopls/internal/test/integration/misc/highlight_test.go
+++ b/gopls/internal/test/integration/misc/highlight_test.go
@@ -124,7 +124,7 @@ func main() {}`
}
func checkHighlights(env *Env, loc protocol.Location, highlightCount int) {
- t := env.T
+ t := env.TB
t.Helper()
highlights := env.DocumentHighlight(loc)
diff --git a/gopls/internal/test/integration/misc/imports_test.go b/gopls/internal/test/integration/misc/imports_test.go
index 98a70478ecf..bcbfacc967a 100644
--- a/gopls/internal/test/integration/misc/imports_test.go
+++ b/gopls/internal/test/integration/misc/imports_test.go
@@ -401,6 +401,31 @@ return nil
}
})
}
+
+// use the import from a different package in the same module
+func Test44510(t *testing.T) {
+ const files = `-- go.mod --
+module test
+go 1.19
+-- foo/foo.go --
+package main
+import strs "strings"
+var _ = strs.Count
+-- bar/bar.go --
+package main
+var _ = strs.Builder
+`
+ WithOptions(
+ WriteGoSum("."),
+ ).Run(t, files, func(T *testing.T, env *Env) {
+ env.OpenFile("bar/bar.go")
+ env.SaveBuffer("bar/bar.go")
+ buf := env.BufferText("bar/bar.go")
+ if !strings.Contains(buf, "strs") {
+ t.Error(buf)
+ }
+ })
+}
func TestRelativeReplace(t *testing.T) {
const files = `
-- go.mod --
@@ -688,3 +713,33 @@ func Test() {
}
})
}
+
+// this test replaces 'package bar' with 'package foo'
+// saves the file, and then looks for the import in the main package.s
+func Test67973(t *testing.T) {
+ const files = `-- go.mod --
+module hello
+go 1.19
+-- hello.go --
+package main
+var _ = foo.Bar
+-- internal/foo/foo.go --
+package bar
+func Bar() {}
+`
+ WithOptions(
+ Settings{"importsSource": settings.ImportsSourceGopls},
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("hello.go")
+ env.AfterChange(env.DoneWithOpen())
+ env.SaveBuffer("hello.go")
+ env.OpenFile("internal/foo/foo.go")
+ env.RegexpReplace("internal/foo/foo.go", "bar", "foo")
+ env.SaveBuffer("internal/foo/foo.go")
+ env.SaveBuffer("hello.go")
+ buf := env.BufferText("hello.go")
+ if !strings.Contains(buf, "internal/foo") {
+ t.Errorf(`expected import "hello/internal/foo" but got %q`, buf)
+ }
+ })
+}
diff --git a/gopls/internal/test/integration/misc/link_test.go b/gopls/internal/test/integration/misc/link_test.go
index 53b0f0818f3..079d84cb6ee 100644
--- a/gopls/internal/test/integration/misc/link_test.go
+++ b/gopls/internal/test/integration/misc/link_test.go
@@ -5,9 +5,12 @@
package misc
import (
+ "path/filepath"
+ "slices"
"strings"
"testing"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
@@ -19,15 +22,35 @@ module mod.test
go 1.12
require import.test v1.2.3
+
+require replace.test v1.2.3
+replace replace.test => replace.test v1.2.4
+
+require replace.fixed.test v1.2.3
+replace replace.fixed.test v1.2.3 => replace.fixed.test v1.2.4
+
+require replace.another.test v1.2.3
+replace replace.another.test => another.test v1.2.3
+
+
+replace example.com/non-exist => ./
+replace example.com/non-exist1 => ../work/
+
-- main.go --
package main
import "import.test/pkg"
+import "replace.test/replace"
+import "replace.fixed.test/fixed"
+import "replace.another.test/another"
func main() {
// Issue 43990: this is not a link that most users can open from an LSP
// client: mongodb://not.a.link.com
println(pkg.Hello)
+ println(replace.Hello)
+ println(fixed.Hello)
+ println(another.Hello)
}`
const proxy = `
@@ -38,6 +61,32 @@ go 1.12
-- import.test@v1.2.3/pkg/const.go --
package pkg
+
+-- replace.test@v1.2.4/go.mod --
+module replace.test
+
+go 1.12
+-- replace.test@v1.2.4/replace/const.go --
+package replace
+
+const Hello = "Hello"
+
+-- replace.fixed.test@v1.2.4/go.mod --
+module replace.fixed.test
+
+go 1.12
+-- replace.fixed.test@v1.2.4/fixed/const.go --
+package fixed
+
+const Hello = "Hello"
+
+-- another.test@v1.2.3/go.mod --
+module another.test
+
+go 1.12
+-- another.test@v1.2.3/another/const.go --
+package another
+
const Hello = "Hello"
`
WithOptions(
@@ -47,25 +96,82 @@ const Hello = "Hello"
env.OpenFile("main.go")
env.OpenFile("go.mod")
- modLink := "https://pkg.go.dev/mod/import.test@v1.2.3"
- pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg"
+ const (
+ modImportLink = "https://pkg.go.dev/mod/import.test@v1.2.3"
+ modReplaceLink = "https://pkg.go.dev/mod/replace.test@v1.2.4"
+ modReplaceFixedeLink = "https://pkg.go.dev/mod/replace.fixed.test@v1.2.4"
+ modAnotherLink = "https://pkg.go.dev/mod/another.test@v1.2.3"
+
+ pkgImportLink = "https://pkg.go.dev/import.test@v1.2.3/pkg"
+ pkgReplaceLink = "https://pkg.go.dev/replace.test@v1.2.4/replace"
+ pkgReplaceFixedLink = "https://pkg.go.dev/replace.fixed.test@v1.2.4/fixed"
+ pkgAnotherLink = "https://pkg.go.dev/another.test@v1.2.3/another"
+ )
// First, check that we get the expected links via hover and documentLink.
content, _ := env.Hover(env.RegexpSearch("main.go", "pkg.Hello"))
- if content == nil || !strings.Contains(content.Value, pkgLink) {
- t.Errorf("hover: got %v in main.go, want contains %q", content, pkgLink)
+ if content == nil || !strings.Contains(content.Value, pkgImportLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgImportLink)
+ }
+ content, _ = env.Hover(env.RegexpSearch("main.go", "replace.Hello"))
+ if content == nil || !strings.Contains(content.Value, pkgReplaceLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgReplaceLink)
+ }
+ content, _ = env.Hover(env.RegexpSearch("main.go", "fixed.Hello"))
+ if content == nil || !strings.Contains(content.Value, pkgReplaceFixedLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgReplaceFixedLink)
+ }
+ content, _ = env.Hover(env.RegexpSearch("main.go", "another.Hello"))
+ if content == nil || !strings.Contains(content.Value, pkgAnotherLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgAnotherLink)
}
+
content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test"))
- if content == nil || !strings.Contains(content.Value, pkgLink) {
- t.Errorf("hover: got %v in go.mod, want contains %q", content, pkgLink)
+ if content == nil || !strings.Contains(content.Value, pkgImportLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgImportLink)
+ }
+ content, _ = env.Hover(env.RegexpSearch("go.mod", "replace.test"))
+ if content == nil || !strings.Contains(content.Value, pkgReplaceLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgReplaceLink)
+ }
+ content, _ = env.Hover(env.RegexpSearch("go.mod", "replace.fixed.test"))
+ if content == nil || !strings.Contains(content.Value, pkgReplaceFixedLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgReplaceFixedLink)
+ }
+ content, _ = env.Hover(env.RegexpSearch("go.mod", "replace.another.test"))
+ if content == nil || !strings.Contains(content.Value, pkgAnotherLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgAnotherLink)
+ }
+
+ getLinks := func(links []protocol.DocumentLink) []string {
+ var got []string
+ for i := range links {
+ got = append(got, *links[i].Target)
+ }
+ return got
}
links := env.DocumentLink("main.go")
- if len(links) != 1 || *links[0].Target != pkgLink {
- t.Errorf("documentLink: got links %+v for main.go, want one link with target %q", links, pkgLink)
+ got, want := getLinks(links), []string{
+ pkgImportLink,
+ pkgReplaceLink,
+ pkgReplaceFixedLink,
+ pkgAnotherLink,
+ }
+ if !slices.Equal(got, want) {
+ t.Errorf("documentLink: got links %v for main.go, want links %v", got, want)
}
+
links = env.DocumentLink("go.mod")
- if len(links) != 1 || *links[0].Target != modLink {
- t.Errorf("documentLink: got links %+v for go.mod, want one link with target %q", links, modLink)
+ localReplacePath := filepath.Join(env.Sandbox.Workdir.RootURI().Path(), "go.mod")
+ got, want = getLinks(links), []string{
+ localReplacePath, localReplacePath,
+ modImportLink,
+ modReplaceLink,
+ modReplaceFixedeLink,
+ modAnotherLink,
+ }
+ if !slices.Equal(got, want) {
+ t.Errorf("documentLink: got links %v for go.mod, want links %v", got, want)
}
// Then change the environment to make these links private.
@@ -75,20 +181,33 @@ const Hello = "Hello"
// Finally, verify that the links are gone.
content, _ = env.Hover(env.RegexpSearch("main.go", "pkg.Hello"))
- if content == nil || strings.Contains(content.Value, pkgLink) {
- t.Errorf("hover: got %v in main.go, want non-empty hover without %q", content, pkgLink)
+ if content == nil || strings.Contains(content.Value, pkgImportLink) {
+ t.Errorf("hover: got %v in main.go, want non-empty hover without %q", content, pkgImportLink)
}
content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test"))
- if content == nil || strings.Contains(content.Value, modLink) {
- t.Errorf("hover: got %v in go.mod, want contains %q", content, modLink)
+ if content == nil || strings.Contains(content.Value, modImportLink) {
+ t.Errorf("hover: got %v in go.mod, want contains %q", content, modImportLink)
}
+
links = env.DocumentLink("main.go")
- if len(links) != 0 {
- t.Errorf("documentLink: got %d document links for main.go, want 0\nlinks: %v", len(links), links)
+ got, want = getLinks(links), []string{
+ pkgReplaceLink,
+ pkgReplaceFixedLink,
+ pkgAnotherLink,
+ }
+ if !slices.Equal(got, want) {
+ t.Errorf("documentLink: got links %v for main.go, want links %v", got, want)
}
+
links = env.DocumentLink("go.mod")
- if len(links) != 0 {
- t.Errorf("documentLink: got %d document links for go.mod, want 0\nlinks: %v", len(links), links)
+ got, want = getLinks(links), []string{
+ localReplacePath, localReplacePath,
+ modReplaceLink,
+ modReplaceFixedeLink,
+ modAnotherLink,
+ }
+ if !slices.Equal(got, want) {
+ t.Errorf("documentLink: got links %v for go.mod, want links %v", got, want)
}
})
}
diff --git a/gopls/internal/test/integration/misc/modify_tags_test.go b/gopls/internal/test/integration/misc/modify_tags_test.go
new file mode 100644
index 00000000000..48b5f772ffb
--- /dev/null
+++ b/gopls/internal/test/integration/misc/modify_tags_test.go
@@ -0,0 +1,159 @@
+// Copyright 2025 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 misc
+
+import (
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
+ "golang.org/x/tools/gopls/internal/test/compare"
+ "golang.org/x/tools/gopls/internal/test/integration"
+)
+
+func TestModifyTags(t *testing.T) {
+ const files = `
+-- go.mod --
+module example.com
+
+go 1.20
+
+-- a.go --
+package a
+
+type A struct {
+ B string
+ C int
+ D bool
+ E string
+}
+
+-- b.go --
+package b
+
+type B struct {
+ B string ` + "`json:\"b,omitempty\"`" + `
+ C int ` + "`json:\"c,omitempty\"`" + `
+ D bool ` + "`json:\"d,omitempty\"`" + `
+ E string ` + "`json:\"e,omitempty\"`" + `
+}
+
+-- c.go --
+package c
+
+type C struct {
+ B string
+ C int
+ D bool ` + "`json:\"d,omitempty\"`" + `
+ E string
+}
+`
+
+ const wantAddTagsEntireStruct = `package a
+
+type A struct {
+ B string ` + "`json:\"b,omitempty\"`" + `
+ C int ` + "`json:\"c,omitempty\"`" + `
+ D bool ` + "`json:\"d,omitempty\"`" + `
+ E string ` + "`json:\"e,omitempty\"`" + `
+}
+`
+
+ const wantRemoveTags = `package b
+
+type B struct {
+ B string
+ C int
+ D bool ` + "`json:\"d,omitempty\"`" + `
+ E string ` + "`json:\"e,omitempty\"`" + `
+}
+`
+
+ const wantAddTagsSingleLine = `package a
+
+type A struct {
+ B string
+ C int
+ D bool ` + "`json:\"d,omitempty\"`" + `
+ E string
+}
+`
+
+ const wantRemoveOptions = `package c
+
+type C struct {
+ B string
+ C int
+ D bool ` + "`json:\"d\"`" + `
+ E string
+}
+`
+
+ tests := []struct {
+ file string
+ args command.ModifyTagsArgs
+ want string
+ }{
+ {file: "a.go", args: command.ModifyTagsArgs{
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 2, Character: 0},
+ End: protocol.Position{Line: 8, Character: 0},
+ },
+ Add: "json",
+ AddOptions: "json=omitempty",
+ }, want: wantAddTagsEntireStruct},
+ {file: "b.go", args: command.ModifyTagsArgs{
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 3, Character: 2},
+ End: protocol.Position{Line: 4, Character: 6},
+ },
+ Remove: "json",
+ }, want: wantRemoveTags},
+ {file: "a.go", args: command.ModifyTagsArgs{
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 5, Character: 0},
+ End: protocol.Position{Line: 5, Character: 7},
+ },
+ Add: "json",
+ AddOptions: "json=omitempty",
+ }, want: wantAddTagsSingleLine},
+ {file: "c.go", args: command.ModifyTagsArgs{
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 3, Character: 0},
+ End: protocol.Position{Line: 7, Character: 0},
+ },
+ RemoveOptions: "json=omitempty",
+ }, want: wantRemoveOptions},
+ }
+
+ for _, test := range tests {
+ integration.Run(t, files, func(t *testing.T, env *integration.Env) {
+ uri := env.Sandbox.Workdir.URI(test.file)
+ args, err := command.MarshalArgs(
+ command.ModifyTagsArgs{
+ URI: uri,
+ Range: test.args.Range,
+ Add: test.args.Add,
+ AddOptions: test.args.AddOptions,
+ Remove: test.args.Remove,
+ RemoveOptions: test.args.RemoveOptions,
+ },
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ var res any
+ env.ExecuteCommand(&protocol.ExecuteCommandParams{
+ Command: command.ModifyTags.String(),
+ Arguments: args,
+ }, &res)
+ // Wait until we finish writing to the file.
+ env.AfterChange()
+ if got := env.BufferText(test.file); got != test.want {
+ t.Errorf("modify_tags returned unexpected diff (-want +got):\n%s", compare.Text(test.want, got))
+ }
+ })
+ }
+}
diff --git a/gopls/internal/test/integration/misc/package_symbols_test.go b/gopls/internal/test/integration/misc/package_symbols_test.go
index 860264f2bb0..1e06a655935 100644
--- a/gopls/internal/test/integration/misc/package_symbols_test.go
+++ b/gopls/internal/test/integration/misc/package_symbols_test.go
@@ -16,6 +16,11 @@ import (
func TestPackageSymbols(t *testing.T) {
const files = `
+-- go.mod --
+module example.com
+
+go 1.20
+
-- a.go --
package a
@@ -33,68 +38,74 @@ func (s *S) M2() {}
func (s *S) M3() {}
func F() {}
+-- unloaded.go --
+//go:build unloaded
+
+package a
+
+var Unloaded int
`
integration.Run(t, files, func(t *testing.T, env *integration.Env) {
- a_uri := env.Sandbox.Workdir.URI("a.go")
- b_uri := env.Sandbox.Workdir.URI("b.go")
+ aURI := env.Sandbox.Workdir.URI("a.go")
+ bURI := env.Sandbox.Workdir.URI("b.go")
args, err := command.MarshalArgs(command.PackageSymbolsArgs{
- URI: a_uri,
+ URI: aURI,
})
if err != nil {
- t.Fatalf("failed to MarshalArgs: %v", err)
+ t.Fatal(err)
}
var res command.PackageSymbolsResult
env.ExecuteCommand(&protocol.ExecuteCommandParams{
- Command: "gopls.package_symbols",
+ Command: command.PackageSymbols.String(),
Arguments: args,
}, &res)
want := command.PackageSymbolsResult{
PackageName: "a",
- Files: []protocol.DocumentURI{a_uri, b_uri},
+ Files: []protocol.DocumentURI{aURI, bURI},
Symbols: []command.PackageSymbol{
- {
- Name: "A",
- Kind: protocol.Variable,
- File: 0,
- },
- {
- Name: "F",
- Kind: protocol.Function,
- File: 1,
- },
- {
- Name: "S",
- Kind: protocol.Struct,
- File: 0,
- Children: []command.PackageSymbol{
- {
- Name: "M1",
- Kind: protocol.Method,
- File: 0,
- },
- {
- Name: "M2",
- Kind: protocol.Method,
- File: 1,
- },
- {
- Name: "M3",
- Kind: protocol.Method,
- File: 1,
- },
- },
- },
- {
- Name: "b",
- Kind: protocol.Variable,
- File: 1,
- },
+ {Name: "A", Kind: protocol.Variable, File: 0},
+ {Name: "F", Kind: protocol.Function, File: 1},
+ {Name: "S", Kind: protocol.Struct, File: 0, Children: []command.PackageSymbol{
+ {Name: "M1", Kind: protocol.Method, File: 0},
+ {Name: "M2", Kind: protocol.Method, File: 1},
+ {Name: "M3", Kind: protocol.Method, File: 1},
+ }},
+ {Name: "b", Kind: protocol.Variable, File: 1},
},
}
- if diff := cmp.Diff(want, res, cmpopts.IgnoreFields(command.PackageSymbol{}, "Range", "SelectionRange", "Detail")); diff != "" {
- t.Errorf("gopls.package_symbols returned unexpected diff (-want +got):\n%s", diff)
+ ignore := cmpopts.IgnoreFields(command.PackageSymbol{}, "Range", "SelectionRange", "Detail")
+ if diff := cmp.Diff(want, res, ignore); diff != "" {
+ t.Errorf("package_symbols returned unexpected diff (-want +got):\n%s", diff)
+ }
+
+ for file, want := range map[string]command.PackageSymbolsResult{
+ "go.mod": {},
+ "unloaded.go": {
+ PackageName: "a",
+ Files: []protocol.DocumentURI{env.Sandbox.Workdir.URI("unloaded.go")},
+ Symbols: []command.PackageSymbol{
+ {Name: "Unloaded", Kind: protocol.Variable, File: 0},
+ },
+ },
+ } {
+ uri := env.Sandbox.Workdir.URI(file)
+ args, err := command.MarshalArgs(command.PackageSymbolsArgs{
+ URI: uri,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ var res command.PackageSymbolsResult
+ env.ExecuteCommand(&protocol.ExecuteCommandParams{
+ Command: command.PackageSymbols.String(),
+ Arguments: args,
+ }, &res)
+
+ if diff := cmp.Diff(want, res, ignore); diff != "" {
+ t.Errorf("package_symbols returned unexpected diff (-want +got):\n%s", diff)
+ }
}
})
}
diff --git a/gopls/internal/test/integration/misc/prompt_test.go b/gopls/internal/test/integration/misc/prompt_test.go
index 37cd654b08d..21da1b5853f 100644
--- a/gopls/internal/test/integration/misc/prompt_test.go
+++ b/gopls/internal/test/integration/misc/prompt_test.go
@@ -429,7 +429,7 @@ func main() {
const maxPrompts = 5 // internal prompt limit defined by gopls
- for i := 0; i < maxPrompts+1; i++ {
+ for i := range maxPrompts + 1 {
WithOptions(
Modes(Default), // no need to run this in all modes
EnvVars{
diff --git a/gopls/internal/test/integration/misc/references_test.go b/gopls/internal/test/integration/misc/references_test.go
index e84dcd71dc3..58fdb3c5cd8 100644
--- a/gopls/internal/test/integration/misc/references_test.go
+++ b/gopls/internal/test/integration/misc/references_test.go
@@ -126,7 +126,7 @@ var _ = unsafe.Slice(nil, 0)
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
- for _, name := range strings.Fields(
+ for name := range strings.FieldsSeq(
"iota error int nil append iota Pointer Sizeof Alignof Add Slice") {
loc := env.RegexpSearch("a.go", `\b`+name+`\b`)
diff --git a/gopls/internal/test/integration/misc/test_test.go b/gopls/internal/test/integration/misc/test_test.go
new file mode 100644
index 00000000000..b282bf57a95
--- /dev/null
+++ b/gopls/internal/test/integration/misc/test_test.go
@@ -0,0 +1,82 @@
+// Copyright 2025 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 misc
+
+// This file defines tests of the source.test ("Run tests and
+// benchmarks") code action.
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/settings"
+ . "golang.org/x/tools/gopls/internal/test/integration"
+)
+
+func TestRunTestsAndBenchmarks(t *testing.T) {
+ file := filepath.Join(t.TempDir(), "out")
+ os.Setenv("TESTFILE", file)
+
+ const src = `
+-- go.mod --
+module example.com
+go 1.19
+
+-- a/a.go --
+package a
+
+-- a/a_test.go --
+package a
+
+import (
+ "os"
+ "testing"
+)
+
+func Test(t *testing.T) {
+ os.WriteFile(os.Getenv("TESTFILE"), []byte("ok"), 0644)
+}
+
+`
+ Run(t, src, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a_test.go")
+ loc := env.RegexpSearch("a/a_test.go", "WriteFile")
+
+ // Request code actions. (settings.GoTest is special:
+ // it is returned only when explicitly requested.)
+ actions, err := env.Editor.Server.CodeAction(env.Ctx, &protocol.CodeActionParams{
+ TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
+ Range: loc.Range,
+ Context: protocol.CodeActionContext{
+ Only: []protocol.CodeActionKind{settings.GoTest},
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(actions) != 1 {
+ t.Fatalf("CodeAction returned %#v, want one source.test action", actions)
+ }
+ if actions[0].Command == nil {
+ t.Fatalf("CodeActions()[0] has no Command")
+ }
+
+ // Execute test.
+ // (ExecuteCommand fails if the test fails.)
+ t.Logf("Running %s...", actions[0].Title)
+ env.ExecuteCommand(&protocol.ExecuteCommandParams{
+ Command: actions[0].Command.Command,
+ Arguments: actions[0].Command.Arguments,
+ }, nil)
+
+ // Check test had expected side effect.
+ data, err := os.ReadFile(file)
+ if string(data) != "ok" {
+ t.Fatalf("Test did not write expected content of %s; ReadFile returned (%q, %v)", file, data, err)
+ }
+ })
+}
diff --git a/gopls/internal/test/integration/misc/vuln_test.go b/gopls/internal/test/integration/misc/vuln_test.go
index 9dad13179af..47f4c6a77b7 100644
--- a/gopls/internal/test/integration/misc/vuln_test.go
+++ b/gopls/internal/test/integration/misc/vuln_test.go
@@ -912,7 +912,6 @@ func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagEx
// Find the diagnostics at loc.start.
var diag *protocol.Diagnostic
for _, g := range got.Diagnostics {
- g := g
if g.Range.Start == loc.Range.Start && w.msg == g.Message {
modPathDiagnostics = append(modPathDiagnostics, g)
diag = &g
diff --git a/gopls/internal/test/integration/misc/workspace_symbol_test.go b/gopls/internal/test/integration/misc/workspace_symbol_test.go
index 9420b146d85..f1148539447 100644
--- a/gopls/internal/test/integration/misc/workspace_symbol_test.go
+++ b/gopls/internal/test/integration/misc/workspace_symbol_test.go
@@ -8,8 +8,8 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- . "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/settings"
+ . "golang.org/x/tools/gopls/internal/test/integration"
)
func TestWorkspaceSymbolMissingMetadata(t *testing.T) {
@@ -103,12 +103,12 @@ const (
}
func checkSymbols(env *Env, query string, want ...string) {
- env.T.Helper()
+ env.TB.Helper()
var got []string
for _, info := range env.Symbol(query) {
got = append(got, info.Name)
}
if diff := cmp.Diff(got, want); diff != "" {
- env.T.Errorf("unexpected Symbol(%q) result (+want -got):\n%s", query, diff)
+ env.TB.Errorf("unexpected Symbol(%q) result (+want -got):\n%s", query, diff)
}
}
diff --git a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go
index 8090388e17d..176a8a64f24 100644
--- a/gopls/internal/test/integration/options.go
+++ b/gopls/internal/test/integration/options.go
@@ -5,6 +5,7 @@
package integration
import (
+ "maps"
"strings"
"testing"
"time"
@@ -25,7 +26,7 @@ type runConfig struct {
func defaultConfig() runConfig {
return runConfig{
editor: fake.EditorConfig{
- Settings: map[string]interface{}{
+ Settings: map[string]any{
// Shorten the diagnostic delay to speed up test execution (else we'd add
// the default delay to each assertion about diagnostics)
"diagnosticsDelay": "10ms",
@@ -109,15 +110,13 @@ func CapabilitiesJSON(capabilities []byte) RunOption {
//
// As a special case, the env setting must not be provided via Settings: use
// EnvVars instead.
-type Settings map[string]interface{}
+type Settings map[string]any
func (s Settings) set(opts *runConfig) {
if opts.editor.Settings == nil {
- opts.editor.Settings = make(map[string]interface{})
- }
- for k, v := range s {
- opts.editor.Settings[k] = v
+ opts.editor.Settings = make(map[string]any)
}
+ maps.Copy(opts.editor.Settings, s)
}
// WorkspaceFolders configures the workdir-relative workspace folders or uri
@@ -135,6 +134,22 @@ func WorkspaceFolders(relFolders ...string) RunOption {
})
}
+// NoDefaultWorkspaceFiles is used to specify whether the fake editor
+// should give a default workspace folder to the LSP server.
+// When it's true, the editor will pass original WorkspaceFolders to the LSP server.
+func NoDefaultWorkspaceFiles() RunOption {
+ return optionSetter(func(opts *runConfig) {
+ opts.editor.NoDefaultWorkspaceFiles = true
+ })
+}
+
+// RootPath configures the roo path which will be converted to rootUri and sent to the LSP server.
+func RootPath(relpath string) RunOption {
+ return optionSetter(func(opts *runConfig) {
+ opts.editor.RelRootPath = relpath
+ })
+}
+
// FolderSettings defines per-folder workspace settings, keyed by relative path
// to the folder.
//
@@ -161,9 +176,7 @@ func (e EnvVars) set(opts *runConfig) {
if opts.editor.Env == nil {
opts.editor.Env = make(map[string]string)
}
- for k, v := range e {
- opts.editor.Env[k] = v
- }
+ maps.Copy(opts.editor.Env, e)
}
// FakeGoPackagesDriver configures gopls to run with a fake GOPACKAGESDRIVER
diff --git a/gopls/internal/test/integration/runner.go b/gopls/internal/test/integration/runner.go
index 6d10b16cab3..8fdcc26af59 100644
--- a/gopls/internal/test/integration/runner.go
+++ b/gopls/internal/test/integration/runner.go
@@ -142,7 +142,6 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
}
for _, tc := range tests {
- tc := tc
config := defaultConfig()
for _, opt := range opts {
opt.set(&config)
@@ -173,7 +172,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
}
// TODO(rfindley): do we need an instance at all? Can it be removed?
- ctx = debug.WithInstance(ctx, "off")
+ ctx = debug.WithInstance(ctx)
rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name()))
if err := os.MkdirAll(rootDir, 0755); err != nil {
@@ -253,7 +252,7 @@ func ConnectGoplsEnv(t testing.TB, ctx context.Context, sandbox *fake.Sandbox, c
t.Fatal(err)
}
env := &Env{
- T: t,
+ TB: t,
Ctx: ctx,
Sandbox: sandbox,
Server: connector,
@@ -266,10 +265,10 @@ func ConnectGoplsEnv(t testing.TB, ctx context.Context, sandbox *fake.Sandbox, c
// longBuilders maps builders that are skipped when -short is set to a
// (possibly empty) justification.
var longBuilders = map[string]string{
- "openbsd-amd64-64": "go.dev/issue/42789",
- "openbsd-386-64": "go.dev/issue/42789",
- "openbsd-386-68": "go.dev/issue/42789",
- "openbsd-amd64-68": "go.dev/issue/42789",
+ "x_tools-gotip-openbsd-amd64": "go.dev/issue/72145",
+ "x_tools-go1.24-openbsd-amd64": "go.dev/issue/72145",
+ "x_tools-go1.23-openbsd-amd64": "go.dev/issue/72145",
+
"darwin-amd64-10_12": "",
"freebsd-amd64-race": "",
"illumos-amd64": "",
@@ -349,7 +348,7 @@ func (r *Runner) defaultServer() jsonrpc2.StreamServer {
func (r *Runner) forwardedServer() jsonrpc2.StreamServer {
r.tsOnce.Do(func() {
ctx := context.Background()
- ctx = debug.WithInstance(ctx, "off")
+ ctx = debug.WithInstance(ctx)
ss := lsprpc.NewStreamServer(cache.New(nil), false, nil)
r.ts = servertest.NewTCPServer(ctx, ss, nil)
})
diff --git a/gopls/internal/test/integration/web/assembly_test.go b/gopls/internal/test/integration/web/assembly_test.go
new file mode 100644
index 00000000000..6820cbb7864
--- /dev/null
+++ b/gopls/internal/test/integration/web/assembly_test.go
@@ -0,0 +1,135 @@
+// Copyright 2025 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 web_test
+
+import (
+ "runtime"
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
+ "golang.org/x/tools/gopls/internal/settings"
+ . "golang.org/x/tools/gopls/internal/test/integration"
+ "golang.org/x/tools/internal/testenv"
+)
+
+// TestAssembly is a basic test of the web-based assembly listing.
+func TestAssembly(t *testing.T) {
+ testenv.NeedsGoCommand1Point(t, 22) // for up-to-date assembly listing
+
+ const files = `
+-- go.mod --
+module example.com
+
+-- a/a.go --
+package a
+
+func f(x int) int {
+ println("hello")
+ defer println("world")
+ return x
+}
+
+func g() {
+ println("goodbye")
+}
+
+var v = [...]int{
+ f(123),
+ f(456),
+}
+
+func init() {
+ f(789)
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+
+ asmFor := func(pattern string) []byte {
+ // Invoke the "Browse assembly" code action to start the server.
+ loc := env.RegexpSearch("a/a.go", pattern)
+ actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger)
+ if err != nil {
+ t.Fatalf("CodeAction: %v", err)
+ }
+ action, err := codeActionByKind(actions, settings.GoAssembly)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Execute the command.
+ // Its side effect should be a single showDocument request.
+ params := &protocol.ExecuteCommandParams{
+ Command: action.Command.Command,
+ Arguments: action.Command.Arguments,
+ }
+ var result command.DebuggingResult
+ collectDocs := env.Awaiter.ListenToShownDocuments()
+ env.ExecuteCommand(params, &result)
+ doc := shownDocument(t, collectDocs(), "http:")
+ if doc == nil {
+ t.Fatalf("no showDocument call had 'file:' prefix")
+ }
+ t.Log("showDocument(package doc) URL:", doc.URI)
+
+ return get(t, doc.URI)
+ }
+
+ // Get the report and do some minimal checks for sensible results.
+ //
+ // Use only portable instructions below! Remember that
+ // this is a test of plumbing, not compilation, so
+ // it's better to skip the tests, rather than refine
+ // them, on any architecture that gives us trouble
+ // (e.g. uses JAL for CALL, or BL for RET).
+ // We conservatively test only on the two most popular
+ // architectures.
+ {
+ report := asmFor("println")
+ checkMatch(t, true, report, `TEXT.*example.com/a.f`)
+ switch runtime.GOARCH {
+ case "amd64", "arm64":
+ checkMatch(t, true, report, `CALL runtime.printlock`)
+ checkMatch(t, true, report, `CALL runtime.printstring`)
+ checkMatch(t, true, report, `CALL runtime.printunlock`)
+ checkMatch(t, true, report, `CALL example.com/a.f.deferwrap`)
+ checkMatch(t, true, report, `RET`)
+ checkMatch(t, true, report, `CALL runtime.morestack_noctxt`)
+ }
+
+ // Nested functions are also shown.
+ //
+ // The condition here was relaxed to unblock go.dev/cl/639515.
+ checkMatch(t, true, report, `example.com/a.f.deferwrap`)
+
+ // But other functions are not.
+ checkMatch(t, false, report, `TEXT.*example.com/a.g`)
+ }
+
+ // Check that code in a package-level var initializer is found too.
+ {
+ report := asmFor(`f\(123\)`)
+ switch runtime.GOARCH {
+ case "amd64", "arm64":
+ checkMatch(t, true, report, `TEXT.*example.com/a.init`)
+ checkMatch(t, true, report, `MOV.? \$123`)
+ checkMatch(t, true, report, `MOV.? \$456`)
+ checkMatch(t, true, report, `CALL example.com/a.f`)
+ }
+ }
+
+ // And code in a source-level init function.
+ {
+ report := asmFor(`f\(789\)`)
+ switch runtime.GOARCH {
+ case "amd64", "arm64":
+ checkMatch(t, true, report, `TEXT.*example.com/a.init`)
+ checkMatch(t, true, report, `MOV.? \$789`)
+ checkMatch(t, true, report, `CALL example.com/a.f`)
+ }
+ }
+ })
+}
diff --git a/gopls/internal/test/integration/web/freesymbols_test.go b/gopls/internal/test/integration/web/freesymbols_test.go
new file mode 100644
index 00000000000..7f44c29ec1f
--- /dev/null
+++ b/gopls/internal/test/integration/web/freesymbols_test.go
@@ -0,0 +1,76 @@
+// Copyright 2025 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 web_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
+ "golang.org/x/tools/gopls/internal/settings"
+ . "golang.org/x/tools/gopls/internal/test/integration"
+)
+
+// TestFreeSymbols is a basic test of interaction with the "free symbols" web report.
+func TestFreeSymbols(t *testing.T) {
+ const files = `
+-- go.mod --
+module example.com
+
+-- a/a.go --
+package a
+
+import "fmt"
+import "bytes"
+
+func f(buf bytes.Buffer, greeting string) {
+/* « */
+ fmt.Fprintf(&buf, "%s", greeting)
+ buf.WriteString(fmt.Sprint("foo"))
+ buf.WriteByte(0)
+/* » */
+ buf.Write(nil)
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+
+ // Invoke the "Browse free symbols" code
+ // action to start the server.
+ loc := env.RegexpSearch("a/a.go", "«((?:.|\n)*)»")
+ actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger)
+ if err != nil {
+ t.Fatalf("CodeAction: %v", err)
+ }
+ action, err := codeActionByKind(actions, settings.GoFreeSymbols)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Execute the command.
+ // Its side effect should be a single showDocument request.
+ params := &protocol.ExecuteCommandParams{
+ Command: action.Command.Command,
+ Arguments: action.Command.Arguments,
+ }
+ var result command.DebuggingResult
+ collectDocs := env.Awaiter.ListenToShownDocuments()
+ env.ExecuteCommand(params, &result)
+ doc := shownDocument(t, collectDocs(), "http:")
+ if doc == nil {
+ t.Fatalf("no showDocument call had 'file:' prefix")
+ }
+ t.Log("showDocument(package doc) URL:", doc.URI)
+
+ // Get the report and do some minimal checks for sensible results.
+ report := get(t, doc.URI)
+ checkMatch(t, true, report, `