diff --git a/cmd/auth/netrcauth/netrcauth.go b/cmd/auth/netrcauth/netrcauth.go
index 7d29c96035e..a730e646a81 100644
--- a/cmd/auth/netrcauth/netrcauth.go
+++ b/cmd/auth/netrcauth/netrcauth.go
@@ -16,7 +16,6 @@ package main
import (
"fmt"
- "io/ioutil"
"log"
"net/http"
"net/url"
@@ -41,7 +40,7 @@ func main() {
path := os.Args[1]
- data, err := ioutil.ReadFile(path)
+ data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return
diff --git a/cmd/bisect/main_test.go b/cmd/bisect/main_test.go
index bff1bf23c0c..7c10ff0fb4b 100644
--- a/cmd/bisect/main_test.go
+++ b/cmd/bisect/main_test.go
@@ -17,6 +17,7 @@ import (
"testing"
"golang.org/x/tools/internal/bisect"
+ "golang.org/x/tools/internal/compat"
"golang.org/x/tools/internal/diffp"
"golang.org/x/tools/txtar"
)
@@ -81,7 +82,7 @@ func Test(t *testing.T) {
have[color] = true
}
if m.ShouldReport(uint64(i)) {
- out = fmt.Appendf(out, "%s %s\n", color, bisect.Marker(uint64(i)))
+ out = compat.Appendf(out, "%s %s\n", color, bisect.Marker(uint64(i)))
}
}
err = nil
diff --git a/cmd/bundle/main.go b/cmd/bundle/main.go
index 194797bd822..a5c426d8f88 100644
--- a/cmd/bundle/main.go
+++ b/cmd/bundle/main.go
@@ -79,7 +79,6 @@ import (
"go/printer"
"go/token"
"go/types"
- "io/ioutil"
"log"
"os"
"strconv"
@@ -149,7 +148,7 @@ func main() {
log.Fatal(err)
}
if *outputFile != "" {
- err := ioutil.WriteFile(*outputFile, code, 0666)
+ err := os.WriteFile(*outputFile, code, 0666)
if err != nil {
log.Fatal(err)
}
diff --git a/cmd/bundle/main_test.go b/cmd/bundle/main_test.go
index 10d790fa28e..4ee8521a074 100644
--- a/cmd/bundle/main_test.go
+++ b/cmd/bundle/main_test.go
@@ -6,7 +6,6 @@ package main
import (
"bytes"
- "io/ioutil"
"os"
"os/exec"
"runtime"
@@ -18,7 +17,7 @@ import (
func TestBundle(t *testing.T) { packagestest.TestAll(t, testBundle) }
func testBundle(t *testing.T, x packagestest.Exporter) {
load := func(name string) string {
- data, err := ioutil.ReadFile(name)
+ data, err := os.ReadFile(name)
if err != nil {
t.Fatal(err)
}
@@ -53,7 +52,7 @@ func testBundle(t *testing.T, x packagestest.Exporter) {
if got, want := string(out), load("testdata/out.golden"); got != want {
t.Errorf("-- got --\n%s\n-- want --\n%s\n-- diff --", got, want)
- if err := ioutil.WriteFile("testdata/out.got", out, 0644); err != nil {
+ if err := os.WriteFile("testdata/out.got", out, 0644); err != nil {
t.Fatal(err)
}
t.Log(diff("testdata/out.golden", "testdata/out.got"))
diff --git a/cmd/compilebench/main.go b/cmd/compilebench/main.go
index 6900b2576d3..681ffd346b2 100644
--- a/cmd/compilebench/main.go
+++ b/cmd/compilebench/main.go
@@ -81,7 +81,6 @@ import (
"encoding/json"
"flag"
"fmt"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -95,12 +94,13 @@ import (
)
var (
- goroot string
- compiler string
- assembler string
- linker string
- runRE *regexp.Regexp
- is6g bool
+ goroot string
+ compiler string
+ assembler string
+ linker string
+ runRE *regexp.Regexp
+ is6g bool
+ needCompilingRuntimeFlag bool
)
var (
@@ -186,6 +186,9 @@ func main() {
if assembler == "" {
_, assembler = toolPath("asm")
}
+ if err := checkCompilingRuntimeFlag(assembler); err != nil {
+ log.Fatalf("checkCompilingRuntimeFlag: %v", err)
+ }
linker = *flagLinker
if linker == "" && !is6g { // TODO: Support 6l
@@ -388,7 +391,7 @@ func (c compile) run(name string, count int) error {
opath := pkg.Dir + "/_compilebench_.o"
if *flagObj {
// TODO(josharian): object files are big; just read enough to find what we seek.
- data, err := ioutil.ReadFile(opath)
+ data, err := os.ReadFile(opath)
if err != nil {
log.Print(err)
}
@@ -498,7 +501,7 @@ func runBuildCmd(name string, count int, dir, tool string, args []string) error
haveAllocs, haveRSS := false, false
var allocs, allocbytes, rssbytes int64
if *flagAlloc || *flagMemprofile != "" {
- out, err := ioutil.ReadFile(dir + "/_compilebench_.memprof")
+ out, err := os.ReadFile(dir + "/_compilebench_.memprof")
if err != nil {
log.Print("cannot find memory profile after compilation")
}
@@ -531,7 +534,7 @@ func runBuildCmd(name string, count int, dir, tool string, args []string) error
if *flagCount != 1 {
outpath = fmt.Sprintf("%s_%d", outpath, count)
}
- if err := ioutil.WriteFile(outpath, out, 0666); err != nil {
+ if err := os.WriteFile(outpath, out, 0666); err != nil {
log.Print(err)
}
}
@@ -539,7 +542,7 @@ func runBuildCmd(name string, count int, dir, tool string, args []string) error
}
if *flagCpuprofile != "" {
- out, err := ioutil.ReadFile(dir + "/_compilebench_.cpuprof")
+ out, err := os.ReadFile(dir + "/_compilebench_.cpuprof")
if err != nil {
log.Print(err)
}
@@ -547,7 +550,7 @@ func runBuildCmd(name string, count int, dir, tool string, args []string) error
if *flagCount != 1 {
outpath = fmt.Sprintf("%s_%d", outpath, count)
}
- if err := ioutil.WriteFile(outpath, out, 0666); err != nil {
+ if err := os.WriteFile(outpath, out, 0666); err != nil {
log.Print(err)
}
os.Remove(dir + "/_compilebench_.cpuprof")
@@ -567,6 +570,45 @@ func runBuildCmd(name string, count int, dir, tool string, args []string) error
return nil
}
+func checkCompilingRuntimeFlag(assembler string) error {
+ td, err := os.MkdirTemp("", "asmsrcd")
+ if err != nil {
+ return fmt.Errorf("MkdirTemp failed: %v", err)
+ }
+ defer os.RemoveAll(td)
+ src := filepath.Join(td, "asm.s")
+ obj := filepath.Join(td, "asm.o")
+ const code = `
+TEXT ·foo(SB),$0-0
+RET
+`
+ if err := os.WriteFile(src, []byte(code), 0644); err != nil {
+ return fmt.Errorf("writing %s failed: %v", src, err)
+ }
+
+ // Try compiling the assembly source file passing
+ // -compiling-runtime; if it succeeds, then we'll need it
+ // when doing assembly of the reflect package later on.
+ // If it does not succeed, the assumption is that it's not
+ // needed.
+ args := []string{"-o", obj, "-p", "reflect", "-compiling-runtime", src}
+ cmd := exec.Command(assembler, args...)
+ cmd.Dir = td
+ out, aerr := cmd.CombinedOutput()
+ if aerr != nil {
+ if strings.Contains(string(out), "flag provided but not defined: -compiling-runtime") {
+ // flag not defined: assume we're using a recent assembler, so
+ // don't use -compiling-runtime.
+ return nil
+ }
+ // error is not flag-related; report it.
+ return fmt.Errorf("problems invoking assembler with args %+v: error %v\n%s\n", args, aerr, out)
+ }
+ // asm invocation succeeded -- assume we need the flag.
+ needCompilingRuntimeFlag = true
+ return nil
+}
+
// genSymAbisFile runs the assembler on the target package asm files
// with "-gensymabis" to produce a symabis file that will feed into
// the Go source compilation. This is fairly hacky in that if the
@@ -579,7 +621,7 @@ func genSymAbisFile(pkg *Pkg, symAbisFile, incdir string) error {
"-I", incdir,
"-D", "GOOS_" + runtime.GOOS,
"-D", "GOARCH_" + runtime.GOARCH}
- if pkg.ImportPath == "reflect" {
+ if pkg.ImportPath == "reflect" && needCompilingRuntimeFlag {
args = append(args, "-compiling-runtime")
}
args = append(args, pkg.SFiles...)
diff --git a/cmd/eg/eg.go b/cmd/eg/eg.go
index 1629b801cd4..5d21138a49e 100644
--- a/cmd/eg/eg.go
+++ b/cmd/eg/eg.go
@@ -15,7 +15,6 @@ import (
"go/parser"
"go/token"
"go/types"
- "io/ioutil"
"os"
"path/filepath"
"strings"
@@ -77,7 +76,7 @@ func doMain() error {
if err != nil {
return err
}
- template, err := ioutil.ReadFile(tAbs)
+ template, err := os.ReadFile(tAbs)
if err != nil {
return err
}
diff --git a/cmd/file2fuzz/main.go b/cmd/file2fuzz/main.go
index ed212cb9d72..c2b7ee52089 100644
--- a/cmd/file2fuzz/main.go
+++ b/cmd/file2fuzz/main.go
@@ -25,7 +25,6 @@ import (
"flag"
"fmt"
"io"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -51,7 +50,7 @@ func dirWriter(dir string) func([]byte) error {
if err := os.MkdirAll(dir, 0777); err != nil {
return err
}
- if err := ioutil.WriteFile(name, b, 0666); err != nil {
+ if err := os.WriteFile(name, b, 0666); err != nil {
os.Remove(name)
return err
}
@@ -98,14 +97,14 @@ func convert(inputArgs []string, outputArg string) error {
output = dirWriter(outputArg)
} else {
output = func(b []byte) error {
- return ioutil.WriteFile(outputArg, b, 0666)
+ return os.WriteFile(outputArg, b, 0666)
}
}
}
}
for _, f := range input {
- b, err := ioutil.ReadAll(f)
+ b, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("unable to read input: %s", err)
}
diff --git a/cmd/file2fuzz/main_test.go b/cmd/file2fuzz/main_test.go
index f9f54919a33..83653d2dd77 100644
--- a/cmd/file2fuzz/main_test.go
+++ b/cmd/file2fuzz/main_test.go
@@ -5,7 +5,6 @@
package main
import (
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -118,9 +117,9 @@ func TestFile2Fuzz(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- tmp, err := ioutil.TempDir(os.TempDir(), "file2fuzz")
+ tmp, err := os.MkdirTemp(os.TempDir(), "file2fuzz")
if err != nil {
- t.Fatalf("ioutil.TempDir failed: %s", err)
+ t.Fatalf("os.MkdirTemp failed: %s", err)
}
defer os.RemoveAll(tmp)
for _, f := range tc.inputFiles {
@@ -129,7 +128,7 @@ func TestFile2Fuzz(t *testing.T) {
t.Fatalf("failed to create test directory: %s", err)
}
} else {
- if err := ioutil.WriteFile(filepath.Join(tmp, f.name), []byte(f.content), 0666); err != nil {
+ if err := os.WriteFile(filepath.Join(tmp, f.name), []byte(f.content), 0666); err != nil {
t.Fatalf("failed to create test input file: %s", err)
}
}
@@ -146,7 +145,7 @@ func TestFile2Fuzz(t *testing.T) {
}
for _, f := range tc.expectedFiles {
- c, err := ioutil.ReadFile(filepath.Join(tmp, f.name))
+ c, err := os.ReadFile(filepath.Join(tmp, f.name))
if err != nil {
t.Fatalf("failed to read expected output file %q: %s", f.name, err)
}
diff --git a/cmd/fiximports/main.go b/cmd/fiximports/main.go
index 8eeacd1eda3..0893b068756 100644
--- a/cmd/fiximports/main.go
+++ b/cmd/fiximports/main.go
@@ -76,7 +76,6 @@ import (
"go/parser"
"go/token"
"io"
- "io/ioutil"
"log"
"os"
"path"
@@ -100,7 +99,7 @@ var (
// seams for testing
var (
stderr io.Writer = os.Stderr
- writeFile = ioutil.WriteFile
+ writeFile = os.WriteFile
)
const usage = `fiximports: rewrite import paths to use canonical package names.
diff --git a/cmd/getgo/download.go b/cmd/getgo/download.go
index 86f0a2fed80..18e1aec2eef 100644
--- a/cmd/getgo/download.go
+++ b/cmd/getgo/download.go
@@ -15,7 +15,6 @@ import (
"encoding/json"
"fmt"
"io"
- "io/ioutil"
"net/http"
"os"
"path/filepath"
@@ -51,7 +50,7 @@ func downloadGoVersion(version, ops, arch, dest string) error {
}
defer resp.Body.Close()
- tmpf, err := ioutil.TempFile("", "go")
+ tmpf, err := os.CreateTemp("", "go")
if err != nil {
return err
}
@@ -75,7 +74,7 @@ func downloadGoVersion(version, ops, arch, dest string) error {
return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed with HTTP status %s", uri, sresp.Status)
}
- shasum, err := ioutil.ReadAll(sresp.Body)
+ shasum, err := io.ReadAll(sresp.Body)
if err != nil {
return err
}
@@ -174,7 +173,7 @@ func getLatestGoVersion() (string, error) {
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1024))
+ b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return "", fmt.Errorf("Could not get current Go release: HTTP %d: %q", resp.StatusCode, b)
}
var releases []struct {
diff --git a/cmd/getgo/download_test.go b/cmd/getgo/download_test.go
index 76cd96cbd1e..b4f2059d14e 100644
--- a/cmd/getgo/download_test.go
+++ b/cmd/getgo/download_test.go
@@ -8,7 +8,6 @@
package main
import (
- "io/ioutil"
"os"
"path/filepath"
"testing"
@@ -19,7 +18,7 @@ func TestDownloadGoVersion(t *testing.T) {
t.Skipf("Skipping download in short mode")
}
- tmpd, err := ioutil.TempDir("", "go")
+ tmpd, err := os.MkdirTemp("", "go")
if err != nil {
t.Fatal(err)
}
diff --git a/cmd/getgo/main_test.go b/cmd/getgo/main_test.go
index fc28c5df904..878137dd3f4 100644
--- a/cmd/getgo/main_test.go
+++ b/cmd/getgo/main_test.go
@@ -10,7 +10,6 @@ package main
import (
"bytes"
"fmt"
- "io/ioutil"
"os"
"os/exec"
"testing"
@@ -37,7 +36,7 @@ func TestMain(m *testing.M) {
}
func createTmpHome(t *testing.T) string {
- tmpd, err := ioutil.TempDir("", "testgetgo")
+ tmpd, err := os.MkdirTemp("", "testgetgo")
if err != nil {
t.Fatalf("creating test tempdir failed: %v", err)
}
@@ -86,7 +85,7 @@ func TestCommandVerbose(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- b, err := ioutil.ReadFile(shellConfig)
+ b, err := os.ReadFile(shellConfig)
if err != nil {
t.Fatal(err)
}
@@ -122,7 +121,7 @@ func TestCommandPathExists(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- b, err := ioutil.ReadFile(shellConfig)
+ b, err := os.ReadFile(shellConfig)
if err != nil {
t.Fatal(err)
}
@@ -146,7 +145,7 @@ export PATH=$PATH:%s/go/bin
t.Fatal(err)
}
- b, err = ioutil.ReadFile(shellConfig)
+ b, err = os.ReadFile(shellConfig)
if err != nil {
t.Fatal(err)
}
diff --git a/cmd/getgo/path_test.go b/cmd/getgo/path_test.go
index 2249c5447b7..8195f2e68d5 100644
--- a/cmd/getgo/path_test.go
+++ b/cmd/getgo/path_test.go
@@ -8,7 +8,6 @@
package main
import (
- "io/ioutil"
"os"
"path/filepath"
"strings"
@@ -16,7 +15,7 @@ import (
)
func TestAppendPath(t *testing.T) {
- tmpd, err := ioutil.TempDir("", "go")
+ tmpd, err := os.MkdirTemp("", "go")
if err != nil {
t.Fatal(err)
}
@@ -35,7 +34,7 @@ func TestAppendPath(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- b, err := ioutil.ReadFile(shellConfig)
+ b, err := os.ReadFile(shellConfig)
if err != nil {
t.Fatal(err)
}
@@ -49,7 +48,7 @@ func TestAppendPath(t *testing.T) {
if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil {
t.Fatal(err)
}
- b, err = ioutil.ReadFile(shellConfig)
+ b, err = os.ReadFile(shellConfig)
if err != nil {
t.Fatal(err)
}
diff --git a/cmd/go-contrib-init/contrib.go b/cmd/go-contrib-init/contrib.go
index e2bb5070c60..9b4d265025c 100644
--- a/cmd/go-contrib-init/contrib.go
+++ b/cmd/go-contrib-init/contrib.go
@@ -14,7 +14,6 @@ import (
"fmt"
"go/build"
exec "golang.org/x/sys/execabs"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -66,7 +65,7 @@ func detectrepo() string {
var googleSourceRx = regexp.MustCompile(`(?m)^(go|go-review)?\.googlesource.com\b`)
func checkCLA() {
- slurp, err := ioutil.ReadFile(cookiesFile())
+ slurp, err := os.ReadFile(cookiesFile())
if err != nil && !os.IsNotExist(err) {
log.Fatal(err)
}
@@ -135,7 +134,7 @@ func checkGoroot() {
"your GOROOT or set it to the path of your development version\n"+
"of Go.", v)
}
- slurp, err := ioutil.ReadFile(filepath.Join(v, "VERSION"))
+ slurp, err := os.ReadFile(filepath.Join(v, "VERSION"))
if err == nil {
slurp = bytes.TrimSpace(slurp)
log.Fatalf("Your GOROOT environment variable is set to %q\n"+
diff --git a/cmd/godex/godex.go b/cmd/godex/godex.go
index e1d7e2f9243..e91dbfcea5f 100644
--- a/cmd/godex/godex.go
+++ b/cmd/godex/godex.go
@@ -10,7 +10,6 @@ import (
"fmt"
"go/build"
"go/types"
- "io/ioutil"
"os"
"path/filepath"
"strings"
@@ -197,7 +196,7 @@ func genPrefixes(out chan string, all bool) {
}
func walkDir(dirname, prefix string, out chan string) {
- fiList, err := ioutil.ReadDir(dirname)
+ fiList, err := os.ReadDir(dirname)
if err != nil {
return
}
diff --git a/cmd/godoc/godoc_test.go b/cmd/godoc/godoc_test.go
index b2ebe8581a6..42582c4b228 100644
--- a/cmd/godoc/godoc_test.go
+++ b/cmd/godoc/godoc_test.go
@@ -9,7 +9,7 @@ import (
"context"
"fmt"
"go/build"
- "io/ioutil"
+ "io"
"net"
"net/http"
"os"
@@ -111,7 +111,7 @@ func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse
if err != nil {
continue
}
- body, err := ioutil.ReadAll(res.Body)
+ body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil || res.StatusCode != http.StatusOK {
continue
@@ -396,7 +396,7 @@ package a; import _ "godoc.test/repo2/a"; const Name = "repo1a"`,
t.Errorf("GET %s failed: %s", url, err)
continue
}
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
strBody := string(body)
resp.Body.Close()
if err != nil {
diff --git a/cmd/goimports/goimports.go b/cmd/goimports/goimports.go
index b354c9e8241..3b6bd72503e 100644
--- a/cmd/goimports/goimports.go
+++ b/cmd/goimports/goimports.go
@@ -13,7 +13,6 @@ import (
"go/scanner"
exec "golang.org/x/sys/execabs"
"io"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -106,7 +105,7 @@ func processFile(filename string, in io.Reader, out io.Writer, argType argumentT
in = f
}
- src, err := ioutil.ReadAll(in)
+ src, err := io.ReadAll(in)
if err != nil {
return err
}
@@ -159,7 +158,7 @@ func processFile(filename string, in io.Reader, out io.Writer, argType argumentT
if fi, err := os.Stat(filename); err == nil {
perms = fi.Mode() & os.ModePerm
}
- err = ioutil.WriteFile(filename, res, perms)
+ err = os.WriteFile(filename, res, perms)
if err != nil {
return err
}
@@ -296,7 +295,7 @@ func gofmtMain() {
}
func writeTempFile(dir, prefix string, data []byte) (string, error) {
- file, err := ioutil.TempFile(dir, prefix)
+ file, err := os.CreateTemp(dir, prefix)
if err != nil {
return "", err
}
diff --git a/cmd/gorename/gorename_test.go b/cmd/gorename/gorename_test.go
index 30b87967140..f72b6f4a429 100644
--- a/cmd/gorename/gorename_test.go
+++ b/cmd/gorename/gorename_test.go
@@ -6,7 +6,6 @@ package main_test
import (
"fmt"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -315,7 +314,7 @@ func buildGorename(t *testing.T) (tmp, bin string, cleanup func()) {
t.Skipf("the dependencies are not available on android")
}
- tmp, err := ioutil.TempDir("", "gorename-regtest-")
+ tmp, err := os.MkdirTemp("", "gorename-regtest-")
if err != nil {
t.Fatal(err)
}
@@ -352,7 +351,7 @@ func setUpPackages(t *testing.T, dir string, packages map[string][]string) (clea
// Write the packages files
for i, val := range files {
file := filepath.Join(pkgDir, strconv.Itoa(i)+".go")
- if err := ioutil.WriteFile(file, []byte(val), os.ModePerm); err != nil {
+ if err := os.WriteFile(file, []byte(val), os.ModePerm); err != nil {
t.Fatal(err)
}
}
@@ -373,7 +372,7 @@ func modifiedFiles(t *testing.T, dir string, packages map[string][]string) (resu
for i, val := range files {
file := filepath.Join(pkgDir, strconv.Itoa(i)+".go")
// read file contents and compare to val
- if contents, err := ioutil.ReadFile(file); err != nil {
+ if contents, err := os.ReadFile(file); err != nil {
t.Fatalf("File missing: %s", err)
} else if string(contents) != val {
results = append(results, strings.TrimPrefix(dir, file))
diff --git a/cmd/gotype/gotype.go b/cmd/gotype/gotype.go
index 08b52057f55..4a731f26233 100644
--- a/cmd/gotype/gotype.go
+++ b/cmd/gotype/gotype.go
@@ -97,7 +97,7 @@ import (
"go/scanner"
"go/token"
"go/types"
- "io/ioutil"
+ "io"
"os"
"path/filepath"
"sync"
@@ -197,7 +197,7 @@ func parse(filename string, src interface{}) (*ast.File, error) {
}
func parseStdin() (*ast.File, error) {
- src, err := ioutil.ReadAll(os.Stdin)
+ src, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, err
}
diff --git a/cmd/goyacc/yacc.go b/cmd/goyacc/yacc.go
index 948e5010417..5a8ede0a482 100644
--- a/cmd/goyacc/yacc.go
+++ b/cmd/goyacc/yacc.go
@@ -50,7 +50,6 @@ import (
"flag"
"fmt"
"go/format"
- "io/ioutil"
"math"
"os"
"strconv"
@@ -3209,7 +3208,7 @@ func exit(status int) {
}
func gofmt() {
- src, err := ioutil.ReadFile(oflag)
+ src, err := os.ReadFile(oflag)
if err != nil {
return
}
@@ -3217,7 +3216,7 @@ func gofmt() {
if err != nil {
return
}
- ioutil.WriteFile(oflag, src, 0666)
+ os.WriteFile(oflag, src, 0666)
}
var yaccpar string // will be processed version of yaccpartext: s/$$/prefix/g
diff --git a/cmd/guru/guru_test.go b/cmd/guru/guru_test.go
index 905a9e2cf49..d3c38e0a472 100644
--- a/cmd/guru/guru_test.go
+++ b/cmd/guru/guru_test.go
@@ -34,7 +34,6 @@ import (
"go/parser"
"go/token"
"io"
- "io/ioutil"
"log"
"os"
"os/exec"
@@ -83,7 +82,7 @@ func parseRegexp(text string) (*regexp.Regexp, error) {
// parseQueries parses and returns the queries in the named file.
func parseQueries(t *testing.T, filename string) []*query {
- filedata, err := ioutil.ReadFile(filename)
+ filedata, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
@@ -266,7 +265,7 @@ func TestGuru(t *testing.T) {
json := strings.Contains(filename, "-json/")
queries := parseQueries(t, filename)
golden := filename + "lden"
- gotfh, err := ioutil.TempFile("", filepath.Base(filename)+"t")
+ gotfh, err := os.CreateTemp("", filepath.Base(filename)+"t")
if err != nil {
t.Fatal(err)
}
diff --git a/cmd/guru/referrers.go b/cmd/guru/referrers.go
index d75196bf93a..70db3d1841a 100644
--- a/cmd/guru/referrers.go
+++ b/cmd/guru/referrers.go
@@ -765,7 +765,7 @@ func (r *referrersPackageResult) foreachRef(f func(id *ast.Ident, text string))
}
}
-// readFile is like ioutil.ReadFile, but
+// readFile is like os.ReadFile, but
// it goes through the virtualized build.Context.
// If non-nil, buf must have been reset.
func readFile(ctxt *build.Context, filename string, buf *bytes.Buffer) ([]byte, error) {
diff --git a/cmd/guru/unit_test.go b/cmd/guru/unit_test.go
index 699e6a1b10f..7c24d714f19 100644
--- a/cmd/guru/unit_test.go
+++ b/cmd/guru/unit_test.go
@@ -7,7 +7,6 @@ package main
import (
"fmt"
"go/build"
- "io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -27,7 +26,7 @@ func TestIssue17515(t *testing.T) {
// (4) symlink & absolute: GOPATH=$HOME/src file= $HOME/go/src/test/test.go
// Create a temporary home directory under /tmp
- home, err := ioutil.TempDir(os.TempDir(), "home")
+ home, err := os.MkdirTemp(os.TempDir(), "home")
if err != nil {
t.Errorf("Unable to create a temporary directory in %s", os.TempDir())
}
diff --git a/cmd/present2md/main.go b/cmd/present2md/main.go
index 748b041e41f..a11e57ecf8b 100644
--- a/cmd/present2md/main.go
+++ b/cmd/present2md/main.go
@@ -25,7 +25,6 @@ import (
"flag"
"fmt"
"io"
- "io/ioutil"
"log"
"net/url"
"os"
@@ -84,7 +83,7 @@ func main() {
// If writeBack is true, the converted version is written back to file.
// If writeBack is false, the converted version is printed to standard output.
func convert(r io.Reader, file string, writeBack bool) error {
- data, err := ioutil.ReadAll(r)
+ data, err := io.ReadAll(r)
if err != nil {
return err
}
@@ -184,7 +183,7 @@ func convert(r io.Reader, file string, writeBack bool) error {
os.Stdout.Write(md.Bytes())
return nil
}
- return ioutil.WriteFile(file, md.Bytes(), 0666)
+ return os.WriteFile(file, md.Bytes(), 0666)
}
func printSectionBody(file string, depth int, w *bytes.Buffer, elems []present.Elem) {
diff --git a/cmd/signature-fuzzer/fuzz-runner/runner.go b/cmd/signature-fuzzer/fuzz-runner/runner.go
index b77b218f5a8..27ab975f0c8 100644
--- a/cmd/signature-fuzzer/fuzz-runner/runner.go
+++ b/cmd/signature-fuzzer/fuzz-runner/runner.go
@@ -12,7 +12,6 @@ package main
import (
"flag"
"fmt"
- "io/ioutil"
"log"
"os"
"os/exec"
@@ -196,7 +195,7 @@ func (c *config) gen(singlepk int, singlefn int) {
func (c *config) action(cmd []string, outfile string, emitout bool) int {
st := docmdout(cmd, c.gendir, outfile)
if emitout {
- content, err := ioutil.ReadFile(outfile)
+ content, err := os.ReadFile(outfile)
if err != nil {
log.Fatal(err)
}
@@ -405,7 +404,7 @@ func main() {
}
verb(1, "in main, verblevel=%d", *verbflag)
- tmpdir, err := ioutil.TempDir("", "fuzzrun")
+ tmpdir, err := os.MkdirTemp("", "fuzzrun")
if err != nil {
fatal("creation of tempdir failed: %v", err)
}
diff --git a/cmd/stress/stress.go b/cmd/stress/stress.go
index f9817a1dda6..6dc563d7a87 100644
--- a/cmd/stress/stress.go
+++ b/cmd/stress/stress.go
@@ -20,7 +20,6 @@ package main
import (
"flag"
"fmt"
- "io/ioutil"
"os"
"path/filepath"
"regexp"
@@ -128,7 +127,7 @@ func main() {
}
fails++
dir, path := filepath.Split(*flagOutput)
- f, err := ioutil.TempFile(dir, path)
+ f, err := os.CreateTemp(dir, path)
if err != nil {
fmt.Printf("failed to create temp file: %v\n", err)
os.Exit(1)
diff --git a/cmd/stringer/endtoend_test.go b/cmd/stringer/endtoend_test.go
index e7faa4810ab..d513c1b52ba 100644
--- a/cmd/stringer/endtoend_test.go
+++ b/cmd/stringer/endtoend_test.go
@@ -11,11 +11,11 @@ package main
import (
"bytes"
+ "flag"
"fmt"
"go/build"
"io"
"os"
- "os/exec"
"path"
"path/filepath"
"strings"
@@ -42,6 +42,11 @@ func TestMain(m *testing.M) {
// command, and much less complicated and expensive to build and clean up.
os.Setenv("STRINGER_TEST_IS_STRINGER", "1")
+ flag.Parse()
+ if testing.Verbose() {
+ os.Setenv("GOPACKAGESDEBUG", "true")
+ }
+
os.Exit(m.Run())
}
@@ -74,11 +79,12 @@ func TestEndToEnd(t *testing.T) {
// This file is used for tag processing in TestTags or TestConstValueChange, below.
continue
}
- if name == "cgo.go" && !build.Default.CgoEnabled {
- t.Logf("cgo is not enabled for %s", name)
- continue
- }
- stringerCompileAndRun(t, t.TempDir(), stringer, typeName(name), name)
+ t.Run(name, func(t *testing.T) {
+ if name == "cgo.go" && !build.Default.CgoEnabled {
+ t.Skipf("cgo is not enabled for %s", name)
+ }
+ stringerCompileAndRun(t, t.TempDir(), stringer, typeName(name), name)
+ })
}
}
@@ -122,7 +128,7 @@ func TestTags(t *testing.T) {
// - Versions of Go earlier than Go 1.11, do not support absolute directories as a pattern.
// - When the current directory is inside a go module, the path will not be considered
// a valid path to a package.
- err := runInDir(dir, stringer, "-type", "Const", ".")
+ err := runInDir(t, dir, stringer, "-type", "Const", ".")
if err != nil {
t.Fatal(err)
}
@@ -137,7 +143,7 @@ func TestTags(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- err = runInDir(dir, stringer, "-type", "Const", "-tags", "tag", ".")
+ err = runInDir(t, dir, stringer, "-type", "Const", "-tags", "tag", ".")
if err != nil {
t.Fatal(err)
}
@@ -162,12 +168,12 @@ func TestConstValueChange(t *testing.T) {
}
stringSource := filepath.Join(dir, "day_string.go")
// Run stringer in the directory that contains the package files.
- err = runInDir(dir, stringer, "-type", "Day", "-output", stringSource)
+ err = runInDir(t, dir, stringer, "-type", "Day", "-output", stringSource)
if err != nil {
t.Fatal(err)
}
// Run the binary in the temporary directory as a sanity check.
- err = run("go", "run", stringSource, source)
+ err = run(t, "go", "run", stringSource, source)
if err != nil {
t.Fatal(err)
}
@@ -185,8 +191,8 @@ func TestConstValueChange(t *testing.T) {
// output. An alternative might be to check that the error output
// matches a set of possible error strings emitted by known
// Go compilers.
- fmt.Fprintf(os.Stderr, "Note: the following messages should indicate an out-of-bounds compiler error\n")
- err = run("go", "build", stringSource, source)
+ t.Logf("Note: the following messages should indicate an out-of-bounds compiler error\n")
+ err = run(t, "go", "build", stringSource, source)
if err == nil {
t.Fatal("unexpected compiler success")
}
@@ -213,7 +219,6 @@ func stringerPath(t *testing.T) string {
// stringerCompileAndRun runs stringer for the named file and compiles and
// runs the target binary in directory dir. That binary will panic if the String method is incorrect.
func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName string) {
- t.Helper()
t.Logf("run: %s %s\n", fileName, typeName)
source := filepath.Join(dir, path.Base(fileName))
err := copy(source, filepath.Join("testdata", fileName))
@@ -222,12 +227,12 @@ func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName strin
}
stringSource := filepath.Join(dir, typeName+"_string.go")
// Run stringer in temporary directory.
- err = run(stringer, "-type", typeName, "-output", stringSource, source)
+ err = run(t, stringer, "-type", typeName, "-output", stringSource, source)
if err != nil {
t.Fatal(err)
}
// Run the binary in the temporary directory.
- err = run("go", "run", stringSource, source)
+ err = run(t, "go", "run", stringSource, source)
if err != nil {
t.Fatal(err)
}
@@ -251,17 +256,24 @@ func copy(to, from string) error {
// run runs a single command and returns an error if it does not succeed.
// os/exec should have this function, to be honest.
-func run(name string, arg ...string) error {
- return runInDir(".", name, arg...)
+func run(t testing.TB, name string, arg ...string) error {
+ t.Helper()
+ return runInDir(t, ".", name, arg...)
}
// runInDir runs a single command in directory dir and returns an error if
// it does not succeed.
-func runInDir(dir, name string, arg ...string) error {
- cmd := exec.Command(name, arg...)
+func runInDir(t testing.TB, dir, name string, arg ...string) error {
+ t.Helper()
+ cmd := testenv.Command(t, name, arg...)
cmd.Dir = dir
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "GO111MODULE=auto")
- return cmd.Run()
+ out, err := cmd.CombinedOutput()
+ if len(out) > 0 {
+ t.Logf("%s", out)
+ }
+ if err != nil {
+ return fmt.Errorf("%v: %v", cmd, err)
+ }
+ return nil
}
diff --git a/cmd/stringer/golden_test.go b/cmd/stringer/golden_test.go
index 250af05f903..a26eef35e36 100644
--- a/cmd/stringer/golden_test.go
+++ b/cmd/stringer/golden_test.go
@@ -453,28 +453,32 @@ func TestGolden(t *testing.T) {
dir := t.TempDir()
for _, test := range golden {
- g := Generator{
- trimPrefix: test.trimPrefix,
- lineComment: test.lineComment,
- }
- input := "package test\n" + test.input
- file := test.name + ".go"
- absFile := filepath.Join(dir, file)
- err := os.WriteFile(absFile, []byte(input), 0644)
- if err != nil {
- t.Error(err)
- }
-
- g.parsePackage([]string{absFile}, nil)
- // Extract the name and type of the constant from the first line.
- tokens := strings.SplitN(test.input, " ", 3)
- if len(tokens) != 3 {
- t.Fatalf("%s: need type declaration on first line", test.name)
- }
- g.generate(tokens[1])
- got := string(g.format())
- if got != test.output {
- t.Errorf("%s: got(%d)\n====\n%q====\nexpected(%d)\n====%q", test.name, len(got), got, len(test.output), test.output)
- }
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ g := Generator{
+ trimPrefix: test.trimPrefix,
+ lineComment: test.lineComment,
+ logf: t.Logf,
+ }
+ input := "package test\n" + test.input
+ file := test.name + ".go"
+ absFile := filepath.Join(dir, file)
+ err := os.WriteFile(absFile, []byte(input), 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ g.parsePackage([]string{absFile}, nil)
+ // Extract the name and type of the constant from the first line.
+ tokens := strings.SplitN(test.input, " ", 3)
+ if len(tokens) != 3 {
+ t.Fatalf("%s: need type declaration on first line", test.name)
+ }
+ g.generate(tokens[1])
+ got := string(g.format())
+ if got != test.output {
+ t.Errorf("%s: got(%d)\n====\n%q====\nexpected(%d)\n====%q", test.name, len(got), got, len(test.output), test.output)
+ }
+ })
}
}
diff --git a/cmd/stringer/stringer.go b/cmd/stringer/stringer.go
index 998d1a51bfd..2b19c93e8ea 100644
--- a/cmd/stringer/stringer.go
+++ b/cmd/stringer/stringer.go
@@ -188,6 +188,8 @@ type Generator struct {
trimPrefix string
lineComment bool
+
+ logf func(format string, args ...interface{}) // test logging hook; nil when not testing
}
func (g *Generator) Printf(format string, args ...interface{}) {
@@ -221,13 +223,14 @@ func (g *Generator) parsePackage(patterns []string, tags []string) {
// in a separate pass? For later.
Tests: false,
BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))},
+ Logf: g.logf,
}
pkgs, err := packages.Load(cfg, patterns...)
if err != nil {
log.Fatal(err)
}
if len(pkgs) != 1 {
- log.Fatalf("error: %d packages found", len(pkgs))
+ log.Fatalf("error: %d packages matching %v", len(pkgs), strings.Join(patterns, " "))
}
g.addPackage(pkgs[0])
}
diff --git a/cmd/toolstash/main.go b/cmd/toolstash/main.go
index ddb1905ae4d..7f38524dfb1 100644
--- a/cmd/toolstash/main.go
+++ b/cmd/toolstash/main.go
@@ -126,15 +126,15 @@ import (
"bufio"
"flag"
"fmt"
- exec "golang.org/x/sys/execabs"
"io"
- "io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"time"
+
+ exec "golang.org/x/sys/execabs"
)
var usageMessage = `usage: toolstash [-n] [-v] [-cmp] command line
@@ -553,13 +553,17 @@ func save() {
}
toolDir := filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
- files, err := ioutil.ReadDir(toolDir)
+ files, err := os.ReadDir(toolDir)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
- if shouldSave(file.Name()) && file.Mode().IsRegular() {
+ info, err := file.Info()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if shouldSave(file.Name()) && info.Mode().IsRegular() {
cp(filepath.Join(toolDir, file.Name()), filepath.Join(stashDir, file.Name()))
}
}
@@ -578,13 +582,17 @@ func save() {
}
func restore() {
- files, err := ioutil.ReadDir(stashDir)
+ files, err := os.ReadDir(stashDir)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
- if shouldSave(file.Name()) && file.Mode().IsRegular() {
+ info, err := file.Info()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if shouldSave(file.Name()) && info.Mode().IsRegular() {
targ := toolDir
if isBinTool(file.Name()) {
targ = binDir
@@ -626,11 +634,11 @@ func cp(src, dst string) {
if *verbose {
fmt.Printf("cp %s %s\n", src, dst)
}
- data, err := ioutil.ReadFile(src)
+ data, err := os.ReadFile(src)
if err != nil {
log.Fatal(err)
}
- if err := ioutil.WriteFile(dst, data, 0777); err != nil {
+ if err := os.WriteFile(dst, data, 0777); err != nil {
log.Fatal(err)
}
}
diff --git a/copyright/copyright.go b/copyright/copyright.go
index db63c59922e..c084bd0cda8 100644
--- a/copyright/copyright.go
+++ b/copyright/copyright.go
@@ -13,7 +13,7 @@ import (
"go/parser"
"go/token"
"io/fs"
- "io/ioutil"
+ "os"
"path/filepath"
"regexp"
"strings"
@@ -67,7 +67,7 @@ func checkFile(toolsDir, filename string) (bool, error) {
if strings.HasSuffix(normalized, "cmd/goyacc/yacc.go") {
return false, nil
}
- content, err := ioutil.ReadFile(filename)
+ content, err := os.ReadFile(filename)
if err != nil {
return false, err
}
diff --git a/go.mod b/go.mod
index 24cca8bec0f..9688b9dae25 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,12 @@
module golang.org/x/tools
-go 1.18 // tagx:compat 1.16
+go 1.18
require (
github.com/yuin/goldmark v1.4.13
- golang.org/x/mod v0.12.0
- golang.org/x/net v0.15.0
- golang.org/x/sys v0.12.0
+ golang.org/x/mod v0.13.0
+ golang.org/x/net v0.16.0
+ golang.org/x/sys v0.13.0
)
-require golang.org/x/sync v0.3.0
+require golang.org/x/sync v0.4.0
diff --git a/go.sum b/go.sum
index 2c884cc6e39..78a350fe890 100644
--- a/go.sum
+++ b/go.sum
@@ -1,46 +1,10 @@
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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
+golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
+golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go
index 63ca6e9eb2e..0c066baa33d 100644
--- a/go/analysis/analysistest/analysistest.go
+++ b/go/analysis/analysistest/analysistest.go
@@ -11,7 +11,6 @@ import (
"go/format"
"go/token"
"go/types"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -35,7 +34,7 @@ import (
// maps file names to contents). On success it returns the name of the
// directory and a cleanup function to delete it.
func WriteFiles(filemap map[string]string) (dir string, cleanup func(), err error) {
- gopath, err := ioutil.TempDir("", "analysistest")
+ gopath, err := os.MkdirTemp("", "analysistest")
if err != nil {
return "", nil, err
}
@@ -44,7 +43,7 @@ func WriteFiles(filemap map[string]string) (dir string, cleanup func(), err erro
for name, content := range filemap {
filename := filepath.Join(gopath, "src", name)
os.MkdirAll(filepath.Dir(filename), 0777) // ignore error
- if err := ioutil.WriteFile(filename, []byte(content), 0666); err != nil {
+ if err := os.WriteFile(filename, []byte(content), 0666); err != nil {
cleanup()
return "", nil, err
}
@@ -212,24 +211,14 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
for _, vf := range ar.Files {
if vf.Name == sf {
found = true
- out, err := diff.ApplyBytes(orig, edits)
- if err != nil {
- t.Errorf("%s: error applying fixes: %v (see possible explanations at RunWithSuggestedFixes)", file.Name(), err)
- continue
- }
// the file may contain multiple trailing
// newlines if the user places empty lines
// between files in the archive. normalize
// this to a single newline.
- want := string(bytes.TrimRight(vf.Data, "\n")) + "\n"
- formatted, err := format.Source(out)
- if err != nil {
- t.Errorf("%s: error formatting edited source: %v\n%s", file.Name(), err, out)
- continue
- }
- if got := string(formatted); got != want {
- unified := diff.Unified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, got)
- t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), unified)
+ golden := append(bytes.TrimRight(vf.Data, "\n"), '\n')
+
+ if err := applyDiffsAndCompare(orig, golden, edits, file.Name()); err != nil {
+ t.Errorf("%s", err)
}
break
}
@@ -246,21 +235,8 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
catchallEdits = append(catchallEdits, edits...)
}
- out, err := diff.ApplyBytes(orig, catchallEdits)
- if err != nil {
- t.Errorf("%s: error applying fixes: %v (see possible explanations at RunWithSuggestedFixes)", file.Name(), err)
- continue
- }
- want := string(ar.Comment)
-
- formatted, err := format.Source(out)
- if err != nil {
- t.Errorf("%s: error formatting resulting source: %v\n%s", file.Name(), err, out)
- continue
- }
- if got := string(formatted); got != want {
- unified := diff.Unified(file.Name()+".golden", "actual", want, got)
- t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), unified)
+ if err := applyDiffsAndCompare(orig, ar.Comment, catchallEdits, file.Name()); err != nil {
+ t.Errorf("%s", err)
}
}
}
@@ -268,6 +244,30 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
return r
}
+// 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)
+ if err != nil {
+ return fmt.Errorf("%s: error applying fixes: %v (see possible explanations at RunWithSuggestedFixes)", fileName, err)
+ }
+ wantRaw, err := format.Source(golden)
+ if err != nil {
+ return fmt.Errorf("%s.golden: error formatting golden file: %v\n%s", fileName, err, out)
+ }
+ want := string(wantRaw)
+
+ formatted, err := format.Source(out)
+ if err != nil {
+ return fmt.Errorf("%s: error formatting resulting source: %v\n%s", fileName, err, out)
+ }
+ 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)
+ }
+ return nil
+}
+
// Run applies an analysis to the packages denoted by the "go list" patterns.
//
// It loads the packages from the specified
@@ -451,7 +451,7 @@ func check(t Testing, gopath string, pass *analysis.Pass, diagnostics []analysis
// Extract 'want' comments from non-Go files.
// TODO(adonovan): we may need to handle //line directives.
for _, filename := range pass.OtherFiles {
- data, err := ioutil.ReadFile(filename)
+ data, err := os.ReadFile(filename)
if err != nil {
t.Errorf("can't read '// want' comments from %s: %v", filename, err)
continue
diff --git a/go/analysis/analysistest/analysistest_test.go b/go/analysis/analysistest/analysistest_test.go
index 8c7ff7363c2..0b5f5ed524c 100644
--- a/go/analysis/analysistest/analysistest_test.go
+++ b/go/analysis/analysistest/analysistest_test.go
@@ -70,6 +70,8 @@ func main() {
// OK (multiple expectations on same line)
println(); println() // want "call of println(...)" "call of println(...)"
+
+ // A Line that is not formatted correctly in the golden file.
}
// OK (facts and diagnostics on same line)
@@ -109,6 +111,8 @@ func main() {
// OK (multiple expectations on same line)
println_TEST_()
println_TEST_() // want "call of println(...)" "call of println(...)"
+
+ // A Line that is not formatted correctly in the golden file.
}
// OK (facts and diagnostics on same line)
diff --git a/go/analysis/doc.go b/go/analysis/doc.go
index c5429c9e239..44867d599e4 100644
--- a/go/analysis/doc.go
+++ b/go/analysis/doc.go
@@ -191,7 +191,7 @@ and buildtag, inspect the raw text of Go source files or even non-Go
files such as assembly. To report a diagnostic against a line of a
raw text file, use the following sequence:
- content, err := ioutil.ReadFile(filename)
+ content, err := os.ReadFile(filename)
if err != nil { ... }
tf := fset.AddFile(filename, -1, len(content))
tf.SetLinesForContent(content)
diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go
index e127a42b97a..9e3fde72bb6 100644
--- a/go/analysis/internal/analysisflags/flags.go
+++ b/go/analysis/internal/analysisflags/flags.go
@@ -14,7 +14,6 @@ import (
"fmt"
"go/token"
"io"
- "io/ioutil"
"log"
"os"
"strconv"
@@ -331,7 +330,7 @@ func PrintPlain(fset *token.FileSet, diag analysis.Diagnostic) {
if !end.IsValid() {
end = posn
}
- data, _ := ioutil.ReadFile(posn.Filename)
+ data, _ := os.ReadFile(posn.Filename)
lines := strings.Split(string(data), "\n")
for i := posn.Line - Context; i <= end.Line+Context; i++ {
if 1 <= i && i <= len(lines) {
diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go
index 2da46925de4..33ca77a06c9 100644
--- a/go/analysis/internal/checker/checker.go
+++ b/go/analysis/internal/checker/checker.go
@@ -17,7 +17,6 @@ import (
"go/format"
"go/token"
"go/types"
- "io/ioutil"
"log"
"os"
"reflect"
@@ -425,7 +424,7 @@ func applyFixes(roots []*action) error {
// Now we've got a set of valid edits for each file. Apply them.
for path, edits := range editsByPath {
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
return err
}
@@ -440,7 +439,7 @@ func applyFixes(roots []*action) error {
out = formatted
}
- if err := ioutil.WriteFile(path, out, 0644); err != nil {
+ if err := os.WriteFile(path, out, 0644); err != nil {
return err
}
}
@@ -480,7 +479,7 @@ func validateEdits(edits []diff.Edit) ([]diff.Edit, int) {
// diff3Conflict returns an error describing two conflicting sets of
// edits on a file at path.
func diff3Conflict(path string, xlabel, ylabel string, xedits, yedits []diff.Edit) error {
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
return err
}
diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go
index 393e7f1854b..be1f1c03869 100644
--- a/go/analysis/internal/checker/checker_test.go
+++ b/go/analysis/internal/checker/checker_test.go
@@ -7,7 +7,7 @@ package checker_test
import (
"fmt"
"go/ast"
- "io/ioutil"
+ "os"
"path/filepath"
"reflect"
"testing"
@@ -51,7 +51,7 @@ func Foo() {
checker.Fix = true
checker.Run([]string{"file=" + path}, []*analysis.Analyzer{analyzer})
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go
index 3ea92b38cc1..e6ac1c1f008 100644
--- a/go/analysis/internal/checker/fix_test.go
+++ b/go/analysis/internal/checker/fix_test.go
@@ -6,7 +6,6 @@ package checker_test
import (
"flag"
- "io/ioutil"
"os"
"os/exec"
"path"
@@ -151,7 +150,7 @@ func Foo() {
for name, want := range fixed {
path := path.Join(dir, "src", name)
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
t.Errorf("error reading %s: %v", path, err)
}
@@ -224,7 +223,7 @@ func Foo() {
// No files updated
for name, want := range files {
path := path.Join(dir, "src", name)
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
t.Errorf("error reading %s: %v", path, err)
}
@@ -298,7 +297,7 @@ func Foo() {
// No files updated
for name, want := range files {
path := path.Join(dir, "src", name)
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
t.Errorf("error reading %s: %v", path, err)
}
diff --git a/go/analysis/internal/checker/start_test.go b/go/analysis/internal/checker/start_test.go
index ede21159bc8..6b0df3033ed 100644
--- a/go/analysis/internal/checker/start_test.go
+++ b/go/analysis/internal/checker/start_test.go
@@ -6,7 +6,7 @@ package checker_test
import (
"go/ast"
- "io/ioutil"
+ "os"
"path/filepath"
"testing"
@@ -40,7 +40,7 @@ package comment
checker.Fix = true
checker.Run([]string{"file=" + path}, []*analysis.Analyzer{commentAnalyzer})
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
diff --git a/go/analysis/passes/appends/appends.go b/go/analysis/passes/appends/appends.go
new file mode 100644
index 00000000000..f0b90a4920e
--- /dev/null
+++ b/go/analysis/passes/appends/appends.go
@@ -0,0 +1,49 @@
+// 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 appends defines an Analyzer that detects
+// if there is only one variable in append.
+package appends
+
+import (
+ _ "embed"
+ "go/ast"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/inspector"
+)
+
+//go:embed doc.go
+var doc string
+
+var Analyzer = &analysis.Analyzer{
+ Name: "appends",
+ Doc: analysisutil.MustExtractDoc(doc, "appends"),
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appends",
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: run,
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+
+ nodeFilter := []ast.Node{
+ (*ast.CallExpr)(nil),
+ }
+ inspect.Preorder(nodeFilter, func(n ast.Node) {
+ call := n.(*ast.CallExpr)
+ if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
+ if _, ok := pass.TypesInfo.Uses[ident].(*types.Builtin); ok {
+ if len(call.Args) == 1 {
+ pass.ReportRangef(call, "append with no values")
+ }
+ }
+ }
+ })
+
+ return nil, nil
+}
diff --git a/go/analysis/passes/appends/appends_test.go b/go/analysis/passes/appends/appends_test.go
new file mode 100644
index 00000000000..bb95aca605c
--- /dev/null
+++ b/go/analysis/passes/appends/appends_test.go
@@ -0,0 +1,18 @@
+// 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 appends_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+ "golang.org/x/tools/go/analysis/passes/appends"
+)
+
+func Test(t *testing.T) {
+ testdata := analysistest.TestData()
+ tests := []string{"a", "b"}
+ analysistest.Run(t, testdata, appends.Analyzer, tests...)
+}
diff --git a/go/analysis/passes/appends/doc.go b/go/analysis/passes/appends/doc.go
new file mode 100644
index 00000000000..2e6a2e010ba
--- /dev/null
+++ b/go/analysis/passes/appends/doc.go
@@ -0,0 +1,20 @@
+// 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 appends defines an Analyzer that detects
+// if there is only one variable in append.
+//
+// # Analyzer appends
+//
+// appends: check for missing values after append
+//
+// This checker reports calls to append that pass
+// no values to be appended to the slice.
+//
+// s := []string{"a", "b", "c"}
+// _ = append(s)
+//
+// Such calls are always no-ops and often indicate an
+// underlying mistake.
+package appends
diff --git a/go/analysis/passes/appends/testdata/src/a/a.go b/go/analysis/passes/appends/testdata/src/a/a.go
new file mode 100644
index 00000000000..5d61620d4e0
--- /dev/null
+++ b/go/analysis/passes/appends/testdata/src/a/a.go
@@ -0,0 +1,32 @@
+// 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 appends checker.
+
+package a
+
+func badAppendSlice1() {
+ sli := []string{"a", "b", "c"}
+ sli = append(sli) // want "append with no values"
+}
+
+func badAppendSlice2() {
+ _ = append([]string{"a"}) // want "append with no values"
+}
+
+func goodAppendSlice1() {
+ sli := []string{"a", "b", "c"}
+ sli = append(sli, "d")
+}
+
+func goodAppendSlice2() {
+ sli1 := []string{"a", "b", "c"}
+ sli2 := []string{"d", "e", "f"}
+ sli1 = append(sli1, sli2...)
+}
+
+func goodAppendSlice3() {
+ sli := []string{"a", "b", "c"}
+ sli = append(sli, "d", "e", "f")
+}
diff --git a/go/analysis/passes/appends/testdata/src/b/b.go b/go/analysis/passes/appends/testdata/src/b/b.go
new file mode 100644
index 00000000000..87a04c4a7bd
--- /dev/null
+++ b/go/analysis/passes/appends/testdata/src/b/b.go
@@ -0,0 +1,18 @@
+// 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 appends checker.
+
+package b
+
+func append(args ...interface{}) []int {
+ println(args)
+ return []int{0}
+}
+
+func userdefine() {
+ sli := []int{1, 2, 3}
+ sli = append(sli, 4, 5, 6)
+ sli = append(sli)
+}
diff --git a/go/analysis/passes/defers/defer.go b/go/analysis/passes/defers/defers.go
similarity index 87%
rename from go/analysis/passes/defers/defer.go
rename to go/analysis/passes/defers/defers.go
index 19474bcc4e8..ed2a122f2b3 100644
--- a/go/analysis/passes/defers/defer.go
+++ b/go/analysis/passes/defers/defers.go
@@ -19,11 +19,12 @@ import (
//go:embed doc.go
var doc string
-// Analyzer is the defer analyzer.
+// Analyzer is the defers analyzer.
var Analyzer = &analysis.Analyzer{
- Name: "defer",
+ Name: "defers",
Requires: []*analysis.Analyzer{inspect.Analyzer},
- Doc: analysisutil.MustExtractDoc(doc, "defer"),
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers",
+ Doc: analysisutil.MustExtractDoc(doc, "defers"),
Run: run,
}
diff --git a/go/analysis/passes/defers/defer_test.go b/go/analysis/passes/defers/defers_test.go
similarity index 100%
rename from go/analysis/passes/defers/defer_test.go
rename to go/analysis/passes/defers/defers_test.go
diff --git a/go/analysis/passes/defers/doc.go b/go/analysis/passes/defers/doc.go
index 60ad3c2cac9..bdb13516282 100644
--- a/go/analysis/passes/defers/doc.go
+++ b/go/analysis/passes/defers/doc.go
@@ -5,11 +5,11 @@
// Package defers defines an Analyzer that checks for common mistakes in defer
// statements.
//
-// # Analyzer defer
+// # Analyzer defers
//
-// defer: report common mistakes in defer statements
+// defers: report common mistakes in defer statements
//
-// The defer analyzer reports a diagnostic when a defer statement would
+// The defers analyzer reports a diagnostic when a defer statement would
// result in a non-deferred call to time.Since, as experience has shown
// that this is nearly always a mistake.
//
diff --git a/go/analysis/passes/directive/directive.go b/go/analysis/passes/directive/directive.go
index 1146d7be457..2691f189aae 100644
--- a/go/analysis/passes/directive/directive.go
+++ b/go/analysis/passes/directive/directive.go
@@ -124,7 +124,7 @@ func (check *checker) nonGoFile(pos token.Pos, fullText string) {
for text != "" {
offset := len(fullText) - len(text)
var line string
- line, text, _ = stringsCut(text, "\n")
+ line, text, _ = strings.Cut(text, "\n")
if !inStar && strings.HasPrefix(line, "//") {
check.comment(pos+token.Pos(offset), line)
@@ -137,7 +137,7 @@ func (check *checker) nonGoFile(pos token.Pos, fullText string) {
line = strings.TrimSpace(line)
if inStar {
var ok bool
- _, line, ok = stringsCut(line, "*/")
+ _, line, ok = strings.Cut(line, "*/")
if !ok {
break
}
@@ -200,14 +200,6 @@ func (check *checker) comment(pos token.Pos, line string) {
}
}
-// Go 1.18 strings.Cut.
-func stringsCut(s, sep string) (before, after string, found bool) {
- if i := strings.Index(s, sep); i >= 0 {
- return s[:i], s[i+len(sep):], true
- }
- return s, "", false
-}
-
// Go 1.20 strings.CutPrefix.
func stringsCutPrefix(s, prefix string) (after string, found bool) {
if !strings.HasPrefix(s, prefix) {
diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go
index ac37e4784e1..a8d84034df1 100644
--- a/go/analysis/passes/internal/analysisutil/util.go
+++ b/go/analysis/passes/internal/analysisutil/util.go
@@ -12,7 +12,7 @@ import (
"go/printer"
"go/token"
"go/types"
- "io/ioutil"
+ "os"
)
// Format returns a string representation of the expression.
@@ -69,7 +69,7 @@ func Unparen(e ast.Expr) ast.Expr {
// ReadFile reads a file and adds it to the FileSet
// so that we can report errors against it using lineStart.
func ReadFile(fset *token.FileSet, filename string) ([]byte, *token.File, error) {
- content, err := ioutil.ReadFile(filename)
+ content, err := os.ReadFile(filename)
if err != nil {
return nil, nil, err
}
diff --git a/go/analysis/passes/printf/printf_test.go b/go/analysis/passes/printf/printf_test.go
index 142afa14e89..ed857fe801c 100644
--- a/go/analysis/passes/printf/printf_test.go
+++ b/go/analysis/passes/printf/printf_test.go
@@ -9,10 +9,13 @@ import (
"golang.org/x/tools/go/analysis/analysistest"
"golang.org/x/tools/go/analysis/passes/printf"
+ "golang.org/x/tools/internal/testenv"
"golang.org/x/tools/internal/typeparams"
)
func Test(t *testing.T) {
+ testenv.NeedsGo1Point(t, 19) // tests use fmt.Appendf
+
testdata := analysistest.TestData()
printf.Analyzer.Flags.Set("funcs", "Warn,Warnf")
diff --git a/go/analysis/unitchecker/main.go b/go/analysis/unitchecker/main.go
index 6e08ce94a3d..4374e7bf945 100644
--- a/go/analysis/unitchecker/main.go
+++ b/go/analysis/unitchecker/main.go
@@ -19,6 +19,7 @@ package main
import (
"golang.org/x/tools/go/analysis/unitchecker"
+ "golang.org/x/tools/go/analysis/passes/appends"
"golang.org/x/tools/go/analysis/passes/asmdecl"
"golang.org/x/tools/go/analysis/passes/assign"
"golang.org/x/tools/go/analysis/passes/atomic"
@@ -52,6 +53,7 @@ import (
func main() {
unitchecker.Main(
+ appends.Analyzer,
asmdecl.Analyzer,
assign.Analyzer,
atomic.Analyzer,
diff --git a/go/analysis/unitchecker/separate_test.go b/go/analysis/unitchecker/separate_test.go
index 37e74e481ec..cf0143f8203 100644
--- a/go/analysis/unitchecker/separate_test.go
+++ b/go/analysis/unitchecker/separate_test.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.19
+
package unitchecker_test
// This file illustrates separate analysis with an example.
diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go
index 4ff45feb4ce..53c3f4a806c 100644
--- a/go/analysis/unitchecker/unitchecker.go
+++ b/go/analysis/unitchecker/unitchecker.go
@@ -193,13 +193,7 @@ type factImporter = func(pkgPath string) ([]byte, error)
// The defaults honor a Config in a manner compatible with 'go vet'.
var (
makeTypesImporter = func(cfg *Config, fset *token.FileSet) types.Importer {
- return importer.ForCompiler(fset, cfg.Compiler, func(importPath string) (io.ReadCloser, error) {
- // Resolve import path to package path (vendoring, etc)
- path, ok := cfg.ImportMap[importPath]
- if !ok {
- return nil, fmt.Errorf("can't resolve import %q", path)
- }
-
+ compilerImporter := importer.ForCompiler(fset, cfg.Compiler, func(path string) (io.ReadCloser, error) {
// path is a resolved package path, not an import path.
file, ok := cfg.PackageFile[path]
if !ok {
@@ -210,6 +204,13 @@ var (
}
return os.Open(file)
})
+ return importerFunc(func(importPath string) (*types.Package, error) {
+ path, ok := cfg.ImportMap[importPath] // resolve vendoring, etc
+ if !ok {
+ return nil, fmt.Errorf("can't resolve import %q", path)
+ }
+ return compilerImporter.Import(path)
+ })
}
exportTypes = func(*Config, *token.FileSet, *types.Package) error {
@@ -433,3 +434,7 @@ type result struct {
diagnostics []analysis.Diagnostic
err error
}
+
+type importerFunc func(path string) (*types.Package, error)
+
+func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go
index 270a3582ccf..9f41c71f9a3 100644
--- a/go/analysis/unitchecker/unitchecker_test.go
+++ b/go/analysis/unitchecker/unitchecker_test.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.19
+
package unitchecker_test
import (
diff --git a/go/analysis/unitchecker/vet_std_test.go b/go/analysis/unitchecker/vet_std_test.go
index e0fb41c77ed..64d4378fe57 100644
--- a/go/analysis/unitchecker/vet_std_test.go
+++ b/go/analysis/unitchecker/vet_std_test.go
@@ -11,6 +11,7 @@ import (
"strings"
"testing"
+ "golang.org/x/tools/go/analysis/passes/appends"
"golang.org/x/tools/go/analysis/passes/asmdecl"
"golang.org/x/tools/go/analysis/passes/assign"
"golang.org/x/tools/go/analysis/passes/atomic"
@@ -47,6 +48,7 @@ import (
// Keep consistent with the actual vet in GOROOT/src/cmd/vet/main.go.
func vet() {
unitchecker.Main(
+ appends.Analyzer,
asmdecl.Analyzer,
assign.Analyzer,
atomic.Analyzer,
diff --git a/go/buildutil/fakecontext.go b/go/buildutil/fakecontext.go
index 15025f645f9..763d18809b4 100644
--- a/go/buildutil/fakecontext.go
+++ b/go/buildutil/fakecontext.go
@@ -8,7 +8,6 @@ import (
"fmt"
"go/build"
"io"
- "io/ioutil"
"os"
"path"
"path/filepath"
@@ -76,7 +75,7 @@ func FakeContext(pkgs map[string]map[string]string) *build.Context {
if !ok {
return nil, fmt.Errorf("file not found: %s", filename)
}
- return ioutil.NopCloser(strings.NewReader(content)), nil
+ return io.NopCloser(strings.NewReader(content)), nil
}
ctxt.IsAbsPath = func(path string) bool {
path = filepath.ToSlash(path)
diff --git a/go/buildutil/overlay.go b/go/buildutil/overlay.go
index bdbfd931478..7e371658d9e 100644
--- a/go/buildutil/overlay.go
+++ b/go/buildutil/overlay.go
@@ -10,7 +10,6 @@ import (
"fmt"
"go/build"
"io"
- "io/ioutil"
"path/filepath"
"strconv"
"strings"
@@ -33,7 +32,7 @@ func OverlayContext(orig *build.Context, overlay map[string][]byte) *build.Conte
// TODO(dominikh): Implement IsDir, HasSubdir and ReadDir
rc := func(data []byte) (io.ReadCloser, error) {
- return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+ return io.NopCloser(bytes.NewBuffer(data)), nil
}
copy := *orig // make a copy
diff --git a/go/buildutil/overlay_test.go b/go/buildutil/overlay_test.go
index 4ee8817f422..267db3f7d63 100644
--- a/go/buildutil/overlay_test.go
+++ b/go/buildutil/overlay_test.go
@@ -6,7 +6,7 @@ package buildutil_test
import (
"go/build"
- "io/ioutil"
+ "io"
"reflect"
"strings"
"testing"
@@ -63,7 +63,7 @@ func TestOverlay(t *testing.T) {
if err != nil {
t.Errorf("unexpected error %v", err)
}
- b, err := ioutil.ReadAll(f)
+ b, err := io.ReadAll(f)
if err != nil {
t.Errorf("unexpected error %v", err)
}
diff --git a/go/buildutil/util_test.go b/go/buildutil/util_test.go
index e6761307583..6c507579a38 100644
--- a/go/buildutil/util_test.go
+++ b/go/buildutil/util_test.go
@@ -6,7 +6,6 @@ package buildutil_test
import (
"go/build"
- "io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -53,7 +52,7 @@ func TestContainingPackage(t *testing.T) {
if runtime.GOOS != "windows" && runtime.GOOS != "plan9" {
// Make a symlink to gopath for test
- tmp, err := ioutil.TempDir(os.TempDir(), "go")
+ tmp, err := os.MkdirTemp(os.TempDir(), "go")
if err != nil {
t.Errorf("Unable to create a temporary directory in %s", os.TempDir())
}
diff --git a/go/callgraph/cha/cha_test.go b/go/callgraph/cha/cha_test.go
index a12b3d0a348..0737a981481 100644
--- a/go/callgraph/cha/cha_test.go
+++ b/go/callgraph/cha/cha_test.go
@@ -16,7 +16,7 @@ import (
"go/parser"
"go/token"
"go/types"
- "io/ioutil"
+ "os"
"sort"
"strings"
"testing"
@@ -98,7 +98,7 @@ func TestCHAGenerics(t *testing.T) {
}
func loadProgInfo(filename string, mode ssa.BuilderMode) (*ssa.Program, *ast.File, *ssa.Package, error) {
- content, err := ioutil.ReadFile(filename)
+ content, err := os.ReadFile(filename)
if err != nil {
return nil, nil, nil, fmt.Errorf("couldn't read file '%s': %s", filename, err)
}
diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go
index 001965b6531..36fe93f6056 100644
--- a/go/callgraph/rta/rta.go
+++ b/go/callgraph/rta/rta.go
@@ -45,6 +45,7 @@ import (
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/compat"
)
// A Result holds the results of Rapid Type Analysis, which includes the
@@ -538,7 +539,7 @@ func fingerprint(mset *types.MethodSet) uint64 {
for i := 0; i < mset.Len(); i++ {
method := mset.At(i).Obj()
sig := method.Type().(*types.Signature)
- sum := crc32.ChecksumIEEE(fmt.Appendf(space[:], "%s/%d/%d",
+ sum := crc32.ChecksumIEEE(compat.Appendf(space[:], "%s/%d/%d",
method.Id(),
sig.Params().Len(),
sig.Results().Len()))
diff --git a/go/callgraph/vta/helpers_test.go b/go/callgraph/vta/helpers_test.go
index facf6afa2ba..1a539e69b63 100644
--- a/go/callgraph/vta/helpers_test.go
+++ b/go/callgraph/vta/helpers_test.go
@@ -9,7 +9,7 @@ import (
"fmt"
"go/ast"
"go/parser"
- "io/ioutil"
+ "os"
"sort"
"strings"
"testing"
@@ -38,7 +38,7 @@ func want(f *ast.File) []string {
// `path`, assumed to define package "testdata," and the
// test want result as list of strings.
func testProg(path string, mode ssa.BuilderMode) (*ssa.Program, []string, error) {
- content, err := ioutil.ReadFile(path)
+ content, err := os.ReadFile(path)
if err != nil {
return nil, nil, err
}
diff --git a/go/expect/expect_test.go b/go/expect/expect_test.go
index e9ae40f7e09..da0ae984ecb 100644
--- a/go/expect/expect_test.go
+++ b/go/expect/expect_test.go
@@ -7,7 +7,7 @@ package expect_test
import (
"bytes"
"go/token"
- "io/ioutil"
+ "os"
"testing"
"golang.org/x/tools/go/expect"
@@ -52,7 +52,7 @@ func TestMarker(t *testing.T) {
},
} {
t.Run(tt.filename, func(t *testing.T) {
- content, err := ioutil.ReadFile(tt.filename)
+ content, err := os.ReadFile(tt.filename)
if err != nil {
t.Fatal(err)
}
diff --git a/go/gccgoexportdata/gccgoexportdata.go b/go/gccgoexportdata/gccgoexportdata.go
index 30ed521ea03..5df2edc13cb 100644
--- a/go/gccgoexportdata/gccgoexportdata.go
+++ b/go/gccgoexportdata/gccgoexportdata.go
@@ -20,7 +20,6 @@ import (
"go/token"
"go/types"
"io"
- "io/ioutil"
"strconv"
"strings"
@@ -46,7 +45,7 @@ func CompilerInfo(gccgo string, args ...string) (version, triple string, dirs []
// NewReader returns a reader for the export data section of an object
// (.o) or archive (.a) file read from r.
func NewReader(r io.Reader) (io.Reader, error) {
- data, err := ioutil.ReadAll(r)
+ data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
diff --git a/go/internal/cgo/cgo.go b/go/internal/cgo/cgo.go
index 3fce4800342..38d5c6c7cd3 100644
--- a/go/internal/cgo/cgo.go
+++ b/go/internal/cgo/cgo.go
@@ -57,7 +57,6 @@ import (
"go/build"
"go/parser"
"go/token"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -70,7 +69,7 @@ import (
// ProcessFiles invokes the cgo preprocessor on bp.CgoFiles, parses
// the output and returns the resulting ASTs.
func ProcessFiles(bp *build.Package, fset *token.FileSet, DisplayPath func(path string) string, mode parser.Mode) ([]*ast.File, error) {
- tmpdir, err := ioutil.TempDir("", strings.Replace(bp.ImportPath, "/", "_", -1)+"_C")
+ tmpdir, err := os.MkdirTemp("", strings.Replace(bp.ImportPath, "/", "_", -1)+"_C")
if err != nil {
return nil, err
}
diff --git a/go/internal/gccgoimporter/importer.go b/go/internal/gccgoimporter/importer.go
index 1094af2c568..53f34c2fbf5 100644
--- a/go/internal/gccgoimporter/importer.go
+++ b/go/internal/gccgoimporter/importer.go
@@ -210,7 +210,7 @@ func GetImporter(searchpaths []string, initmap map[*types.Package]InitData) Impo
// Excluded for now: Standard gccgo doesn't support this import format currently.
// case goimporterMagic:
// var data []byte
- // data, err = ioutil.ReadAll(reader)
+ // data, err = io.ReadAll(reader)
// if err != nil {
// return
// }
diff --git a/go/loader/stdlib_test.go b/go/loader/stdlib_test.go
index f3f3e39bf74..83d70dabdca 100644
--- a/go/loader/stdlib_test.go
+++ b/go/loader/stdlib_test.go
@@ -15,7 +15,7 @@ import (
"go/build"
"go/token"
"go/types"
- "io/ioutil"
+ "os"
"path/filepath"
"runtime"
"strings"
@@ -190,7 +190,7 @@ func TestCgoOption(t *testing.T) {
}
// Load the file and check the object is declared at the right place.
- b, err := ioutil.ReadFile(posn.Filename)
+ b, err := os.ReadFile(posn.Filename)
if err != nil {
t.Errorf("can't read %s: %s", posn.Filename, err)
continue
diff --git a/go/packages/doc.go b/go/packages/doc.go
index da4ab89fe63..a7a8f73e3d1 100644
--- a/go/packages/doc.go
+++ b/go/packages/doc.go
@@ -35,7 +35,7 @@ The Package struct provides basic information about the package, including
- Imports, a map from source import strings to the Packages they name;
- Types, the type information for the package's exported symbols;
- Syntax, the parsed syntax trees for the package's source code; and
- - TypeInfo, the result of a complete type-check of the package syntax trees.
+ - TypesInfo, the result of a complete type-check of the package syntax trees.
(See the documentation for type Package for the complete list of fields
and more detailed descriptions.)
diff --git a/go/packages/golist.go b/go/packages/golist.go
index b5de9cf9f21..1f1eade0ac8 100644
--- a/go/packages/golist.go
+++ b/go/packages/golist.go
@@ -9,7 +9,6 @@ import (
"context"
"encoding/json"
"fmt"
- "io/ioutil"
"log"
"os"
"path"
@@ -1109,7 +1108,7 @@ func (state *golistState) writeOverlays() (filename string, cleanup func(), err
if len(state.cfg.Overlay) == 0 {
return "", func() {}, nil
}
- dir, err := ioutil.TempDir("", "gopackages-*")
+ dir, err := os.MkdirTemp("", "gopackages-*")
if err != nil {
return "", nil, err
}
@@ -1128,7 +1127,7 @@ func (state *golistState) writeOverlays() (filename string, cleanup func(), err
// Create a unique filename for the overlaid files, to avoid
// creating nested directories.
noSeparator := strings.Join(strings.Split(filepath.ToSlash(k), "/"), "")
- f, err := ioutil.TempFile(dir, fmt.Sprintf("*-%s", noSeparator))
+ f, err := os.CreateTemp(dir, fmt.Sprintf("*-%s", noSeparator))
if err != nil {
return "", func() {}, err
}
@@ -1146,7 +1145,7 @@ func (state *golistState) writeOverlays() (filename string, cleanup func(), err
}
// Write out the overlay file that contains the filepath mappings.
filename = filepath.Join(dir, "overlay.json")
- if err := ioutil.WriteFile(filename, b, 0665); err != nil {
+ if err := os.WriteFile(filename, b, 0665); err != nil {
return "", func() {}, err
}
return filename, cleanup, nil
diff --git a/go/packages/overlay_test.go b/go/packages/overlay_test.go
index 4318739eb79..5760b7774b3 100644
--- a/go/packages/overlay_test.go
+++ b/go/packages/overlay_test.go
@@ -6,7 +6,6 @@ package packages_test
import (
"fmt"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -499,7 +498,7 @@ func TestAdHocOverlays(t *testing.T) {
// This test doesn't use packagestest because we are testing ad-hoc packages,
// which are outside of $GOPATH and outside of a module.
- tmp, err := ioutil.TempDir("", "testAdHocOverlays")
+ tmp, err := os.MkdirTemp("", "testAdHocOverlays")
if err != nil {
t.Fatal(err)
}
@@ -554,18 +553,18 @@ func TestOverlayModFileChanges(t *testing.T) {
testenv.NeedsTool(t, "go")
// Create two unrelated modules in a temporary directory.
- tmp, err := ioutil.TempDir("", "tmp")
+ tmp, err := os.MkdirTemp("", "tmp")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
// mod1 has a dependency on golang.org/x/xerrors.
- mod1, err := ioutil.TempDir(tmp, "mod1")
+ mod1, err := os.MkdirTemp(tmp, "mod1")
if err != nil {
t.Fatal(err)
}
- if err := ioutil.WriteFile(filepath.Join(mod1, "go.mod"), []byte(`module mod1
+ if err := os.WriteFile(filepath.Join(mod1, "go.mod"), []byte(`module mod1
require (
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
@@ -575,7 +574,7 @@ func TestOverlayModFileChanges(t *testing.T) {
}
// mod2 does not have any dependencies.
- mod2, err := ioutil.TempDir(tmp, "mod2")
+ mod2, err := os.MkdirTemp(tmp, "mod2")
if err != nil {
t.Fatal(err)
}
@@ -584,7 +583,7 @@ func TestOverlayModFileChanges(t *testing.T) {
go 1.11
`
- if err := ioutil.WriteFile(filepath.Join(mod2, "go.mod"), []byte(want), 0775); err != nil {
+ if err := os.WriteFile(filepath.Join(mod2, "go.mod"), []byte(want), 0775); err != nil {
t.Fatal(err)
}
@@ -610,7 +609,7 @@ func main() {}
}
// Check that mod2/go.mod has not been modified.
- got, err := ioutil.ReadFile(filepath.Join(mod2, "go.mod"))
+ got, err := os.ReadFile(filepath.Join(mod2, "go.mod"))
if err != nil {
t.Fatal(err)
}
@@ -1045,7 +1044,7 @@ func TestOverlaysInReplace(t *testing.T) {
// Create module b.com in a temporary directory. Do not add any Go files
// on disk.
- tmpPkgs, err := ioutil.TempDir("", "modules")
+ tmpPkgs, err := os.MkdirTemp("", "modules")
if err != nil {
t.Fatal(err)
}
@@ -1055,7 +1054,7 @@ func TestOverlaysInReplace(t *testing.T) {
if err := os.Mkdir(dirB, 0775); err != nil {
t.Fatal(err)
}
- if err := ioutil.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"), []byte(fmt.Sprintf("module %s.com", dirB)), 0775); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(dirB, "inner"), 0775); err != nil {
@@ -1063,7 +1062,7 @@ func TestOverlaysInReplace(t *testing.T) {
}
// Create a separate module that requires and replaces b.com.
- tmpWorkspace, err := ioutil.TempDir("", "workspace")
+ tmpWorkspace, err := os.MkdirTemp("", "workspace")
if err != nil {
t.Fatal(err)
}
@@ -1078,7 +1077,7 @@ replace (
b.com => %s
)
`, dirB)
- if err := ioutil.WriteFile(filepath.Join(tmpWorkspace, "go.mod"), []byte(goModContent), 0775); err != nil {
+ if err := os.WriteFile(filepath.Join(tmpWorkspace, "go.mod"), []byte(goModContent), 0775); err != nil {
t.Fatal(err)
}
diff --git a/go/packages/packages.go b/go/packages/packages.go
index 124a6fe143b..ece0e7c603e 100644
--- a/go/packages/packages.go
+++ b/go/packages/packages.go
@@ -16,7 +16,6 @@ import (
"go/token"
"go/types"
"io"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -1127,7 +1126,7 @@ func (ld *loader) parseFile(filename string) (*ast.File, error) {
var err error
if src == nil {
ioLimit <- true // wait
- src, err = ioutil.ReadFile(filename)
+ src, err = os.ReadFile(filename)
<-ioLimit // signal
}
if err != nil {
diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go
index a89887f171c..60fdf9fbadd 100644
--- a/go/packages/packages_test.go
+++ b/go/packages/packages_test.go
@@ -15,7 +15,6 @@ import (
"go/parser"
"go/token"
"go/types"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -931,7 +930,7 @@ func TestAdHocPackagesBadImport(t *testing.T) {
// This test doesn't use packagestest because we are testing ad-hoc packages,
// which are outside of $GOPATH and outside of a module.
- tmp, err := ioutil.TempDir("", "a")
+ tmp, err := os.MkdirTemp("", "a")
if err != nil {
t.Fatal(err)
}
@@ -942,7 +941,7 @@ func TestAdHocPackagesBadImport(t *testing.T) {
import _ "badimport"
const A = 1
`)
- if err := ioutil.WriteFile(filename, content, 0775); err != nil {
+ if err := os.WriteFile(filename, content, 0775); err != nil {
t.Fatal(err)
}
@@ -1748,7 +1747,7 @@ func testAdHocContains(t *testing.T, exporter packagestest.Exporter) {
}}})
defer exported.Cleanup()
- tmpfile, err := ioutil.TempFile("", "adhoc*.go")
+ tmpfile, err := os.CreateTemp("", "adhoc*.go")
filename := tmpfile.Name()
if err != nil {
t.Fatal(err)
@@ -1985,11 +1984,11 @@ import "C"`,
}
func buildFakePkgconfig(t *testing.T, env []string) string {
- tmpdir, err := ioutil.TempDir("", "fakepkgconfig")
+ tmpdir, err := os.MkdirTemp("", "fakepkgconfig")
if err != nil {
t.Fatal(err)
}
- err = ioutil.WriteFile(filepath.Join(tmpdir, "pkg-config.go"), []byte(`
+ err = os.WriteFile(filepath.Join(tmpdir, "pkg-config.go"), []byte(`
package main
import "fmt"
@@ -2452,7 +2451,7 @@ func testIssue37098(t *testing.T, exporter packagestest.Exporter) {
if err != nil {
t.Errorf("Failed to parse file '%s' as a Go source: %v", file, err)
- contents, err := ioutil.ReadFile(file)
+ contents, err := os.ReadFile(file)
if err != nil {
t.Fatalf("Failed to read the un-parsable file '%s': %v", file, err)
}
@@ -2633,7 +2632,7 @@ func testExternal_NotHandled(t *testing.T, exporter packagestest.Exporter) {
skipIfShort(t, "builds and links fake driver binaries")
testenv.NeedsGoBuild(t)
- tempdir, err := ioutil.TempDir("", "testexternal")
+ tempdir, err := os.MkdirTemp("", "testexternal")
if err != nil {
t.Fatal(err)
}
@@ -2647,12 +2646,12 @@ func testExternal_NotHandled(t *testing.T, exporter packagestest.Exporter) {
import (
"fmt"
- "io/ioutil"
+ "io"
"os"
)
func main() {
- ioutil.ReadAll(os.Stdin)
+ io.ReadAll(os.Stdin)
fmt.Println("{}")
}
`,
@@ -2660,12 +2659,12 @@ func main() {
import (
"fmt"
- "io/ioutil"
+ "io"
"os"
)
func main() {
- ioutil.ReadAll(os.Stdin)
+ io.ReadAll(os.Stdin)
fmt.Println("{\"NotHandled\": true}")
}
`,
@@ -2755,7 +2754,7 @@ func TestEmptyEnvironment(t *testing.T) {
func TestPackageLoadSingleFile(t *testing.T) {
testenv.NeedsTool(t, "go")
- tmp, err := ioutil.TempDir("", "a")
+ tmp, err := os.MkdirTemp("", "a")
if err != nil {
t.Fatal(err)
}
@@ -2763,7 +2762,7 @@ func TestPackageLoadSingleFile(t *testing.T) {
filename := filepath.Join(tmp, "a.go")
- if err := ioutil.WriteFile(filename, []byte(`package main; func main() { println("hello world") }`), 0775); err != nil {
+ if err := os.WriteFile(filename, []byte(`package main; func main() { println("hello world") }`), 0775); err != nil {
t.Fatal(err)
}
@@ -2899,7 +2898,7 @@ func copyAll(srcPath, dstPath string) error {
if info.IsDir() {
return nil
}
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
return err
}
@@ -2911,7 +2910,7 @@ func copyAll(srcPath, dstPath string) error {
if err := os.MkdirAll(filepath.Dir(dstFilePath), 0755); err != nil {
return err
}
- if err := ioutil.WriteFile(dstFilePath, contents, 0644); err != nil {
+ if err := os.WriteFile(dstFilePath, contents, 0644); err != nil {
return err
}
return nil
diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go
index 92c20a64a8d..00a30f713e2 100644
--- a/go/packages/packagestest/expect.go
+++ b/go/packages/packagestest/expect.go
@@ -7,7 +7,6 @@ package packagestest
import (
"fmt"
"go/token"
- "io/ioutil"
"os"
"path/filepath"
"reflect"
@@ -226,7 +225,7 @@ func goModMarkers(e *Exported, gomod string) ([]*expect.Note, error) {
}
gomod = strings.TrimSuffix(gomod, ".temp")
// If we are in Modules mode, copy the original contents file back into go.mod
- if err := ioutil.WriteFile(gomod, content, 0644); err != nil {
+ if err := os.WriteFile(gomod, content, 0644); err != nil {
return nil, nil
}
return expect.Parse(e.ExpectFileSet, gomod, content)
diff --git a/go/packages/packagestest/export.go b/go/packages/packagestest/export.go
index 16ded99ba6e..3558ccfdd01 100644
--- a/go/packages/packagestest/export.go
+++ b/go/packages/packagestest/export.go
@@ -69,7 +69,6 @@ import (
"fmt"
"go/token"
"io"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -198,7 +197,7 @@ func Export(t testing.TB, exporter Exporter, modules []Module) *Exported {
dirname := strings.Replace(t.Name(), "/", "_", -1)
dirname = strings.Replace(dirname, "#", "_", -1) // duplicate subtests get a #NNN suffix.
- temp, err := ioutil.TempDir("", dirname)
+ temp, err := os.MkdirTemp("", dirname)
if err != nil {
t.Fatal(err)
}
@@ -254,7 +253,7 @@ func Export(t testing.TB, exporter Exporter, modules []Module) *Exported {
t.Fatal(err)
}
case string:
- if err := ioutil.WriteFile(fullpath, []byte(value), 0644); err != nil {
+ if err := os.WriteFile(fullpath, []byte(value), 0644); err != nil {
t.Fatal(err)
}
default:
@@ -278,7 +277,7 @@ func Export(t testing.TB, exporter Exporter, modules []Module) *Exported {
// It is intended for source files that are shell scripts.
func Script(contents string) Writer {
return func(filename string) error {
- return ioutil.WriteFile(filename, []byte(contents), 0755)
+ return os.WriteFile(filename, []byte(contents), 0755)
}
}
@@ -659,7 +658,7 @@ func (e *Exported) FileContents(filename string) ([]byte, error) {
if content, found := e.Config.Overlay[filename]; found {
return content, nil
}
- content, err := ioutil.ReadFile(filename)
+ content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
diff --git a/go/packages/packagestest/export_test.go b/go/packages/packagestest/export_test.go
index 1172f7c150a..eb13f560916 100644
--- a/go/packages/packagestest/export_test.go
+++ b/go/packages/packagestest/export_test.go
@@ -5,7 +5,6 @@
package packagestest_test
import (
- "io/ioutil"
"os"
"path/filepath"
"reflect"
@@ -197,7 +196,7 @@ func TestMustCopyFiles(t *testing.T) {
"nested/b/b.go": "package b",
}
- tmpDir, err := ioutil.TempDir("", t.Name())
+ tmpDir, err := os.MkdirTemp("", t.Name())
if err != nil {
t.Fatalf("failed to create a temporary directory: %v", err)
}
@@ -208,7 +207,7 @@ func TestMustCopyFiles(t *testing.T) {
if err := os.MkdirAll(filepath.Dir(fullpath), 0755); err != nil {
t.Fatal(err)
}
- if err := ioutil.WriteFile(fullpath, []byte(contents), 0644); err != nil {
+ if err := os.WriteFile(fullpath, []byte(contents), 0644); err != nil {
t.Fatal(err)
}
}
diff --git a/go/packages/packagestest/modules.go b/go/packages/packagestest/modules.go
index 69a6c935dc7..1299c6c3c73 100644
--- a/go/packages/packagestest/modules.go
+++ b/go/packages/packagestest/modules.go
@@ -7,7 +7,6 @@ package packagestest
import (
"context"
"fmt"
- "io/ioutil"
"os"
"path"
"path/filepath"
@@ -90,11 +89,11 @@ func (modules) Finalize(exported *Exported) error {
// If the primary module already has a go.mod, write the contents to a temp
// go.mod for now and then we will reset it when we are getting all the markers.
if gomod := exported.written[exported.primary]["go.mod"]; gomod != "" {
- contents, err := ioutil.ReadFile(gomod)
+ contents, err := os.ReadFile(gomod)
if err != nil {
return err
}
- if err := ioutil.WriteFile(gomod+".temp", contents, 0644); err != nil {
+ if err := os.WriteFile(gomod+".temp", contents, 0644); err != nil {
return err
}
}
@@ -115,7 +114,7 @@ func (modules) Finalize(exported *Exported) error {
primaryGomod += fmt.Sprintf("\t%v %v\n", other, version)
}
primaryGomod += ")\n"
- if err := ioutil.WriteFile(filepath.Join(primaryDir, "go.mod"), []byte(primaryGomod), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(primaryDir, "go.mod"), []byte(primaryGomod), 0644); err != nil {
return err
}
@@ -136,7 +135,7 @@ func (modules) Finalize(exported *Exported) error {
if v, ok := versions[module]; ok {
module = v.module
}
- if err := ioutil.WriteFile(modfile, []byte("module "+module+"\n"), 0644); err != nil {
+ if err := os.WriteFile(modfile, []byte("module "+module+"\n"), 0644); err != nil {
return err
}
files["go.mod"] = modfile
@@ -193,7 +192,7 @@ func (modules) Finalize(exported *Exported) error {
func writeModuleFiles(rootDir, module, ver string, filePaths map[string]string) error {
fileData := make(map[string][]byte)
for name, path := range filePaths {
- contents, err := ioutil.ReadFile(path)
+ contents, err := os.ReadFile(path)
if err != nil {
return err
}
diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go
index 06a8ee63878..25b72fc1ec6 100644
--- a/go/ssa/builder_test.go
+++ b/go/ssa/builder_test.go
@@ -713,9 +713,7 @@ var indirect = R[int].M
// TestTypeparamTest builds SSA over compilable examples in $GOROOT/test/typeparam/*.go.
func TestTypeparamTest(t *testing.T) {
- if !typeparams.Enabled {
- return
- }
+ testenv.NeedsGo1Point(t, 19) // fails with infinite recursion at 1.18 -- not investigated
// Tests use a fake goroot to stub out standard libraries with delcarations in
// testdata/src. Decreases runtime from ~80s to ~1s.
diff --git a/go/ssa/interp/interp_go117_test.go b/go/ssa/interp/interp_go117_test.go
deleted file mode 100644
index 58bbaa39c91..00000000000
--- a/go/ssa/interp/interp_go117_test.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2021 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.17
-// +build go1.17
-
-package interp_test
-
-func init() {
- testdataTests = append(testdataTests, "slice2arrayptr.go")
-}
diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go
index b3edc9916e2..9728d6ec523 100644
--- a/go/ssa/interp/interp_test.go
+++ b/go/ssa/interp/interp_test.go
@@ -2,6 +2,10 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// This test fails at Go 1.18 due to infinite recursion in go/types.
+
+//go:build go1.19
+
package interp_test
// This test runs the SSA interpreter over sample Go programs.
@@ -125,6 +129,7 @@ var testdataTests = []string{
"range.go",
"recover.go",
"reflect.go",
+ "slice2arrayptr.go",
"static.go",
"width32.go",
diff --git a/go/ssa/source_test.go b/go/ssa/source_test.go
index 4fba8a5f5d8..9a7b30675b5 100644
--- a/go/ssa/source_test.go
+++ b/go/ssa/source_test.go
@@ -13,7 +13,6 @@ import (
"go/parser"
"go/token"
"go/types"
- "io/ioutil"
"os"
"runtime"
"strings"
@@ -33,7 +32,7 @@ func TestObjValueLookup(t *testing.T) {
}
conf := loader.Config{ParserMode: parser.ParseComments}
- src, err := ioutil.ReadFile("testdata/objlookup.go")
+ src, err := os.ReadFile("testdata/objlookup.go")
if err != nil {
t.Fatal(err)
}
diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go
index d5a164e5eba..79cdc5afc7c 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.19
+
// 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.
@@ -30,6 +32,7 @@ import (
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/typeparams"
)
// TODO(adonovan):
@@ -137,16 +140,59 @@ func handleSelectJSON(w http.ResponseWriter, req *http.Request) {
// Expression type information
if innermostExpr != nil {
if tv, ok := pkg.TypesInfo.Types[innermostExpr]; ok {
- // TODO(adonovan): show tv.mode.
- // e.g. IsVoid IsType IsBuiltin IsValue IsNil Addressable Assignable HasOk
- fmt.Fprintf(out, "%T has type %v", innermostExpr, tv.Type)
+ var modes []string
+ for _, mode := range []struct {
+ name string
+ condition func(types.TypeAndValue) bool
+ }{
+ {"IsVoid", types.TypeAndValue.IsVoid},
+ {"IsType", types.TypeAndValue.IsType},
+ {"IsBuiltin", types.TypeAndValue.IsBuiltin},
+ {"IsValue", types.TypeAndValue.IsValue},
+ {"IsNil", types.TypeAndValue.IsNil},
+ {"Addressable", types.TypeAndValue.Addressable},
+ {"Assignable", types.TypeAndValue.Assignable},
+ {"HasOk", types.TypeAndValue.HasOk},
+ } {
+ if mode.condition(tv) {
+ modes = append(modes, mode.name)
+ }
+ }
+ fmt.Fprintf(out, "%T has type %v, mode %s",
+ innermostExpr, tv.Type, modes)
+ if tu := tv.Type.Underlying(); tu != tv.Type {
+ fmt.Fprintf(out, ", underlying type %v", tu)
+ }
+ if tc := typeparams.CoreType(tv.Type); tc != tv.Type {
+ fmt.Fprintf(out, ", core type %v", tc)
+ }
if tv.Value != nil {
- fmt.Fprintf(out, " and constant value %v", tv.Value)
+ fmt.Fprintf(out, ", and constant value %v", tv.Value)
}
fmt.Fprintf(out, "\n\n")
}
}
+ // selection x.f information (if cursor is over .f)
+ for _, n := range path[:2] {
+ if sel, ok := n.(*ast.SelectorExpr); ok {
+ seln, ok := pkg.TypesInfo.Selections[sel]
+ if ok {
+ fmt.Fprintf(out, "Selection: %s recv=%v obj=%v type=%v indirect=%t index=%d\n\n",
+ strings.Fields("FieldVal MethodVal MethodExpr")[seln.Kind()],
+ seln.Recv(),
+ seln.Obj(),
+ seln.Type(),
+ seln.Indirect(),
+ seln.Index())
+
+ } else {
+ fmt.Fprintf(out, "Selector is qualified identifier.\n\n")
+ }
+ break
+ }
+ }
+
// Object type information.
switch n := path[0].(type) {
case *ast.Ident:
diff --git a/godoc/server.go b/godoc/server.go
index 57576e10282..a6df6d74e68 100644
--- a/godoc/server.go
+++ b/godoc/server.go
@@ -16,7 +16,6 @@ import (
htmlpkg "html"
htmltemplate "html/template"
"io"
- "io/ioutil"
"log"
"net/http"
"os"
@@ -85,7 +84,7 @@ func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode,
if err != nil {
return nil, err
}
- return ioutil.NopCloser(bytes.NewReader(data)), nil
+ return io.NopCloser(bytes.NewReader(data)), nil
}
// Make the syscall/js package always visible by default.
diff --git a/godoc/static/gen.go b/godoc/static/gen.go
index 7991c8208da..9fe0bd56f3c 100644
--- a/godoc/static/gen.go
+++ b/godoc/static/gen.go
@@ -10,7 +10,7 @@ import (
"bytes"
"fmt"
"go/format"
- "io/ioutil"
+ "os"
"unicode"
)
@@ -71,7 +71,7 @@ func Generate() ([]byte, error) {
fmt.Fprintf(buf, "%v\n\n%v\n\npackage static\n\n", license, warning)
fmt.Fprintf(buf, "var Files = map[string]string{\n")
for _, fn := range files {
- b, err := ioutil.ReadFile(fn)
+ b, err := os.ReadFile(fn)
if err != nil {
return b, err
}
diff --git a/godoc/static/gen_test.go b/godoc/static/gen_test.go
index 7f743290319..1f1c62e0e9c 100644
--- a/godoc/static/gen_test.go
+++ b/godoc/static/gen_test.go
@@ -6,7 +6,7 @@ package static
import (
"bytes"
- "io/ioutil"
+ "os"
"runtime"
"strconv"
"testing"
@@ -17,7 +17,7 @@ func TestStaticIsUpToDate(t *testing.T) {
if runtime.GOOS == "android" {
t.Skip("files not available on android")
}
- oldBuf, err := ioutil.ReadFile("static.go")
+ oldBuf, err := os.ReadFile("static.go")
if err != nil {
t.Errorf("error while reading static.go: %v\n", err)
}
diff --git a/godoc/static/makestatic.go b/godoc/static/makestatic.go
index ef7b9042aac..a8a652f8ed5 100644
--- a/godoc/static/makestatic.go
+++ b/godoc/static/makestatic.go
@@ -11,7 +11,6 @@ package main
import (
"fmt"
- "io/ioutil"
"os"
"golang.org/x/tools/godoc/static"
@@ -29,7 +28,7 @@ func makestatic() error {
if err != nil {
return fmt.Errorf("error while generating static.go: %v\n", err)
}
- err = ioutil.WriteFile("static.go", buf, 0666)
+ err = os.WriteFile("static.go", buf, 0666)
if err != nil {
return fmt.Errorf("error while writing static.go: %v\n", err)
}
diff --git a/godoc/vfs/mapfs/mapfs_test.go b/godoc/vfs/mapfs/mapfs_test.go
index 6b7db290ee3..954ef7e151b 100644
--- a/godoc/vfs/mapfs/mapfs_test.go
+++ b/godoc/vfs/mapfs/mapfs_test.go
@@ -5,7 +5,7 @@
package mapfs
import (
- "io/ioutil"
+ "io"
"os"
"reflect"
"testing"
@@ -36,7 +36,7 @@ func TestOpenRoot(t *testing.T) {
t.Errorf("Open(%q) = %v", tt.path, err)
continue
}
- slurp, err := ioutil.ReadAll(rsc)
+ slurp, err := io.ReadAll(rsc)
if err != nil {
t.Error(err)
}
diff --git a/godoc/vfs/vfs.go b/godoc/vfs/vfs.go
index d70526d5ac9..f4ec2aa7a02 100644
--- a/godoc/vfs/vfs.go
+++ b/godoc/vfs/vfs.go
@@ -8,7 +8,6 @@ package vfs // import "golang.org/x/tools/godoc/vfs"
import (
"io"
- "io/ioutil"
"os"
)
@@ -54,5 +53,5 @@ func ReadFile(fs Opener, path string) ([]byte, error) {
return nil, err
}
defer rc.Close()
- return ioutil.ReadAll(rc)
+ return io.ReadAll(rc)
}
diff --git a/godoc/vfs/zipfs/zipfs_test.go b/godoc/vfs/zipfs/zipfs_test.go
index 2c52a60c68c..b6f2431b0b5 100644
--- a/godoc/vfs/zipfs/zipfs_test.go
+++ b/godoc/vfs/zipfs/zipfs_test.go
@@ -8,7 +8,6 @@ import (
"bytes"
"fmt"
"io"
- "io/ioutil"
"os"
"reflect"
"testing"
@@ -174,7 +173,7 @@ func TestZipFSOpenSeek(t *testing.T) {
// test Seek() multiple times
for i := 0; i < 3; i++ {
- all, err := ioutil.ReadAll(f)
+ all, err := io.ReadAll(f)
if err != nil {
t.Error(err)
return
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index 2ff9434d0b6..55c199ce3eb 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -6,6 +6,21 @@ Details about how to enable/disable these analyses can be found
[here](settings.md#analyses).
+## **appends**
+
+check for missing values after append
+
+This checker reports calls to append that pass
+no values to be appended to the slice.
+
+ s := []string{"a", "b", "c"}
+ _ = append(s)
+
+Such calls are always no-ops and often indicate an
+underlying mistake.
+
+**Enabled by default.**
+
## **asmdecl**
report mismatches between assembly files and Go declarations
@@ -108,11 +123,11 @@ errors is discouraged.
**Enabled by default.**
-## **defer**
+## **defers**
report common mistakes in defer statements
-The defer analyzer reports a diagnostic when a defer statement would
+The defers analyzer reports a diagnostic when a defer statement would
result in a non-deferred call to time.Since, as experience has shown
that this is nearly always a mistake.
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index eff548a442c..833dad9a1d0 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -41,6 +41,22 @@ Args:
}
```
+### **update the given telemetry counters.**
+Identifier: `gopls.add_telemetry_counters`
+
+Gopls will prepend "fwd/" to all the counters updated using this command
+to avoid conflicts with other counters gopls collects.
+
+Args:
+
+```
+{
+ // Names and Values must have the same length.
+ "Names": []string,
+ "Values": []int64,
+}
+```
+
### **Apply a fix**
Identifier: `gopls.apply_fix`
@@ -117,7 +133,7 @@ Args:
Result:
```
-map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/govulncheck.Result
+map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result
```
### **Toggle gc_details**
@@ -222,6 +238,12 @@ Result:
}
```
+### **checks for the right conditions, and then prompts**
+Identifier: `gopls.maybe_prompt_for_telemetry`
+
+the user to ask if they want to enable Go telemetry uploading. If the user
+responds 'Yes', the telemetry mode is set to "on".
+
### **fetch memory statistics**
Identifier: `gopls.mem_stats`
@@ -307,7 +329,7 @@ Args:
}
```
-### **Run govulncheck.**
+### **Run vulncheck.**
Identifier: `gopls.run_govulncheck`
Run vulnerability check (`govulncheck`).
diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go
index 51987f6a7b0..34034fb4e58 100644
--- a/gopls/doc/generate.go
+++ b/gopls/doc/generate.go
@@ -18,7 +18,6 @@ import (
"go/token"
"go/types"
"io"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -573,7 +572,7 @@ func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) {
}
func rewriteFile(file string, api *source.APIJSON, write bool, rewrite func([]byte, *source.APIJSON) ([]byte, error)) (bool, error) {
- old, err := ioutil.ReadFile(file)
+ old, err := os.ReadFile(file)
if err != nil {
return false, err
}
@@ -587,7 +586,7 @@ func rewriteFile(file string, api *source.APIJSON, write bool, rewrite func([]by
return bytes.Equal(old, new), nil
}
- if err := ioutil.WriteFile(file, new, 0); err != nil {
+ if err := os.WriteFile(file, new, 0); err != nil {
return false, err
}
diff --git a/gopls/doc/generate_test.go b/gopls/doc/generate_test.go
index 6e1c23b94db..f92ff1fb8e1 100644
--- a/gopls/doc/generate_test.go
+++ b/gopls/doc/generate_test.go
@@ -23,6 +23,6 @@ func TestGenerated(t *testing.T) {
t.Fatal(err)
}
if !ok {
- t.Error("documentation needs updating. Run: cd gopls && go generate ./doc")
+ t.Error("documentation needs updating. Run: cd gopls && go generate")
}
}
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index 781b7124dbe..eca3ee803d2 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -269,6 +269,16 @@ such as "someSlice.sort!".
Default: `true`.
+##### **completeFunctionCalls** *bool*
+
+completeFunctionCalls enables function call completion.
+
+When completing a statement, or when a function return type matches the
+expected of the expression being completed, completion may suggest call
+expressions (i.e. may include parentheses).
+
+Default: `true`.
+
#### Diagnostic
##### **analyses** *map[string]bool*
@@ -529,7 +539,7 @@ Runs `go generate` for a given directory.
Identifier: `regenerate_cgo`
Regenerates cgo definitions.
-### **Run govulncheck.**
+### **Run vulncheck.**
Identifier: `run_govulncheck`
diff --git a/gopls/doc/vim.md b/gopls/doc/vim.md
index 8be533cfc36..ac040fb7eb3 100644
--- a/gopls/doc/vim.md
+++ b/gopls/doc/vim.md
@@ -140,42 +140,60 @@ cd "$dir"
git clone 'https://github.com/neovim/nvim-lspconfig.git' .
```
-### Custom Configuration
+### Configuration
-You can add custom configuration using Lua. Here is an example of enabling the
-`unusedparams` check as well as `staticcheck`:
+nvim-lspconfig aims to provide reasonable defaults, so your setup can be very
+brief.
-```vim
-lua <Imports
+### Imports and Formatting
Use the following configuration to have your imports organized on save using
-the logic of `goimports`. Note: this requires Neovim v0.7.0 or later.
+the logic of `goimports` and your code formatted.
```lua
-vim.api.nvim_create_autocmd('BufWritePre', {
- pattern = '*.go',
+autocmd("BufWritePre", {
+ pattern = "*.go",
callback = function()
- vim.lsp.buf.code_action({ context = { only = { 'source.organizeImports' } }, apply = true })
+ local params = vim.lsp.util.make_range_params()
+ params.context = {only = {"source.organizeImports"}}
+ -- buf_request_sync defaults to a 1000ms timeout. Depending on your
+ -- machine and codebase, you may want longer. Add an additional
+ -- argument after params if you find that you have to write the file
+ -- twice for changes to be saved.
+ -- E.g., vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, 3000)
+ local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params)
+ for cid, res in pairs(result or {}) do
+ for _, r in pairs(res.result or {}) do
+ if r.edit then
+ local enc = (vim.lsp.get_client_by_id(cid) or {}).offset_encoding or "utf-16"
+ vim.lsp.util.apply_workspace_edit(r.edit, enc)
+ end
+ end
+ end
+ vim.lsp.buf.format({async = false})
end
})
```
diff --git a/gopls/go.mod b/gopls/go.mod
index 3198e0f81bb..866fb259581 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -7,15 +7,15 @@ require (
github.com/jba/printsrc v0.2.2
github.com/jba/templatecheck v0.6.0
github.com/sergi/go-diff v1.1.0
- golang.org/x/mod v0.12.0
- golang.org/x/sync v0.3.0
- golang.org/x/sys v0.12.0
- golang.org/x/telemetry v0.0.0-20230822160736-17171dbf1d76
+ golang.org/x/mod v0.13.0
+ golang.org/x/sync v0.4.0
+ golang.org/x/sys v0.13.0
+ golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3
golang.org/x/text v0.13.0
- golang.org/x/tools v0.6.0
- golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815
+ golang.org/x/tools v0.13.1-0.20230920233436-f9b8da7b22be
+ golang.org/x/vuln v1.0.1
gopkg.in/yaml.v3 v3.0.1
- honnef.co/go/tools v0.4.2
+ honnef.co/go/tools v0.4.5
mvdan.cc/gofumpt v0.4.0
mvdan.cc/xurls/v2 v2.4.0
)
@@ -23,8 +23,8 @@ require (
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/google/safehtml v0.1.0 // indirect
- golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect
+
)
replace golang.org/x/tools => ../
diff --git a/gopls/go.sum b/gopls/go.sum
index 78b66349fe3..365fd0c1a4f 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -1,21 +1,11 @@
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
-github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8=
github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
@@ -25,70 +15,45 @@ github.com/jba/templatecheck v0.6.0 h1:SwM8C4hlK/YNLsdcXStfnHWE2HKkuTVwy5FKQHt5r
github.com/jba/templatecheck v0.6.0/go.mod h1:/1k7EajoSErFI9GLHAsiIJEaNLt3ALKNw2TV7z2SYv4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
-golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
-golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
+golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/telemetry v0.0.0-20230822160736-17171dbf1d76 h1:Lv25uIMpljmSMN0+GCC+xgiC/4ikIdKMkQfw/EVq2Nk=
-golang.org/x/telemetry v0.0.0-20230822160736-17171dbf1d76/go.mod h1:kO7uNSGGmqCHII6C0TYfaLwSBIfcyhj53//nu0+Fy4A=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3 h1:vxxQvncMbcRAtqHV5HsHGJkbya+BIOYIY+y6cdPZhzk=
+golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3/go.mod h1:ppZ76JTkRgJC2GQEgtVY3fiuJR+N8FU2MAlp+gfN1E4=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815 h1:A9kONVi4+AnuOr1dopsibH6hLi1Huy54cbeJxnq4vmU=
-golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815/go.mod h1:XJiVExZgoZfrrxoTeVsFYrSSk1snhfpOEC95JL+A4T0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU=
+golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
@@ -99,12 +64,9 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
-honnef.co/go/tools v0.4.2 h1:6qXr+R5w+ktL5UkwEbPp+fEvfyoMPche6GkOpGHZcLc=
-honnef.co/go/tools v0.4.2/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
+honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo=
+honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM=
mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
-mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio=
-mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5/go.mod h1:b8RRCBm0eeiWR8cfN88xeq2G5SG3VKGO+5UPWi5FSOY=
mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc=
mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg=
diff --git a/gopls/integration/govim/artifacts.go b/gopls/integration/govim/artifacts.go
index a069ff185aa..db375a21e41 100644
--- a/gopls/integration/govim/artifacts.go
+++ b/gopls/integration/govim/artifacts.go
@@ -7,7 +7,7 @@ package main
import (
"flag"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"os"
"path"
@@ -56,11 +56,11 @@ func download(artifactURL string) error {
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("got status code %d from GCS", resp.StatusCode)
}
- data, err := ioutil.ReadAll(resp.Body)
+ data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading result: %v", err)
}
- if err := ioutil.WriteFile(name, data, 0644); err != nil {
+ if err := os.WriteFile(name, data, 0644); err != nil {
return fmt.Errorf("writing artifact: %v", err)
}
return nil
diff --git a/gopls/internal/govulncheck/types_118.go b/gopls/internal/govulncheck/types_118.go
deleted file mode 100644
index 7b354d622a8..00000000000
--- a/gopls/internal/govulncheck/types_118.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2022 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.18
-// +build go1.18
-
-// Package govulncheck provides an experimental govulncheck API.
-package govulncheck
-
-import (
- "golang.org/x/vuln/exp/govulncheck"
-)
-
-var (
- // Source reports vulnerabilities that affect the analyzed packages.
- Source = govulncheck.Source
-
- // DefaultCache constructs cache for a vulnerability database client.
- DefaultCache = govulncheck.DefaultCache
-)
-
-type (
- // Config is the configuration for Main.
- Config = govulncheck.Config
-
- // Vuln represents a single OSV entry.
- Vuln = govulncheck.Vuln
-
- // Module represents a specific vulnerability relevant to a
- // single module or package.
- Module = govulncheck.Module
-
- // Package is a Go package with known vulnerable symbols.
- Package = govulncheck.Package
-
- // CallStacks contains a representative call stack for each
- // vulnerable symbol that is called.
- CallStack = govulncheck.CallStack
-
- // StackFrame represents a call stack entry.
- StackFrame = govulncheck.StackFrame
-)
diff --git a/gopls/internal/govulncheck/types_not118.go b/gopls/internal/govulncheck/types_not118.go
deleted file mode 100644
index faf5a7055b5..00000000000
--- a/gopls/internal/govulncheck/types_not118.go
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright 2022 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.18
-// +build !go1.18
-
-package govulncheck
-
-import (
- "go/token"
-
- "golang.org/x/vuln/osv"
-)
-
-// Vuln represents a single OSV entry.
-type Vuln struct {
- // OSV contains all data from the OSV entry for this vulnerability.
- OSV *osv.Entry
-
- // Modules contains all of the modules in the OSV entry where a
- // vulnerable package is imported by the target source code or binary.
- //
- // For example, a module M with two packages M/p1 and M/p2, where only p1
- // is vulnerable, will appear in this list if and only if p1 is imported by
- // the target source code or binary.
- Modules []*Module
-}
-
-func (v *Vuln) IsCalled() bool {
- return false
-}
-
-// Module represents a specific vulnerability relevant to a single module.
-type Module struct {
- // Path is the module path of the module containing the vulnerability.
- //
- // Importable packages in the standard library will have the path "stdlib".
- Path string
-
- // FoundVersion is the module version where the vulnerability was found.
- FoundVersion string
-
- // FixedVersion is the module version where the vulnerability was
- // fixed. If there are multiple fixed versions in the OSV report, this will
- // be the latest fixed version.
- //
- // This is empty if a fix is not available.
- FixedVersion string
-
- // Packages contains all the vulnerable packages in OSV entry that are
- // imported by the target source code or binary.
- //
- // For example, given a module M with two packages M/p1 and M/p2, where
- // both p1 and p2 are vulnerable, p1 and p2 will each only appear in this
- // list they are individually imported by the target source code or binary.
- Packages []*Package
-}
-
-// Package is a Go package with known vulnerable symbols.
-type Package struct {
- // Path is the import path of the package containing the vulnerability.
- Path string
-
- // CallStacks contains a representative call stack for each
- // vulnerable symbol that is called.
- //
- // For vulnerabilities found from binary analysis, only CallStack.Symbol
- // will be provided.
- //
- // For non-affecting vulnerabilities reported from the source mode
- // analysis, this will be empty.
- CallStacks []CallStack
-}
-
-// CallStacks contains a representative call stack for a vulnerable
-// symbol.
-type CallStack struct {
- // Symbol is the name of the detected vulnerable function
- // or method.
- //
- // This follows the naming convention in the OSV report.
- Symbol string
-
- // Summary is a one-line description of the callstack, used by the
- // default govulncheck mode.
- //
- // Example: module3.main calls github.com/shiyanhui/dht.DHT.Run
- Summary string
-
- // Frames contains an entry for each stack in the call stack.
- //
- // Frames are sorted starting from the entry point to the
- // imported vulnerable symbol. The last frame in Frames should match
- // Symbol.
- Frames []*StackFrame
-}
-
-// StackFrame represents a call stack entry.
-type StackFrame struct {
- // PackagePath is the import path.
- PkgPath string
-
- // FuncName is the function name.
- FuncName string
-
- // RecvType is the fully qualified receiver type,
- // if the called symbol is a method.
- //
- // The client can create the final symbol name by
- // prepending RecvType to FuncName.
- RecvType string
-
- // Position describes an arbitrary source position
- // including the file, line, and column location.
- // A Position is valid if the line number is > 0.
- Position token.Position
-}
-
-func (sf *StackFrame) Name() string {
- return ""
-}
-
-func (sf *StackFrame) Pos() string {
- return ""
-}
diff --git a/gopls/internal/govulncheck/vulncache.go b/gopls/internal/govulncheck/vulncache.go
deleted file mode 100644
index a259f027336..00000000000
--- a/gopls/internal/govulncheck/vulncache.go
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright 2022 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.18
-// +build go1.18
-
-package govulncheck
-
-import (
- "sync"
- "time"
-
- vulnc "golang.org/x/vuln/client"
- "golang.org/x/vuln/osv"
-)
-
-// inMemoryCache is an implementation of the [client.Cache] interface
-// that "decorates" another instance of that interface to provide
-// an additional layer of (memory-based) caching.
-type inMemoryCache struct {
- mu sync.Mutex
- underlying vulnc.Cache
- db map[string]*db
-}
-
-var _ vulnc.Cache = &inMemoryCache{}
-
-type db struct {
- retrieved time.Time
- index vulnc.DBIndex
- entry map[string][]*osv.Entry
-}
-
-// NewInMemoryCache returns a new memory-based cache that decorates
-// the provided cache (file-based, perhaps).
-func NewInMemoryCache(underlying vulnc.Cache) *inMemoryCache {
- return &inMemoryCache{
- underlying: underlying,
- db: make(map[string]*db),
- }
-}
-
-func (c *inMemoryCache) lookupDBLocked(dbName string) *db {
- cached := c.db[dbName]
- if cached == nil {
- cached = &db{entry: make(map[string][]*osv.Entry)}
- c.db[dbName] = cached
- }
- return cached
-}
-
-// ReadIndex returns the index for dbName from the cache, or returns zero values
-// if it is not present.
-func (c *inMemoryCache) ReadIndex(dbName string) (vulnc.DBIndex, time.Time, error) {
- c.mu.Lock()
- defer c.mu.Unlock()
- cached := c.lookupDBLocked(dbName)
-
- if cached.retrieved.IsZero() {
- // First time ReadIndex is called.
- index, retrieved, err := c.underlying.ReadIndex(dbName)
- if err != nil {
- return index, retrieved, err
- }
- cached.index, cached.retrieved = index, retrieved
- }
- return cached.index, cached.retrieved, nil
-}
-
-// WriteIndex puts the index and retrieved time into the cache.
-func (c *inMemoryCache) WriteIndex(dbName string, index vulnc.DBIndex, retrieved time.Time) error {
- c.mu.Lock()
- defer c.mu.Unlock()
- cached := c.lookupDBLocked(dbName)
- cached.index, cached.retrieved = index, retrieved
- // TODO(hyangah): shouldn't we invalidate all cached entries?
- return c.underlying.WriteIndex(dbName, index, retrieved)
-}
-
-// ReadEntries returns the vulndb entries for path from the cache.
-func (c *inMemoryCache) ReadEntries(dbName, path string) ([]*osv.Entry, error) {
- c.mu.Lock()
- defer c.mu.Unlock()
- cached := c.lookupDBLocked(dbName)
- entries, ok := cached.entry[path]
- if !ok {
- // cache miss
- entries, err := c.underlying.ReadEntries(dbName, path)
- if err != nil {
- return entries, err
- }
- cached.entry[path] = entries
- }
- return entries, nil
-}
-
-// WriteEntries puts the entries for path into the cache.
-func (c *inMemoryCache) WriteEntries(dbName, path string, entries []*osv.Entry) error {
- c.mu.Lock()
- defer c.mu.Unlock()
- cached := c.lookupDBLocked(dbName)
- cached.entry[path] = entries
- return c.underlying.WriteEntries(dbName, path, entries)
-}
diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go
index a6ad65f6a26..53dc4975a36 100644
--- a/gopls/internal/hooks/diff.go
+++ b/gopls/internal/hooks/diff.go
@@ -7,7 +7,6 @@ package hooks
import (
"encoding/json"
"fmt"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -42,7 +41,7 @@ var (
// save writes a JSON record of statistics about diff requests to a temporary file.
func (s *diffstat) save() {
diffStatsOnce.Do(func() {
- f, err := ioutil.TempFile("", "gopls-diff-stats-*")
+ f, err := os.CreateTemp("", "gopls-diff-stats-*")
if err != nil {
log.Printf("can't create diff stats temp file: %v", err) // e.g. disk full
return
@@ -91,7 +90,7 @@ func disaster(before, after string) string {
// We use NUL as a separator: it should never appear in Go source.
data := before + "\x00" + after
- if err := ioutil.WriteFile(filename, []byte(data), 0600); err != nil {
+ if err := os.WriteFile(filename, []byte(data), 0600); err != nil {
log.Printf("failed to write diff bug report: %v", err)
return ""
}
diff --git a/gopls/internal/hooks/diff_test.go b/gopls/internal/hooks/diff_test.go
index a46bf3b2d28..0a809589892 100644
--- a/gopls/internal/hooks/diff_test.go
+++ b/gopls/internal/hooks/diff_test.go
@@ -5,7 +5,6 @@
package hooks
import (
- "io/ioutil"
"os"
"testing"
@@ -20,7 +19,7 @@ func TestDisaster(t *testing.T) {
a := "This is a string,(\u0995) just for basic\nfunctionality"
b := "This is another string, (\u0996) to see if disaster will store stuff correctly"
fname := disaster(a, b)
- buf, err := ioutil.ReadFile(fname)
+ buf, err := os.ReadFile(fname)
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/hooks/licenses_test.go b/gopls/internal/hooks/licenses_test.go
index 609f05a606c..4f3ed4f9a56 100644
--- a/gopls/internal/hooks/licenses_test.go
+++ b/gopls/internal/hooks/licenses_test.go
@@ -6,7 +6,7 @@ package hooks
import (
"bytes"
- "io/ioutil"
+ "os"
"os/exec"
"runtime"
"testing"
@@ -23,7 +23,7 @@ func TestLicenses(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skip("generating licenses only works on Unixes")
}
- tmp, err := ioutil.TempFile("", "")
+ tmp, err := os.CreateTemp("", "")
if err != nil {
t.Fatal(err)
}
@@ -33,11 +33,11 @@ func TestLicenses(t *testing.T) {
t.Fatalf("generating licenses failed: %q, %v", out, err)
}
- got, err := ioutil.ReadFile(tmp.Name())
+ got, err := os.ReadFile(tmp.Name())
if err != nil {
t.Fatal(err)
}
- want, err := ioutil.ReadFile("licenses.go")
+ want, err := os.ReadFile("licenses.go")
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go
index 5676a7814a2..dfb09951494 100644
--- a/gopls/internal/lsp/cache/analysis.go
+++ b/gopls/internal/lsp/cache/analysis.go
@@ -192,7 +192,7 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit,
toSrc := make(map[*analysis.Analyzer]*source.Analyzer)
var enabled []*analysis.Analyzer // enabled subset + transitive requirements
for _, a := range analyzers {
- if a.IsEnabled(snapshot.view.Options()) {
+ if a.IsEnabled(snapshot.options) {
toSrc[a.Analyzer] = a
enabled = append(enabled, a.Analyzer)
}
@@ -309,7 +309,7 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit,
// Now that we have read all files,
// we no longer need the snapshot.
// (but options are needed for progress reporting)
- options := snapshot.view.Options()
+ options := snapshot.options
snapshot = nil
// Progress reporting. If supported, gopls reports progress on analysis
diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go
index 473d2513b51..b1cdfcef16b 100644
--- a/gopls/internal/lsp/cache/cache.go
+++ b/gopls/internal/lsp/cache/cache.go
@@ -11,7 +11,6 @@ import (
"sync/atomic"
"time"
- "golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/memoize"
@@ -56,17 +55,12 @@ type Cache struct {
// The provided optionsOverrides may be nil.
//
// TODO(rfindley): move this to session.go.
-func NewSession(ctx context.Context, c *Cache, optionsOverrides func(*source.Options)) *Session {
+func NewSession(ctx context.Context, c *Cache) *Session {
index := atomic.AddInt64(&sessionIndex, 1)
- options := source.DefaultOptions().Clone()
- if optionsOverrides != nil {
- optionsOverrides(options)
- }
s := &Session{
id: strconv.FormatInt(index, 10),
cache: c,
gocmdRunner: &gocommand.Runner{},
- options: options,
overlayFS: newOverlayFS(c),
parseCache: newParseCache(1 * time.Minute), // keep recently parsed files for a minute, to optimize typing CPU
}
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index b7267e983ec..e0be99b64f5 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -323,9 +323,6 @@ type (
//
// Both pre and post may be called concurrently.
func (s *snapshot) forEachPackage(ctx context.Context, ids []PackageID, pre preTypeCheck, post postTypeCheck) error {
- s.typeCheckMu.Lock()
- defer s.typeCheckMu.Unlock()
-
ctx, done := event.Start(ctx, "cache.forEachPackage", tag.PackageCount.Of(len(ids)))
defer done()
@@ -1345,8 +1342,8 @@ func (s *snapshot) typeCheckInputs(ctx context.Context, m *source.Metadata) (typ
depsByImpPath: m.DepsByImpPath,
goVersion: goVersion,
- relatedInformation: s.view.Options().RelatedInformationSupported,
- linkTarget: s.view.Options().LinkTarget,
+ relatedInformation: s.options.RelatedInformationSupported,
+ linkTarget: s.options.LinkTarget,
moduleMode: s.view.moduleMode(),
}, nil
}
diff --git a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/lsp/cache/filemap.go
new file mode 100644
index 00000000000..52b7a13ba95
--- /dev/null
+++ b/gopls/internal/lsp/cache/filemap.go
@@ -0,0 +1,151 @@
+// Copyright 2022 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 cache
+
+import (
+ "path/filepath"
+
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/internal/persistent"
+)
+
+// A fileMap maps files in the snapshot, with some additional bookkeeping:
+// It keeps track of overlays as well as directories containing any observed
+// file.
+type fileMap struct {
+ files *persistent.Map[span.URI, source.FileHandle]
+ overlays *persistent.Map[span.URI, *Overlay] // the subset of files that are overlays
+ dirs *persistent.Set[string] // all dirs containing files; if nil, dirs have not been initialized
+}
+
+func newFileMap() *fileMap {
+ return &fileMap{
+ files: new(persistent.Map[span.URI, source.FileHandle]),
+ overlays: new(persistent.Map[span.URI, *Overlay]),
+ dirs: new(persistent.Set[string]),
+ }
+}
+
+// Clone creates a copy of the fileMap, incorporating the changes specified by
+// the changes map.
+func (m *fileMap) Clone(changes map[span.URI]source.FileHandle) *fileMap {
+ m2 := &fileMap{
+ files: m.files.Clone(),
+ overlays: m.overlays.Clone(),
+ }
+ if m.dirs != nil {
+ m2.dirs = m.dirs.Clone()
+ }
+
+ // Handle file changes.
+ //
+ // Note, we can't simply delete the file unconditionally and let it be
+ // re-read by the snapshot, as (1) the snapshot must always observe all
+ // overlays, and (2) deleting a file forces directories to be reevaluated, as
+ // it may be the last file in a directory. We want to avoid that work in the
+ // common case where a file has simply changed.
+ //
+ // For that reason, we also do this in two passes, processing deletions
+ // first, as a set before a deletion would result in pointless work.
+ for uri, fh := range changes {
+ if !fileExists(fh) {
+ m2.Delete(uri)
+ }
+ }
+ for uri, fh := range changes {
+ if fileExists(fh) {
+ m2.Set(uri, fh)
+ }
+ }
+ return m2
+}
+
+func (m *fileMap) Destroy() {
+ m.files.Destroy()
+ m.overlays.Destroy()
+ if m.dirs != nil {
+ m.dirs.Destroy()
+ }
+}
+
+// Get returns the file handle mapped by the given key, or (nil, false) if the
+// key is not present.
+func (m *fileMap) Get(key span.URI) (source.FileHandle, bool) {
+ return m.files.Get(key)
+}
+
+// Range calls f for each (uri, fh) in the map.
+func (m *fileMap) Range(f func(uri span.URI, fh source.FileHandle)) {
+ m.files.Range(f)
+}
+
+// Set stores the given file handle for key, updating overlays and directories
+// accordingly.
+func (m *fileMap) Set(key span.URI, fh source.FileHandle) {
+ m.files.Set(key, fh, nil)
+
+ // update overlays
+ if o, ok := fh.(*Overlay); ok {
+ m.overlays.Set(key, o, nil)
+ } else {
+ // Setting a non-overlay must delete the corresponding overlay, to preserve
+ // the accuracy of the overlay set.
+ m.overlays.Delete(key)
+ }
+
+ // update dirs, if they have been computed
+ if m.dirs != nil {
+ m.addDirs(key)
+ }
+}
+
+// addDirs adds all directories containing u to the dirs set.
+func (m *fileMap) addDirs(u span.URI) {
+ dir := filepath.Dir(u.Filename())
+ for dir != "" && !m.dirs.Contains(dir) {
+ m.dirs.Add(dir)
+ dir = filepath.Dir(dir)
+ }
+}
+
+// Delete removes a file from the map, and updates overlays and dirs
+// accordingly.
+func (m *fileMap) Delete(key span.URI) {
+ m.files.Delete(key)
+ m.overlays.Delete(key)
+
+ // Deleting a file may cause the set of dirs to shrink; therefore we must
+ // re-evaluate the dir set.
+ //
+ // Do this lazily, to avoid work if there are multiple deletions in a row.
+ if m.dirs != nil {
+ m.dirs.Destroy()
+ m.dirs = nil
+ }
+}
+
+// Overlays returns a new unordered array of overlay files.
+func (m *fileMap) Overlays() []*Overlay {
+ var overlays []*Overlay
+ m.overlays.Range(func(_ span.URI, o *Overlay) {
+ overlays = append(overlays, o)
+ })
+ return overlays
+}
+
+// Dirs reports returns the set of dirs observed by the fileMap.
+//
+// This operation mutates the fileMap.
+// The result must not be mutated by the caller.
+func (m *fileMap) Dirs() *persistent.Set[string] {
+ if m.dirs == nil {
+ m.dirs = new(persistent.Set[string])
+ m.files.Range(func(u span.URI, _ source.FileHandle) {
+ m.addDirs(u)
+ })
+ }
+ return m.dirs
+}
diff --git a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/filemap_test.go
new file mode 100644
index 00000000000..3d5bab41c67
--- /dev/null
+++ b/gopls/internal/lsp/cache/filemap_test.go
@@ -0,0 +1,108 @@
+// 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 cache
+
+import (
+ "path/filepath"
+ "sort"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/span"
+)
+
+func TestFileMap(t *testing.T) {
+ const (
+ set = iota
+ del
+ )
+ type op struct {
+ op int // set or remove
+ path string
+ overlay bool
+ }
+ tests := []struct {
+ label string
+ ops []op
+ wantFiles []string
+ wantOverlays []string
+ wantDirs []string
+ }{
+ {"empty", nil, nil, nil, nil},
+ {"singleton", []op{
+ {set, "/a/b", false},
+ }, []string{"/a/b"}, nil, []string{"/", "/a"}},
+ {"overlay", []op{
+ {set, "/a/b", true},
+ }, []string{"/a/b"}, []string{"/a/b"}, []string{"/", "/a"}},
+ {"replace overlay", []op{
+ {set, "/a/b", true},
+ {set, "/a/b", false},
+ }, []string{"/a/b"}, nil, []string{"/", "/a"}},
+ {"multi dir", []op{
+ {set, "/a/b", false},
+ {set, "/c/d", false},
+ }, []string{"/a/b", "/c/d"}, nil, []string{"/", "/a", "/c"}},
+ {"empty dir", []op{
+ {set, "/a/b", false},
+ {set, "/c/d", false},
+ {del, "/a/b", false},
+ }, []string{"/c/d"}, nil, []string{"/", "/c"}},
+ }
+
+ // Normalize paths for windows compatibility.
+ normalize := func(path string) string {
+ return strings.TrimPrefix(filepath.ToSlash(path), "C:") // the span packages adds 'C:'
+ }
+
+ for _, test := range tests {
+ t.Run(test.label, func(t *testing.T) {
+ m := newFileMap()
+ for _, op := range test.ops {
+ uri := span.URIFromPath(filepath.FromSlash(op.path))
+ switch op.op {
+ case set:
+ var fh source.FileHandle
+ if op.overlay {
+ fh = &Overlay{uri: uri}
+ } else {
+ fh = &DiskFile{uri: uri}
+ }
+ m.Set(uri, fh)
+ case del:
+ m.Delete(uri)
+ }
+ }
+
+ var gotFiles []string
+ m.Range(func(uri span.URI, _ source.FileHandle) {
+ gotFiles = append(gotFiles, normalize(uri.Filename()))
+ })
+ sort.Strings(gotFiles)
+ if diff := cmp.Diff(test.wantFiles, gotFiles); diff != "" {
+ t.Errorf("Files mismatch (-want +got):\n%s", diff)
+ }
+
+ var gotOverlays []string
+ for _, o := range m.Overlays() {
+ gotOverlays = append(gotOverlays, normalize(o.URI().Filename()))
+ }
+ if diff := cmp.Diff(test.wantOverlays, gotOverlays); diff != "" {
+ t.Errorf("Overlays mismatch (-want +got):\n%s", diff)
+ }
+
+ var gotDirs []string
+ m.Dirs().Range(func(dir string) {
+ gotDirs = append(gotDirs, normalize(dir))
+ })
+ sort.Strings(gotDirs)
+ if diff := cmp.Diff(test.wantDirs, gotDirs); diff != "" {
+ t.Errorf("Dirs mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/fs_memoized.go
index 37f59e4ef26..bfc71205765 100644
--- a/gopls/internal/lsp/cache/fs_memoized.go
+++ b/gopls/internal/lsp/cache/fs_memoized.go
@@ -46,7 +46,7 @@ func (h *DiskFile) FileIdentity() source.FileIdentity {
}
}
-func (h *DiskFile) Saved() bool { return true }
+func (h *DiskFile) SameContentsOnDisk() bool { return true }
func (h *DiskFile) Version() int32 { return 0 }
func (h *DiskFile) Content() ([]byte, error) { return h.content, h.err }
diff --git a/gopls/internal/lsp/cache/fs_overlay.go b/gopls/internal/lsp/cache/fs_overlay.go
index 157eb8610f8..6764adda063 100644
--- a/gopls/internal/lsp/cache/fs_overlay.go
+++ b/gopls/internal/lsp/cache/fs_overlay.go
@@ -74,5 +74,5 @@ func (o *Overlay) FileIdentity() source.FileIdentity {
func (o *Overlay) Content() ([]byte, error) { return o.content, nil }
func (o *Overlay) Version() int32 { return o.version }
-func (o *Overlay) Saved() bool { return o.saved }
+func (o *Overlay) SameContentsOnDisk() bool { return o.saved }
func (o *Overlay) Kind() source.FileKind { return o.kind }
diff --git a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go
index 55085a2a1e0..028607608cc 100644
--- a/gopls/internal/lsp/cache/imports.go
+++ b/gopls/internal/lsp/cache/imports.go
@@ -54,15 +54,13 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot
// view.goEnv is immutable -- changes make a new view. Options can change.
// We can't compare build flags directly because we may add -modfile.
- snapshot.view.optionsMu.Lock()
- localPrefix := snapshot.view.options.Local
- currentBuildFlags := snapshot.view.options.BuildFlags
- currentDirectoryFilters := snapshot.view.options.DirectoryFilters
+ localPrefix := snapshot.options.Local
+ currentBuildFlags := snapshot.options.BuildFlags
+ currentDirectoryFilters := snapshot.options.DirectoryFilters
changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) ||
- snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) ||
+ snapshot.options.VerboseOutput != (s.processEnv.Logf != nil) ||
modFileHash != s.cachedModFileHash ||
- !reflect.DeepEqual(snapshot.view.options.DirectoryFilters, s.cachedDirectoryFilters)
- snapshot.view.optionsMu.Unlock()
+ !reflect.DeepEqual(snapshot.options.DirectoryFilters, s.cachedDirectoryFilters)
// If anything relevant to imports has changed, clear caches and
// update the processEnv. Clearing caches blocks on any background
@@ -120,7 +118,7 @@ func populateProcessEnvFromSnapshot(ctx context.Context, pe *imports.ProcessEnv,
ctx, done := event.Start(ctx, "cache.populateProcessEnvFromSnapshot")
defer done()
- if snapshot.view.Options().VerboseOutput {
+ if snapshot.options.VerboseOutput {
pe.Logf = func(format string, args ...interface{}) {
event.Log(ctx, fmt.Sprintf(format, args...))
}
@@ -135,7 +133,7 @@ func populateProcessEnvFromSnapshot(ctx context.Context, pe *imports.ProcessEnv,
// and has led to memory leaks in the past, when the snapshot was
// unintentionally held past its lifetime.
_, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{
- WorkingDir: snapshot.view.workingDir().Filename(),
+ WorkingDir: snapshot.view.goCommandDir.Filename(),
})
if err != nil {
return err
@@ -154,7 +152,7 @@ func populateProcessEnvFromSnapshot(ctx context.Context, pe *imports.ProcessEnv,
// We don't actually use the invocation, so clean it up now.
cleanupInvocation()
// TODO(rfindley): should this simply be inv.WorkingDir?
- pe.WorkingDir = snapshot.view.workingDir().Filename()
+ pe.WorkingDir = snapshot.view.goCommandDir.Filename()
return nil
}
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 03db2a35d0d..331e0e7ebc7 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -75,7 +75,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
if err != nil {
continue
}
- if isStandaloneFile(contents, s.view.Options().StandaloneTags) {
+ if isStandaloneFile(contents, s.options.StandaloneTags) {
standalone = true
query = append(query, uri.Filename())
} else {
@@ -117,7 +117,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
flags |= source.AllowNetwork
}
_, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{
- WorkingDir: s.view.workingDir().Filename(),
+ WorkingDir: s.view.goCommandDir.Filename(),
})
if err != nil {
return err
@@ -160,7 +160,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
}
moduleErrs := make(map[string][]packages.Error) // module path -> errors
- filterFunc := s.view.filterFunc()
+ filterFunc := s.filterFunc()
newMetadata := make(map[PackageID]*source.Metadata)
for _, pkg := range pkgs {
// The Go command returns synthetic list results for module queries that
@@ -178,7 +178,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
continue
}
- if !containsDir || s.view.Options().VerboseOutput {
+ if !containsDir || s.options.VerboseOutput {
event.Log(ctx, eventName, append(
source.SnapshotLabels(s),
tag.Package.Of(pkg.ID),
@@ -359,7 +359,7 @@ func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, fi
for _, fh := range files {
// Place the diagnostics on the package or module declarations.
var rng protocol.Range
- switch s.view.FileKind(fh) {
+ switch s.FileKind(fh) {
case source.Go:
if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil {
// Check that we have a valid `package foo` range to use for positioning the error.
@@ -616,7 +616,7 @@ func containsPackageLocked(s *snapshot, m *source.Metadata) bool {
uris[uri] = struct{}{}
}
- filterFunc := s.view.filterFunc()
+ filterFunc := s.filterFunc()
for uri := range uris {
// Don't use view.contains here. go.work files may include modules
// outside of the workspace folder.
@@ -671,7 +671,7 @@ func containsFileInWorkspaceLocked(s *snapshot, m *source.Metadata) bool {
// The package's files are in this view. It may be a workspace package.
// Vendored packages are not likely to be interesting to the user.
- if !strings.Contains(string(uri), "/vendor/") && s.view.contains(uri) {
+ if !strings.Contains(string(uri), "/vendor/") && s.contains(uri) {
return true
}
}
diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go
deleted file mode 100644
index edb72d5c123..00000000000
--- a/gopls/internal/lsp/cache/maps.go
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright 2022 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 cache
-
-import (
- "golang.org/x/tools/gopls/internal/lsp/source"
- "golang.org/x/tools/gopls/internal/span"
- "golang.org/x/tools/internal/persistent"
-)
-
-type filesMap struct {
- impl *persistent.Map[span.URI, source.FileHandle]
- overlayMap map[span.URI]*Overlay // the subset that are overlays
-}
-
-func newFilesMap() filesMap {
- return filesMap{
- impl: new(persistent.Map[span.URI, source.FileHandle]),
- overlayMap: make(map[span.URI]*Overlay),
- }
-}
-
-func (m filesMap) Clone() filesMap {
- overlays := make(map[span.URI]*Overlay, len(m.overlayMap))
- for k, v := range m.overlayMap {
- overlays[k] = v
- }
- return filesMap{
- impl: m.impl.Clone(),
- overlayMap: overlays,
- }
-}
-
-func (m filesMap) Destroy() {
- m.impl.Destroy()
-}
-
-func (m filesMap) Get(key span.URI) (source.FileHandle, bool) {
- value, ok := m.impl.Get(key)
- if !ok {
- return nil, false
- }
- return value.(source.FileHandle), true
-}
-
-func (m filesMap) Range(do func(key span.URI, value source.FileHandle)) {
- m.impl.Range(do)
-}
-
-func (m filesMap) Set(key span.URI, value source.FileHandle) {
- m.impl.Set(key, value, nil)
-
- if o, ok := value.(*Overlay); ok {
- m.overlayMap[key] = o
- } else {
- // Setting a non-overlay must delete the corresponding overlay, to preserve
- // the accuracy of the overlay set.
- delete(m.overlayMap, key)
- }
-}
-
-func (m *filesMap) Delete(key span.URI) {
- m.impl.Delete(key)
- delete(m.overlayMap, key)
-}
-
-// overlays returns a new unordered array of overlay files.
-func (m filesMap) overlays() []*Overlay {
- // In practice we will always have at least one overlay, so there is no need
- // to optimize for the len=0 case by returning a nil slice.
- overlays := make([]*Overlay, 0, len(m.overlayMap))
- for _, o := range m.overlayMap {
- overlays = append(overlays, o)
- }
- return overlays
-}
diff --git a/gopls/internal/lsp/cache/mod_vuln.go b/gopls/internal/lsp/cache/mod_vuln.go
index dcd58bfa94a..8c635c181bf 100644
--- a/gopls/internal/lsp/cache/mod_vuln.go
+++ b/gopls/internal/lsp/cache/mod_vuln.go
@@ -6,45 +6,29 @@ package cache
import (
"context"
- "os"
- "golang.org/x/tools/gopls/internal/govulncheck"
- "golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/gopls/internal/vulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/scan"
"golang.org/x/tools/internal/memoize"
)
// ModVuln returns import vulnerability analysis for the given go.mod URI.
// Concurrent requests are combined into a single command.
-func (s *snapshot) ModVuln(ctx context.Context, modURI span.URI) (*govulncheck.Result, error) {
+func (s *snapshot) ModVuln(ctx context.Context, modURI span.URI) (*vulncheck.Result, error) {
s.mu.Lock()
entry, hit := s.modVulnHandles.Get(modURI)
s.mu.Unlock()
type modVuln struct {
- result *govulncheck.Result
+ result *vulncheck.Result
err error
}
// Cache miss?
if !hit {
- // If the file handle is an overlay, it may not be written to disk.
- // The go.mod file has to be on disk for vulncheck to work.
- //
- // TODO(hyangah): use overlays for vulncheck.
- fh, err := s.ReadFile(ctx, modURI)
- if err != nil {
- return nil, err
- }
- if _, ok := fh.(*Overlay); ok {
- if info, _ := os.Stat(modURI.Filename()); info == nil {
- return nil, source.ErrNoModOnDisk
- }
- }
-
handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg interface{}) interface{} {
- result, err := modVulnImpl(ctx, arg.(*snapshot), modURI)
+ result, err := scan.VulnerablePackages(ctx, arg.(*snapshot))
return modVuln{result, err}
})
@@ -62,14 +46,3 @@ func (s *snapshot) ModVuln(ctx context.Context, modURI span.URI) (*govulncheck.R
res := v.(modVuln)
return res.result, res.err
}
-
-func modVulnImpl(ctx context.Context, s *snapshot, uri span.URI) (*govulncheck.Result, error) {
- if vulncheck.VulnerablePackages == nil {
- return &govulncheck.Result{}, nil
- }
- fh, err := s.ReadFile(ctx, uri)
- if err != nil {
- return nil, err
- }
- return vulncheck.VulnerablePackages(ctx, s, fh)
-}
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index 1e463fa3f4f..7346e24f82a 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -13,10 +13,10 @@ import (
"sync/atomic"
"golang.org/x/tools/gopls/internal/bug"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/source/typerefs"
"golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
@@ -33,12 +33,9 @@ type Session struct {
cache *Cache // shared cache
gocmdRunner *gocommand.Runner // limits go command concurrency
- optionsMu sync.Mutex
- options *source.Options
-
viewMu sync.Mutex
views []*View
- viewMap map[span.URI]*View // map of URI->best view
+ viewMap map[span.URI]*View // file->best view
parseCache *parseCache
@@ -54,20 +51,6 @@ func (s *Session) GoCommandRunner() *gocommand.Runner {
return s.gocmdRunner
}
-// Options returns a copy of the SessionOptions for this session.
-func (s *Session) Options() *source.Options {
- s.optionsMu.Lock()
- defer s.optionsMu.Unlock()
- return s.options
-}
-
-// SetOptions sets the options of this session to new values.
-func (s *Session) SetOptions(options *source.Options) {
- s.optionsMu.Lock()
- defer s.optionsMu.Unlock()
- s.options = options
-}
-
// Shutdown the session and all views it has created.
func (s *Session) Shutdown(ctx context.Context) {
var views []*View
@@ -134,14 +117,13 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
v := &View{
id: strconv.FormatInt(index, 10),
gocmdRunner: s.gocmdRunner,
+ lastOptions: options,
initialWorkspaceLoad: make(chan struct{}),
initializationSema: make(chan struct{}, 1),
- options: options,
baseCtx: baseCtx,
name: name,
- folder: folder,
moduleUpgrades: map[span.URI]map[string]string{},
- vulns: map[span.URI]*govulncheck.Result{},
+ vulns: map[span.URI]*vulncheck.Result{},
parseCache: s.parseCache,
fs: s.overlayFS,
workspaceInformation: info,
@@ -172,7 +154,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
store: s.cache.store,
packages: new(persistent.Map[PackageID, *packageHandle]),
meta: new(metadataGraph),
- files: newFilesMap(),
+ files: newFileMap(),
activePackages: new(persistent.Map[PackageID, *Package]),
symbolizeHandles: new(persistent.Map[span.URI, *memoize.Promise]),
workspacePackages: make(map[PackageID]PackagePath),
@@ -182,10 +164,10 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
modTidyHandles: new(persistent.Map[span.URI, *memoize.Promise]),
modVulnHandles: new(persistent.Map[span.URI, *memoize.Promise]),
modWhyHandles: new(persistent.Map[span.URI, *memoize.Promise]),
- knownSubdirs: new(persistent.Set[span.URI]),
workspaceModFiles: wsModFiles,
workspaceModFilesErr: wsModFilesErr,
pkgIndex: typerefs.NewPackageIndex(),
+ options: options,
}
// Save one reference in the view.
v.releaseSnapshot = v.snapshot.Acquire()
@@ -274,9 +256,15 @@ func bestViewForURI(uri span.URI, views []*View) *View {
}
// TODO(rfindley): this should consider the workspace layout (i.e.
// go.work).
- if view.contains(uri) {
+ snapshot, release, err := view.getSnapshot()
+ if err != nil {
+ // view is shutdown
+ continue
+ }
+ if snapshot.contains(uri) {
longest = view
}
+ release()
}
if longest != nil {
return longest
@@ -295,6 +283,7 @@ func bestViewForURI(uri span.URI, views []*View) *View {
func (s *Session) RemoveView(view *View) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
+
i := s.dropView(view)
if i == -1 { // error reported elsewhere
return
@@ -304,18 +293,11 @@ func (s *Session) RemoveView(view *View) {
s.views = removeElement(s.views, i)
}
-// updateView recreates the view with the given options.
+// updateViewLocked recreates the view with the given options.
//
// If the resulting error is non-nil, the view may or may not have already been
// dropped from the session.
-func (s *Session) updateView(ctx context.Context, view *View, options *source.Options) (*View, error) {
- s.viewMu.Lock()
- defer s.viewMu.Unlock()
-
- return s.updateViewLocked(ctx, view, options)
-}
-
-func (s *Session) updateViewLocked(ctx context.Context, view *View, options *source.Options) (*View, error) {
+func (s *Session) updateViewLocked(ctx context.Context, view *View, options *source.Options) error {
// Preserve the snapshot ID if we are recreating the view.
view.snapshotMu.Lock()
if view.snapshot == nil {
@@ -327,7 +309,7 @@ func (s *Session) updateViewLocked(ctx context.Context, view *View, options *sou
i := s.dropView(view)
if i == -1 {
- return nil, fmt.Errorf("view %q not found", view.id)
+ return fmt.Errorf("view %q not found", view.id)
}
v, snapshot, release, err := s.createView(ctx, view.name, view.folder, options, seqID)
@@ -336,7 +318,7 @@ func (s *Session) updateViewLocked(ctx context.Context, view *View, options *sou
// this should not happen and is very bad, but we still need to clean
// up the view array if it happens
s.views = removeElement(s.views, i)
- return nil, err
+ return err
}
defer release()
@@ -352,7 +334,7 @@ func (s *Session) updateViewLocked(ctx context.Context, view *View, options *sou
// substitute the new view into the array where the old view was
s.views[i] = v
- return v, nil
+ return nil
}
// removeElement removes the ith element from the slice replacing it with the last element.
@@ -390,19 +372,6 @@ func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModifica
return err
}
-// TODO(rfindley): fileChange seems redundant with source.FileModification.
-// De-dupe into a common representation for changes.
-type fileChange struct {
- content []byte
- exists bool
- fileHandle source.FileHandle
-
- // isUnchanged indicates whether the file action is one that does not
- // change the actual contents of the file. Opens and closes should not
- // be treated like other changes, since the file content doesn't change.
- isUnchanged bool
-}
-
// DidModifyFiles reports a file modification to the session. It returns
// the new snapshots after the modifications have been applied, paired with
// the affected file URIs for those snapshots.
@@ -436,6 +405,8 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
checkViews := false
for _, c := range changes {
+ // TODO(rfindley): go.work files need not be named "go.work" -- we need to
+ // check each view's source.
if isGoMod(c.URI) || isGoWork(c.URI) {
// Change, InvalidateMetadata, and UnknownFileAction actions do not cause
// us to re-evaluate views.
@@ -456,7 +427,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
// synchronously to change processing? Can we assume that the env did not
// change, and derive go.work using a combination of the configured
// GOWORK value and filesystem?
- info, err := s.getWorkspaceInformation(ctx, view.folder, view.Options())
+ info, err := s.getWorkspaceInformation(ctx, view.folder, view.lastOptions)
if err != nil {
// Catastrophic failure, equivalent to a failure of session
// initialization and therefore should almost never happen. One
@@ -470,8 +441,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
}
if info != view.workspaceInformation {
- _, err := s.updateViewLocked(ctx, view, view.Options())
- if err != nil {
+ if err := s.updateViewLocked(ctx, view, view.lastOptions); err != nil {
// More catastrophic failure. The view may or may not still exist.
// The best we can do is log and move on.
event.Error(ctx, "recreating view", err)
@@ -481,7 +451,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
}
// Collect information about views affected by these changes.
- views := make(map[*View]map[span.URI]*fileChange)
+ views := make(map[*View]map[span.URI]source.FileHandle)
affectedViews := map[span.URI][]*View{}
// forceReloadMetadata records whether any change is the magic
// source.InvalidateMetadata action.
@@ -514,37 +484,22 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
}
affectedViews[c.URI] = changedViews
- isUnchanged := c.Action == source.Open || c.Action == source.Close
-
// Apply the changes to all affected views.
+ fh := mustReadFile(ctx, s, c.URI)
for _, view := range changedViews {
// Make sure that the file is added to the view's seenFiles set.
view.markKnown(c.URI)
if _, ok := views[view]; !ok {
- views[view] = make(map[span.URI]*fileChange)
- }
- fh, err := s.ReadFile(ctx, c.URI)
- if err != nil {
- return nil, nil, err
- }
- content, err := fh.Content()
- if err != nil {
- // Ignore the error: the file may be deleted.
- content = nil
- }
- views[view][c.URI] = &fileChange{
- content: content,
- exists: err == nil,
- fileHandle: fh,
- isUnchanged: isUnchanged,
+ views[view] = make(map[span.URI]source.FileHandle)
}
+ views[view][c.URI] = fh
}
}
var releases []func()
viewToSnapshot := map[*View]*snapshot{}
for view, changed := range views {
- snapshot, release := view.invalidateContent(ctx, changed, forceReloadMetadata)
+ snapshot, release := view.invalidateContent(ctx, changed, nil, forceReloadMetadata)
releases = append(releases, release)
viewToSnapshot[view] = snapshot
}
@@ -594,15 +549,23 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes
}
s.viewMu.Unlock()
- knownDirs := knownDirectories(ctx, snapshots)
- defer knownDirs.Destroy()
-
+ // Expand the modification to any file we could care about, which we define
+ // to be any file observed by any of the snapshots.
+ //
+ // There may be other files in the directory, but if we haven't read them yet
+ // we don't need to invalidate them.
var result []source.FileModification
for _, c := range changes {
- if !knownDirs.Contains(c.URI) {
+ expanded := make(map[span.URI]bool)
+ for _, snapshot := range snapshots {
+ for _, uri := range snapshot.filesInDir(c.URI) {
+ expanded[uri] = true
+ }
+ }
+ if len(expanded) == 0 {
result = append(result, c)
} else {
- for uri := range knownFilesInDir(ctx, snapshots, c.URI) {
+ for uri := range expanded {
result = append(result, source.FileModification{
URI: uri,
Action: c.Action,
@@ -616,36 +579,6 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes
return result
}
-// knownDirectories returns all of the directories known to the given
-// snapshots, including workspace directories and their subdirectories.
-// It is responsibility of the caller to destroy the returned set.
-func knownDirectories(ctx context.Context, snapshots []*snapshot) *persistent.Set[span.URI] {
- result := new(persistent.Set[span.URI])
- for _, snapshot := range snapshots {
- dirs := snapshot.dirs(ctx)
- for _, dir := range dirs {
- result.Add(dir)
- }
- knownSubdirs := snapshot.getKnownSubdirs(dirs)
- result.AddAll(knownSubdirs)
- knownSubdirs.Destroy()
- }
- return result
-}
-
-// knownFilesInDir returns the files known to the snapshots in the session.
-// It does not respect symlinks.
-func knownFilesInDir(ctx context.Context, snapshots []*snapshot, dir span.URI) map[span.URI]struct{} {
- files := map[span.URI]struct{}{}
-
- for _, snapshot := range snapshots {
- for _, uri := range snapshot.knownFilesInDir(ctx, dir) {
- files[uri] = struct{}{}
- }
- }
- return files
-}
-
// Precondition: caller holds s.viewMu lock.
// TODO(rfindley): move this to fs_overlay.go.
func (fs *overlayFS) updateOverlays(ctx context.Context, changes []source.FileModification) error {
@@ -715,10 +648,7 @@ func (fs *overlayFS) updateOverlays(ctx context.Context, changes []source.FileMo
}
sameContentOnDisk = true
default:
- fh, err := fs.delegate.ReadFile(ctx, c.URI)
- if err != nil {
- return err
- }
+ fh := mustReadFile(ctx, fs.delegate, c.URI)
_, readErr := fh.Content()
sameContentOnDisk = (readErr == nil && fh.FileIdentity().Hash == hash)
}
@@ -740,6 +670,29 @@ func (fs *overlayFS) updateOverlays(ctx context.Context, changes []source.FileMo
return nil
}
+func mustReadFile(ctx context.Context, fs source.FileSource, uri span.URI) source.FileHandle {
+ ctx = xcontext.Detach(ctx)
+ fh, err := fs.ReadFile(ctx, uri)
+ if err != nil {
+ // ReadFile cannot fail with an uncancellable context.
+ bug.Reportf("reading file failed unexpectedly: %v", err)
+ return brokenFile{uri, err}
+ }
+ return fh
+}
+
+// A brokenFile represents an unexpected failure to read a file.
+type brokenFile struct {
+ uri span.URI
+ err error
+}
+
+func (b brokenFile) URI() span.URI { return b.uri }
+func (b brokenFile) FileIdentity() source.FileIdentity { return source.FileIdentity{URI: b.uri} }
+func (b brokenFile) SameContentsOnDisk() bool { return false }
+func (b brokenFile) Version() int32 { return 0 }
+func (b brokenFile) Content() ([]byte, error) { return nil, b.err }
+
// FileWatchingGlobPatterns returns a new set of glob patterns to
// watch every directory known by the view. For views within a module,
// this is the module root, any directory in the module root, and any
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 94eceed869b..6fc69bdda3d 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -97,7 +97,7 @@ type snapshot struct {
// files maps file URIs to their corresponding FileHandles.
// It may invalidated when a file's content changes.
- files filesMap
+ files *fileMap
// symbolizeHandles maps each file URI to a handle for the future
// result of computing the symbols declared in that file.
@@ -120,6 +120,7 @@ type snapshot struct {
// workspacePackages contains the workspace's packages, which are loaded
// when the view is created. It contains no intermediate test variants.
+ // TODO(rfindley): use a persistent.Map.
workspacePackages map[PackageID]PackagePath
// shouldLoad tracks packages that need to be reloaded, mapping a PackageID
@@ -150,15 +151,6 @@ type snapshot struct {
modWhyHandles *persistent.Map[span.URI, *memoize.Promise] // *memoize.Promise[modWhyResult]
modVulnHandles *persistent.Map[span.URI, *memoize.Promise] // *memoize.Promise[modVulnResult]
- // knownSubdirs is the set of subdirectory URIs in the workspace,
- // used to create glob patterns for file watching.
- knownSubdirs *persistent.Set[span.URI]
- knownSubdirsCache map[string]struct{} // memo of knownSubdirs as a set of filenames
- // unprocessedSubdirChanges are any changes that might affect the set of
- // subdirectories in the workspace. They are not reflected to knownSubdirs
- // during the snapshot cloning step as it can slow down cloning.
- unprocessedSubdirChanges []*fileChange
-
// workspaceModFiles holds the set of mod files active in this snapshot.
//
// This is either empty, a single entry for the workspace go.mod file, or the
@@ -184,17 +176,9 @@ type snapshot struct {
ignoreFilterOnce sync.Once
ignoreFilter *ignoreFilter
- // typeCheckMu guards type checking.
- //
- // Only one type checking pass should be running at a given time, for two reasons:
- // 1. type checking batches are optimized to use all available processors.
- // Generally speaking, running two type checking batches serially is about
- // as fast as running them in parallel.
- // 2. type checking produces cached artifacts that may be re-used by the
- // next type-checking batch: the shared import graph and the set of
- // active packages. Running type checking batches in parallel after an
- // invalidation can cause redundant calculation of this shared state.
- typeCheckMu sync.Mutex
+ // options holds the user configuration at the time this snapshot was
+ // created.
+ options *source.Options
}
var globalSnapshotID uint64
@@ -262,7 +246,6 @@ func (s *snapshot) destroy(destroyedBy string) {
s.packages.Destroy()
s.activePackages.Destroy()
s.files.Destroy()
- s.knownSubdirs.Destroy()
s.symbolizeHandles.Destroy()
s.parseModHandles.Destroy()
s.parseWorkHandles.Destroy()
@@ -284,12 +267,39 @@ func (s *snapshot) View() source.View {
return s.view
}
-func (s *snapshot) FileKind(h source.FileHandle) source.FileKind {
- return s.view.FileKind(h)
+func (s *snapshot) FileKind(fh source.FileHandle) source.FileKind {
+ // The kind of an unsaved buffer comes from the
+ // TextDocumentItem.LanguageID field in the didChange event,
+ // not from the file name. They may differ.
+ if o, ok := fh.(*Overlay); ok {
+ if o.kind != source.UnknownKind {
+ return o.kind
+ }
+ }
+
+ fext := filepath.Ext(fh.URI().Filename())
+ switch fext {
+ case ".go":
+ return source.Go
+ case ".mod":
+ return source.Mod
+ case ".sum":
+ return source.Sum
+ case ".work":
+ return source.Work
+ }
+ exts := s.options.TemplateExtensions
+ for _, ext := range exts {
+ if fext == ext || fext == "."+ext {
+ return source.Tmpl
+ }
+ }
+ // and now what? This should never happen, but it does for cgo before go1.15
+ return source.Go
}
func (s *snapshot) Options() *source.Options {
- return s.view.Options() // temporarily return view options.
+ return s.options // temporarily return view options.
}
func (s *snapshot) BackgroundContext() context.Context {
@@ -315,7 +325,7 @@ func (s *snapshot) Templates() map[span.URI]source.FileHandle {
tmpls := map[span.URI]source.FileHandle{}
s.files.Range(func(k span.URI, fh source.FileHandle) {
- if s.view.FileKind(fh) == source.Tmpl {
+ if s.FileKind(fh) == source.Tmpl {
tmpls[k] = fh
}
})
@@ -363,8 +373,7 @@ func (s *snapshot) workspaceMode() workspaceMode {
return mode
}
mode |= moduleMode
- options := s.view.Options()
- if options.TempModfile {
+ if s.options.TempModfile {
mode |= tempModfile
}
return mode
@@ -377,9 +386,6 @@ func (s *snapshot) workspaceMode() workspaceMode {
// multiple modules in on config, so buildOverlay needs to filter overlays by
// module.
func (s *snapshot) config(ctx context.Context, inv *gocommand.Invocation) *packages.Config {
- s.view.optionsMu.Lock()
- verboseOutput := s.view.options.VerboseOutput
- s.view.optionsMu.Unlock()
cfg := &packages.Config{
Context: ctx,
@@ -402,7 +408,7 @@ func (s *snapshot) config(ctx context.Context, inv *gocommand.Invocation) *packa
panic("go/packages must not be used to parse files")
},
Logf: func(format string, args ...interface{}) {
- if verboseOutput {
+ if s.options.VerboseOutput {
event.Log(ctx, fmt.Sprintf(format, args...))
}
},
@@ -484,18 +490,16 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri
// it used only after call to tempModFile. Clarify that it is only
// non-nil on success.
func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.InvocationFlags, inv *gocommand.Invocation) (tmpURI span.URI, updatedInv *gocommand.Invocation, cleanup func(), err error) {
- s.view.optionsMu.Lock()
- allowModfileModificationOption := s.view.options.AllowModfileModifications
- allowNetworkOption := s.view.options.AllowImplicitNetworkAccess
+ allowModfileModificationOption := s.options.AllowModfileModifications
+ allowNetworkOption := s.options.AllowImplicitNetworkAccess
// TODO(rfindley): this is very hard to follow, and may not even be doing the
// right thing: should inv.Env really trample view.options? Do we ever invoke
// this with a non-empty inv.Env?
//
// We should refactor to make it clearer that the correct env is being used.
- inv.Env = append(append(append(os.Environ(), s.view.options.EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.GO111MODULE())
- inv.BuildFlags = append([]string{}, s.view.options.BuildFlags...)
- s.view.optionsMu.Unlock()
+ inv.Env = append(append(append(os.Environ(), s.options.EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.GO111MODULE())
+ inv.BuildFlags = append([]string{}, s.options.BuildFlags...)
cleanup = func() {} // fallback
// All logic below is for module mode.
@@ -629,7 +633,7 @@ func (s *snapshot) overlays() []*Overlay {
s.mu.Lock()
defer s.mu.Unlock()
- return s.files.overlays()
+ return s.files.Overlays()
}
// Package data kinds, identifying various package data that may be stored in
@@ -899,10 +903,8 @@ func (s *snapshot) resetActivePackagesLocked() {
s.activePackages = new(persistent.Map[PackageID, *Package])
}
-const fileExtensions = "go,mod,sum,work"
-
func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]struct{} {
- extensions := fileExtensions
+ extensions := "go,mod,sum,work"
for _, ext := range s.Options().TemplateExtensions {
extensions += "," + ext
}
@@ -920,19 +922,17 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
}
// Add a pattern for each Go module in the workspace that is not within the view.
- dirs := s.dirs(ctx)
+ dirs := s.workspaceDirs(ctx)
for _, dir := range dirs {
- dirName := dir.Filename()
-
// If the directory is within the view's folder, we're already watching
// it with the first pattern above.
- if source.InDir(s.view.folder.Filename(), dirName) {
+ if source.InDir(s.view.folder.Filename(), dir) {
continue
}
// TODO(rstambler): If microsoft/vscode#3025 is resolved before
// microsoft/vscode#101042, we will need a work-around for Windows
// drive letter casing.
- patterns[fmt.Sprintf("%s/**/*.{%s}", dirName, extensions)] = struct{}{}
+ patterns[fmt.Sprintf("%s/**/*.{%s}", dir, extensions)] = struct{}{}
}
if s.watchSubdirs() {
@@ -943,6 +943,11 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
// directories. There may be thousands of patterns, each a single
// directory.
//
+ // We compute this set by looking at files that we've previously observed.
+ // This may miss changed to directories that we haven't observed, but that
+ // shouldn't matter as there is nothing to invalidate (if a directory falls
+ // in forest, etc).
+ //
// (A previous iteration created a single glob pattern holding a union of
// all the directories, but this was found to cause VS Code to get stuck
// for several minutes after a buffer was saved twice in a workspace that
@@ -956,13 +961,52 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
return patterns
}
+func (s *snapshot) addKnownSubdirs(patterns map[string]unit, wsDirs []string) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ s.files.Dirs().Range(func(dir string) {
+ for _, wsDir := range wsDirs {
+ if source.InDir(wsDir, dir) {
+ patterns[dir] = unit{}
+ }
+ }
+ })
+}
+
+// workspaceDirs returns the workspace directories for the loaded modules.
+//
+// A workspace directory is, roughly speaking, a directory for which we care
+// about file changes.
+func (s *snapshot) workspaceDirs(ctx context.Context) []string {
+ dirSet := make(map[string]unit)
+
+ // Dirs should, at the very least, contain the working directory and folder.
+ dirSet[s.view.goCommandDir.Filename()] = unit{}
+ dirSet[s.view.folder.Filename()] = unit{}
+
+ // Additionally, if e.g. go.work indicates other workspace modules, we should
+ // include their directories too.
+ if s.workspaceModFilesErr == nil {
+ for modFile := range s.workspaceModFiles {
+ dir := filepath.Dir(modFile.Filename())
+ dirSet[dir] = unit{}
+ }
+ }
+ var dirs []string
+ for d := range dirSet {
+ dirs = append(dirs, d)
+ }
+ sort.Strings(dirs)
+ return dirs
+}
+
// watchSubdirs reports whether gopls should request separate file watchers for
// each relevant subdirectory. This is necessary only for clients (namely VS
// Code) that do not send notifications for individual files in a directory
// when the entire directory is deleted.
func (s *snapshot) watchSubdirs() bool {
- opts := s.view.Options()
- switch p := opts.SubdirWatchPatterns; p {
+ switch p := s.options.SubdirWatchPatterns; p {
case source.SubdirWatchPatternsOn:
return true
case source.SubdirWatchPatternsOff:
@@ -975,7 +1019,7 @@ 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 opts.ClientInfo != nil && opts.ClientInfo.Name == "Visual Studio Code" {
+ if s.options.ClientInfo != nil && s.options.ClientInfo.Name == "Visual Studio Code" {
return true
}
return false
@@ -985,127 +1029,19 @@ func (s *snapshot) watchSubdirs() bool {
}
}
-func (s *snapshot) addKnownSubdirs(patterns map[string]struct{}, wsDirs []span.URI) {
- s.mu.Lock()
- defer s.mu.Unlock()
-
- // First, process any pending changes and update the set of known
- // subdirectories.
- // It may change list of known subdirs and therefore invalidate the cache.
- s.applyKnownSubdirsChangesLocked(wsDirs)
-
- // TODO(adonovan): is it still necessary to memoize the Range
- // and URI.Filename operations?
- if s.knownSubdirsCache == nil {
- s.knownSubdirsCache = make(map[string]struct{})
- s.knownSubdirs.Range(func(uri span.URI) {
- s.knownSubdirsCache[uri.Filename()] = struct{}{}
- })
- }
-
- for pattern := range s.knownSubdirsCache {
- patterns[pattern] = struct{}{}
- }
-}
-
-// collectAllKnownSubdirs collects all of the subdirectories within the
-// snapshot's workspace directories. None of the workspace directories are
-// included.
-func (s *snapshot) collectAllKnownSubdirs(ctx context.Context) {
- dirs := s.dirs(ctx)
-
- s.mu.Lock()
- defer s.mu.Unlock()
-
- s.knownSubdirs.Destroy()
- s.knownSubdirs = new(persistent.Set[span.URI])
- s.knownSubdirsCache = nil
- s.files.Range(func(uri span.URI, fh source.FileHandle) {
- s.addKnownSubdirLocked(uri, dirs)
- })
-}
-
-func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) *persistent.Set[span.URI] {
+// filesInDir returns all files observed by the snapshot that are contained in
+// a directory with the provided URI.
+func (s *snapshot) filesInDir(uri span.URI) []span.URI {
s.mu.Lock()
defer s.mu.Unlock()
- // First, process any pending changes and update the set of known
- // subdirectories.
- s.applyKnownSubdirsChangesLocked(wsDirs)
-
- return s.knownSubdirs.Clone()
-}
-
-func (s *snapshot) applyKnownSubdirsChangesLocked(wsDirs []span.URI) {
- for _, c := range s.unprocessedSubdirChanges {
- if c.isUnchanged {
- continue
- }
- if !c.exists {
- s.removeKnownSubdirLocked(c.fileHandle.URI())
- } else {
- s.addKnownSubdirLocked(c.fileHandle.URI(), wsDirs)
- }
- }
- s.unprocessedSubdirChanges = nil
-}
-
-func (s *snapshot) addKnownSubdirLocked(uri span.URI, dirs []span.URI) {
- dir := filepath.Dir(uri.Filename())
- // First check if the directory is already known, because then we can
- // return early.
- if s.knownSubdirs.Contains(span.URIFromPath(dir)) {
- return
- }
- var matched span.URI
- for _, wsDir := range dirs {
- if source.InDir(wsDir.Filename(), dir) {
- matched = wsDir
- break
- }
- }
- // Don't watch any directory outside of the workspace directories.
- if matched == "" {
- return
- }
- for {
- if dir == "" || dir == matched.Filename() {
- break
- }
- uri := span.URIFromPath(dir)
- if s.knownSubdirs.Contains(uri) {
- break
- }
- s.knownSubdirs.Add(uri)
- dir = filepath.Dir(dir)
- s.knownSubdirsCache = nil
- }
-}
-
-func (s *snapshot) removeKnownSubdirLocked(uri span.URI) {
- dir := filepath.Dir(uri.Filename())
- for dir != "" {
- uri := span.URIFromPath(dir)
- if !s.knownSubdirs.Contains(uri) {
- break
- }
- if info, _ := os.Stat(dir); info == nil {
- s.knownSubdirs.Remove(uri)
- s.knownSubdirsCache = nil
- }
- dir = filepath.Dir(dir)
+ dir := uri.Filename()
+ if !s.files.Dirs().Contains(dir) {
+ return nil
}
-}
-
-// knownFilesInDir returns the files known to the given snapshot that are in
-// the given directory. It does not respect symlinks.
-func (s *snapshot) knownFilesInDir(ctx context.Context, dir span.URI) []span.URI {
var files []span.URI
- s.mu.Lock()
- defer s.mu.Unlock()
-
- s.files.Range(func(uri span.URI, fh source.FileHandle) {
- if source.InDir(dir.Filename(), uri.Filename()) {
+ s.files.Range(func(uri span.URI, _ source.FileHandle) {
+ if source.InDir(dir, uri.Filename()) {
files = append(files, uri)
}
})
@@ -1581,7 +1517,7 @@ func (s *snapshot) reloadOrphanedOpenFiles(ctx context.Context) error {
var files []*Overlay
for _, o := range open {
uri := o.URI()
- if s.IsBuiltin(uri) || s.view.FileKind(o) != source.Go {
+ if s.IsBuiltin(uri) || s.FileKind(o) != source.Go {
continue
}
if len(meta.ids[uri]) == 0 {
@@ -1682,7 +1618,7 @@ func (s *snapshot) OrphanedFileDiagnostics(ctx context.Context) (map[span.URI]*s
searchOverlays:
for _, o := range s.overlays() {
uri := o.URI()
- if s.IsBuiltin(uri) || s.view.FileKind(o) != source.Go {
+ if s.IsBuiltin(uri) || s.FileKind(o) != source.Go {
continue
}
md, err := s.MetadataForFile(ctx, uri)
@@ -1881,79 +1817,13 @@ func inVendor(uri span.URI) bool {
return found && strings.Contains(after, "/")
}
-// unappliedChanges is a file source that handles an uncloned snapshot.
-type unappliedChanges struct {
- originalSnapshot *snapshot
- changes map[span.URI]*fileChange
-}
-
-func (ac *unappliedChanges) ReadFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
- if c, ok := ac.changes[uri]; ok {
- return c.fileHandle, nil
- }
- return ac.originalSnapshot.ReadFile(ctx, uri)
-}
-
-func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) (*snapshot, func()) {
+func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source.FileHandle, newOptions *source.Options, forceReloadMetadata bool) (*snapshot, func()) {
ctx, done := event.Start(ctx, "cache.snapshot.clone")
defer done()
- reinit := false
- wsModFiles, wsModFilesErr := s.workspaceModFiles, s.workspaceModFilesErr
-
- if workURI, _ := s.view.GOWORK(); workURI != "" {
- if change, ok := changes[workURI]; ok {
- wsModFiles, wsModFilesErr = computeWorkspaceModFiles(ctx, s.view.gomod, workURI, s.view.effectiveGO111MODULE(), &unappliedChanges{
- originalSnapshot: s,
- changes: changes,
- })
- // TODO(rfindley): don't rely on 'isUnchanged' here. Use a content hash instead.
- reinit = change.fileHandle.Saved() && !change.isUnchanged
- }
- }
-
- // Reinitialize if any workspace mod file has changed on disk.
- for uri, change := range changes {
- if _, ok := wsModFiles[uri]; ok && change.fileHandle.Saved() && !change.isUnchanged {
- reinit = true
- }
- }
-
- // Finally, process sumfile changes that may affect loading.
- for uri, change := range changes {
- if !change.fileHandle.Saved() {
- continue // like with go.mod files, we only reinit when things are saved
- }
- if filepath.Base(uri.Filename()) == "go.work.sum" && s.view.gowork != "" {
- if filepath.Dir(uri.Filename()) == filepath.Dir(s.view.gowork) {
- reinit = true
- }
- }
- if filepath.Base(uri.Filename()) == "go.sum" {
- dir := filepath.Dir(uri.Filename())
- modURI := span.URIFromPath(filepath.Join(dir, "go.mod"))
- if _, active := wsModFiles[modURI]; active {
- reinit = true
- }
- }
- }
-
s.mu.Lock()
defer s.mu.Unlock()
- // Changes to vendor tree may require reinitialization,
- // either because of an initialization error
- // (e.g. "inconsistent vendoring detected"), or because
- // one or more modules may have moved into or out of the
- // vendor tree after 'go mod vendor' or 'rm -fr vendor/'.
- for uri := range changes {
- if inVendor(uri) && s.initializedErr != nil ||
- strings.HasSuffix(string(uri), "/vendor/modules.txt") {
- reinit = true
- break
- }
- }
-
bgCtx, cancel := context.WithCancel(bgCtx)
result := &snapshot{
sequenceID: s.sequenceID + 1,
@@ -1967,26 +1837,24 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
initializedErr: s.initializedErr,
packages: s.packages.Clone(),
activePackages: s.activePackages.Clone(),
- files: s.files.Clone(),
- symbolizeHandles: s.symbolizeHandles.Clone(),
+ files: s.files.Clone(changes),
+ symbolizeHandles: cloneWithout(s.symbolizeHandles, changes),
workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)),
- unloadableFiles: s.unloadableFiles.Clone(), // see the TODO for unloadableFiles below
- parseModHandles: s.parseModHandles.Clone(),
- parseWorkHandles: s.parseWorkHandles.Clone(),
- modTidyHandles: s.modTidyHandles.Clone(),
- modWhyHandles: s.modWhyHandles.Clone(),
- modVulnHandles: s.modVulnHandles.Clone(),
- knownSubdirs: s.knownSubdirs.Clone(),
- workspaceModFiles: wsModFiles,
- workspaceModFilesErr: wsModFilesErr,
+ unloadableFiles: s.unloadableFiles.Clone(), // not cloneWithout: typing in a file doesn't necessarily make it loadable
+ parseModHandles: cloneWithout(s.parseModHandles, changes),
+ parseWorkHandles: cloneWithout(s.parseWorkHandles, changes),
+ modTidyHandles: cloneWithout(s.modTidyHandles, changes),
+ modWhyHandles: cloneWithout(s.modWhyHandles, changes),
+ modVulnHandles: cloneWithout(s.modVulnHandles, changes),
+ workspaceModFiles: s.workspaceModFiles,
+ workspaceModFilesErr: s.workspaceModFilesErr,
importGraph: s.importGraph,
pkgIndex: s.pkgIndex,
+ options: s.options,
}
- // The snapshot should be initialized if either s was uninitialized, or we've
- // detected a change that triggers reinitialization.
- if reinit {
- result.initialized = false
+ if newOptions != nil {
+ result.options = newOptions
}
// Create a lease on the new snapshot.
@@ -1994,19 +1862,87 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
// incref/decref operation that might destroy it prematurely.)
release := result.Acquire()
- // TODO(rfindley): this looks wrong. Should we clear unloadableFiles on
- // changes to environment or workspace layout, or more generally on any
- // metadata change?
+ reinit := false
+
+ // Changes to vendor tree may require reinitialization,
+ // either because of an initialization error
+ // (e.g. "inconsistent vendoring detected"), or because
+ // one or more modules may have moved into or out of the
+ // vendor tree after 'go mod vendor' or 'rm -fr vendor/'.
//
- // Maybe not, as major configuration changes cause a new view.
+ // TODO(rfindley): revisit the location of this check.
+ for uri := range changes {
+ if inVendor(uri) && s.initializedErr != nil ||
+ strings.HasSuffix(string(uri), "/vendor/modules.txt") {
+ reinit = true
+ break
+ }
+ }
- // Add all of the known subdirectories, but don't update them for the
- // changed files. We need to rebuild the workspace module to know the
- // true set of known subdirectories, but we don't want to do that in clone.
- result.knownSubdirs = s.knownSubdirs.Clone()
- result.knownSubdirsCache = s.knownSubdirsCache
- for _, c := range changes {
- result.unprocessedSubdirChanges = append(result.unprocessedSubdirChanges, c)
+ // Collect observed file handles for changed URIs from the old snapshot, if
+ // they exist. Importantly, we don't call ReadFile here: consider the case
+ // where a file is added on disk; we don't want to read the newly added file
+ // into the old snapshot, as that will break our change detection below.
+ oldFiles := make(map[span.URI]source.FileHandle)
+ for uri := range changes {
+ if fh, ok := s.files.Get(uri); ok {
+ oldFiles[uri] = fh
+ }
+ }
+ // changedOnDisk determines if the new file handle may have changed on disk.
+ // It over-approximates, returning true if the new file is saved and either
+ // the old file wasn't saved, or the on-disk contents changed.
+ //
+ // oldFH may be nil.
+ changedOnDisk := func(oldFH, newFH source.FileHandle) bool {
+ if !newFH.SameContentsOnDisk() {
+ return false
+ }
+ if oe, ne := (oldFH != nil && fileExists(oldFH)), fileExists(newFH); !oe || !ne {
+ return oe != ne
+ }
+ return !oldFH.SameContentsOnDisk() || oldFH.FileIdentity() != newFH.FileIdentity()
+ }
+
+ if workURI, _ := s.view.GOWORK(); workURI != "" {
+ if newFH, ok := changes[workURI]; ok {
+ result.workspaceModFiles, result.workspaceModFilesErr = computeWorkspaceModFiles(ctx, s.view.gomod, workURI, s.view.effectiveGO111MODULE(), result)
+ if changedOnDisk(oldFiles[workURI], newFH) {
+ reinit = true
+ }
+ }
+ }
+
+ // Reinitialize if any workspace mod file has changed on disk.
+ for uri, newFH := range changes {
+ if _, ok := result.workspaceModFiles[uri]; ok && changedOnDisk(oldFiles[uri], newFH) {
+ reinit = true
+ }
+ }
+
+ // Finally, process sumfile changes that may affect loading.
+ for uri, newFH := range changes {
+ if !changedOnDisk(oldFiles[uri], newFH) {
+ continue // like with go.mod files, we only reinit when things change on disk
+ }
+ dir, base := filepath.Split(uri.Filename())
+ if base == "go.work.sum" && s.view.gowork != "" {
+ if dir == filepath.Dir(s.view.gowork) {
+ reinit = true
+ }
+ }
+ if base == "go.sum" {
+ modURI := span.URIFromPath(filepath.Join(dir, "go.mod"))
+ if _, active := result.workspaceModFiles[modURI]; active {
+ reinit = true
+ }
+ }
+ }
+
+ // The snapshot should be initialized if either s was uninitialized, or we've
+ // detected a change that triggers reinitialization.
+ if reinit {
+ result.initialized = false
}
// directIDs keeps track of package IDs that have directly changed.
@@ -2016,6 +1952,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
// Invalidate all package metadata if the workspace module has changed.
if reinit {
for k := range s.meta.metadata {
+ // TODO(rfindley): this seems brittle; can we just start over?
directIDs[k] = true
}
}
@@ -2025,22 +1962,14 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
anyFileOpenedOrClosed := false // opened files affect workspace packages
anyFileAdded := false // adding a file can resolve missing dependencies
- for uri, change := range changes {
- // Invalidate go.mod-related handles.
- result.modTidyHandles.Delete(uri)
- result.modWhyHandles.Delete(uri)
- result.modVulnHandles.Delete(uri)
-
- // Invalidate handles for cached symbols.
- result.symbolizeHandles.Delete(uri)
-
+ for uri, newFH := range changes {
// The original FileHandle for this URI is cached on the snapshot.
- originalFH, _ := s.files.Get(uri)
- var originalOpen, newOpen bool
- _, originalOpen = originalFH.(*Overlay)
- _, newOpen = change.fileHandle.(*Overlay)
- anyFileOpenedOrClosed = anyFileOpenedOrClosed || (originalOpen != newOpen)
- anyFileAdded = anyFileAdded || (originalFH == nil && change.fileHandle != nil)
+ oldFH, _ := oldFiles[uri] // may be nil
+ _, oldOpen := oldFH.(*Overlay)
+ _, newOpen := newFH.(*Overlay)
+
+ anyFileOpenedOrClosed = anyFileOpenedOrClosed || (oldOpen != newOpen)
+ anyFileAdded = anyFileAdded || (oldFH == nil || !fileExists(oldFH)) && fileExists(newFH)
// If uri is a Go file, check if it has changed in a way that would
// invalidate metadata. Note that we can't use s.view.FileKind here,
@@ -2048,7 +1977,11 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
// but what the Go command sees.
var invalidateMetadata, pkgFileChanged, importDeleted bool
if strings.HasSuffix(uri.Filename(), ".go") {
- invalidateMetadata, pkgFileChanged, importDeleted = metadataChanges(ctx, s, originalFH, change.fileHandle)
+ invalidateMetadata, pkgFileChanged, importDeleted = metadataChanges(ctx, s, oldFH, newFH)
+ }
+ if invalidateMetadata {
+ // If this is a metadata-affecting change, perhaps a reload will succeed.
+ result.unloadableFiles.Remove(uri)
}
invalidateMetadata = invalidateMetadata || forceReloadMetadata || reinit
@@ -2062,7 +1995,12 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
// Invalidate the previous modTidyHandle if any of the files have been
// saved or if any of the metadata has been invalidated.
- if invalidateMetadata || fileWasSaved(originalFH, change.fileHandle) {
+ //
+ // TODO(rfindley): this seems like too-aggressive invalidation of mod
+ // results. We should instead thread through overlays to the Go command
+ // invocation and only run this if invalidateMetadata (and perhaps then
+ // still do it less frequently).
+ if invalidateMetadata || fileWasSaved(oldFH, newFH) {
// Only invalidate mod tidy results for the most relevant modfile in the
// workspace. This is a potentially lossy optimization for workspaces
// with many modules (such as google-cloud-go, which has 145 modules as
@@ -2089,33 +2027,13 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
result.modTidyHandles.Clear()
}
- // TODO(rfindley): should we apply the above heuristic to mod vuln
- // or mod handles as well?
+ // TODO(rfindley): should we apply the above heuristic to mod vuln or mod
+ // why handles as well?
//
- // TODO(rfindley): no tests fail if I delete the below line.
+ // TODO(rfindley): no tests fail if I delete the line below.
result.modWhyHandles.Clear()
result.modVulnHandles.Clear()
}
-
- result.parseModHandles.Delete(uri)
- result.parseWorkHandles.Delete(uri)
- // Handle the invalidated file; it may have new contents or not exist.
- if !change.exists {
- result.files.Delete(uri)
- } else {
- // TODO(golang/go#57558): the line below is strictly necessary to ensure
- // that snapshots have each overlay, but it is problematic that we must
- // set any content in snapshot.clone: if the file has changed, let it be
- // re-read.
- result.files.Set(uri, change.fileHandle)
- }
-
- // Make sure to remove the changed file from the unloadable set.
- //
- // TODO(rfindley): this also looks wrong, as typing in an unloadable file
- // will result in repeated reloads. We should only delete if metadata
- // changed.
- result.unloadableFiles.Remove(uri)
}
// Deleting an import can cause list errors due to import cycles to be
@@ -2192,25 +2110,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
result.activePackages.Delete(id)
}
- // If a file has been deleted, we must delete metadata for all packages
- // containing that file.
- //
- // TODO(rfindley): why not keep invalid metadata in this case? If we
- // otherwise allow operate on invalid metadata, why not continue to do so,
- // skipping the missing file?
- skipID := map[PackageID]bool{}
- for _, c := range changes {
- if c.exists {
- continue
- }
- // The file has been deleted.
- if ids, ok := s.meta.ids[c.fileHandle.URI()]; ok {
- for _, id := range ids {
- skipID[id] = true
- }
- }
- }
-
// Any packages that need loading in s still need loading in the new
// snapshot.
for k, v := range s.shouldLoad {
@@ -2247,10 +2146,11 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
}
// Check whether the metadata should be deleted.
- if skipID[k] || invalidateMetadata {
+ if invalidateMetadata {
metadataUpdates[k] = nil
continue
}
+
}
// Update metadata, if necessary.
@@ -2279,6 +2179,14 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
return result, release
}
+func cloneWithout[V any](m *persistent.Map[span.URI, V], changes map[span.URI]source.FileHandle) *persistent.Map[span.URI, V] {
+ m2 := m.Clone()
+ for k := range changes {
+ m2.Delete(k)
+ }
+ return m2
+}
+
// deleteMostRelevantModFile deletes the mod file most likely to be the mod
// file for the changed URI, if it exists.
//
@@ -2396,9 +2304,9 @@ func fileWasSaved(originalFH, currentFH source.FileHandle) bool {
// - importDeleted means that an import has been deleted, or we can't
// determine if an import was deleted due to errors.
func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH source.FileHandle) (invalidate, pkgFileChanged, importDeleted bool) {
- if oldFH == nil || newFH == nil { // existential changes
- changed := (oldFH == nil) != (newFH == nil)
- return changed, changed, (newFH == nil) // we don't know if an import was deleted
+ if oe, ne := oldFH != nil && fileExists(oldFH), fileExists(newFH); !oe || !ne { // existential changes
+ changed := oe != ne
+ return changed, changed, !ne // we don't know if an import was deleted
}
// If the file hasn't changed, there's no need to reload.
@@ -2412,11 +2320,6 @@ func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH
newHeads, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, source.ParseHeader, false, newFH)
if oldErr != nil || newErr != nil {
- // TODO(rfindley): we can get here if newFH does not exist. There is
- // asymmetry, in that newFH may be non-nil even if the underlying file does
- // not exist.
- //
- // We should not produce a non-nil filehandle for a file that does not exist.
errChanged := (oldErr == nil) != (newErr == nil)
return errChanged, errChanged, (newErr != nil) // we don't know if an import was deleted
}
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index fbdb6047a78..ed1e2fe56ef 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -24,10 +24,10 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
exec "golang.org/x/sys/execabs"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
@@ -46,14 +46,16 @@ type View struct {
// name is the user-specified name of this view.
name string
- optionsMu sync.Mutex
- options *source.Options
+ // lastOptions holds the most recent options on this view, used for detecting
+ // major changes.
+ //
+ // Guarded by Session.viewMu.
+ lastOptions *source.Options
// Workspace information. The fields below are immutable, and together with
// options define the build list. Any change to these fields results in a new
// View.
- folder span.URI // user-specified workspace folder
- workspaceInformation // Go environment information
+ workspaceInformation // Go environment information
importsState *importsState
@@ -64,7 +66,7 @@ type View struct {
// vulns maps each go.mod file's URI to its known vulnerabilities.
vulnsMu sync.Mutex
- vulns map[span.URI]*govulncheck.Result
+ vulns map[span.URI]*vulncheck.Result
// parseCache holds an LRU cache of recently parsed files.
parseCache *parseCache
@@ -72,7 +74,7 @@ type View struct {
// fs is the file source used to populate this view.
fs *overlayFS
- // seenFiles tracks files that the view has accessed.
+ // knownFiles tracks files that the view has accessed.
// TODO(golang/go#57558): this notion is fundamentally problematic, and
// should be removed.
knownFilesMu sync.Mutex
@@ -114,6 +116,9 @@ type View struct {
//
// This type is compared to see if the View needs to be reconstructed.
type workspaceInformation struct {
+ // folder is the LSP workspace folder.
+ folder span.URI
+
// `go env` variables that need to be tracked by gopls.
goEnv
@@ -136,6 +141,13 @@ type workspaceInformation struct {
// inGOPATH reports whether the workspace directory is contained in a GOPATH
// directory.
inGOPATH bool
+
+ // goCommandDir is the dir to use for running go commands.
+ //
+ // The only case where this should matter is if we've narrowed the workspace to
+ // a single nested module. In that case, the go command won't be able to find
+ // the module unless we tell it the nested directory.
+ goCommandDir span.URI
}
// effectiveGO111MODULE reports the value of GO111MODULE effective in the go
@@ -403,44 +415,19 @@ func (v *View) Folder() span.URI {
return v.folder
}
-func (v *View) Options() *source.Options {
- v.optionsMu.Lock()
- defer v.optionsMu.Unlock()
- return v.options
-}
-
-func (v *View) FileKind(fh source.FileHandle) source.FileKind {
- // The kind of an unsaved buffer comes from the
- // TextDocumentItem.LanguageID field in the didChange event,
- // not from the file name. They may differ.
- if o, ok := fh.(*Overlay); ok {
- if o.kind != source.UnknownKind {
- return o.kind
- }
- }
-
- fext := filepath.Ext(fh.URI().Filename())
- switch fext {
- case ".go":
- return source.Go
- case ".mod":
- return source.Mod
- case ".sum":
- return source.Sum
- case ".work":
- return source.Work
- }
- exts := v.Options().TemplateExtensions
- for _, ext := range exts {
- if fext == ext || fext == "."+ext {
- return source.Tmpl
- }
- }
- // and now what? This should never happen, but it does for cgo before go1.15
- return source.Go
-}
-
func minorOptionsChange(a, b *source.Options) bool {
+ // TODO(rfindley): this function detects whether a view should be recreated,
+ // but this is also checked by the getWorkspaceInformation logic.
+ //
+ // We should eliminate this redundancy.
+ //
+ // Additionally, this function has existed for a long time, but git history
+ // suggests that it was added arbitrarily, not due to an actual performance
+ // problem.
+ //
+ // Especially now that we have optimized reinitialization of the session, we
+ // should consider just always creating a new view on any options change.
+
// Check if any of the settings that modify our understanding of files have
// been changed.
if !reflect.DeepEqual(a.Env, b.Env) {
@@ -468,29 +455,42 @@ func minorOptionsChange(a, b *source.Options) bool {
return reflect.DeepEqual(aBuildFlags, bBuildFlags)
}
-// SetViewOptions sets the options of the given view to new values. Calling
-// this may cause the view to be invalidated and a replacement view added to
-// the session. If so the new view will be returned, otherwise the original one
-// will be returned.
-func (s *Session) SetViewOptions(ctx context.Context, v *View, options *source.Options) (*View, error) {
+// SetFolderOptions updates the options of each View associated with the folder
+// of the given URI.
+//
+// Calling this may cause each related view to be invalidated and a replacement
+// view added to the session.
+func (s *Session) SetFolderOptions(ctx context.Context, uri span.URI, options *source.Options) error {
+ s.viewMu.Lock()
+ defer s.viewMu.Unlock()
+
+ for _, v := range s.views {
+ if v.folder == uri {
+ if err := s.setViewOptions(ctx, v, options); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (s *Session) setViewOptions(ctx context.Context, v *View, options *source.Options) error {
// no need to rebuild the view if the options were not materially changed
- v.optionsMu.Lock()
- if minorOptionsChange(v.options, options) {
- v.options = options
- v.optionsMu.Unlock()
- return v, nil
- }
- v.optionsMu.Unlock()
- newView, err := s.updateView(ctx, v, options)
- return newView, err
+ if minorOptionsChange(v.lastOptions, options) {
+ _, release := v.invalidateContent(ctx, nil, options, false)
+ release()
+ v.lastOptions = options
+ return nil
+ }
+ return s.updateViewLocked(ctx, v, options)
}
// viewEnv returns a string describing the environment of a newly created view.
//
// It must not be called concurrently with any other view methods.
func viewEnv(v *View) string {
- env := v.options.EnvSlice()
- buildFlags := append([]string{}, v.options.BuildFlags...)
+ env := v.snapshot.options.EnvSlice()
+ buildFlags := append([]string{}, v.snapshot.options.BuildFlags...)
var buf bytes.Buffer
fmt.Fprintf(&buf, `go info for %v
@@ -501,7 +501,7 @@ func viewEnv(v *View) string {
(selected go env: %v)
`,
v.folder.Filename(),
- v.workingDir().Filename(),
+ v.goCommandDir.Filename(),
strings.TrimRight(v.workspaceInformation.goversionOutput, "\n"),
v.snapshot.validBuildConfiguration(),
buildFlags,
@@ -539,13 +539,13 @@ func fileHasExtension(path string, suffixes []string) bool {
// locateTemplateFiles ensures that the snapshot has mapped template files
// within the workspace folder.
func (s *snapshot) locateTemplateFiles(ctx context.Context) {
- if len(s.view.Options().TemplateExtensions) == 0 {
+ if len(s.options.TemplateExtensions) == 0 {
return
}
- suffixes := s.view.Options().TemplateExtensions
+ suffixes := s.options.TemplateExtensions
searched := 0
- filterFunc := s.view.filterFunc()
+ filterFunc := s.filterFunc()
err := filepath.WalkDir(s.view.folder.Filename(), func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
@@ -579,33 +579,33 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) {
}
}
-func (v *View) contains(uri span.URI) bool {
+func (s *snapshot) contains(uri span.URI) bool {
// If we've expanded the go dir to a parent directory, consider if the
// expanded dir contains the uri.
// TODO(rfindley): should we ignore the root here? It is not provided by the
// user. It would be better to explicitly consider the set of active modules
// wherever relevant.
inGoDir := false
- if source.InDir(v.workingDir().Filename(), v.folder.Filename()) {
- inGoDir = source.InDir(v.workingDir().Filename(), uri.Filename())
+ if source.InDir(s.view.goCommandDir.Filename(), s.view.folder.Filename()) {
+ inGoDir = source.InDir(s.view.goCommandDir.Filename(), uri.Filename())
}
- inFolder := source.InDir(v.folder.Filename(), uri.Filename())
+ inFolder := source.InDir(s.view.folder.Filename(), uri.Filename())
if !inGoDir && !inFolder {
return false
}
- return !v.filterFunc()(uri)
+ return !s.filterFunc()(uri)
}
// filterFunc returns a func that reports whether uri is filtered by the currently configured
// directoryFilters.
-func (v *View) filterFunc() func(span.URI) bool {
- filterer := buildFilterer(v.folder.Filename(), v.gomodcache, v.Options())
+func (s *snapshot) filterFunc() func(span.URI) bool {
+ filterer := buildFilterer(s.view.folder.Filename(), s.view.gomodcache, s.options)
return func(uri span.URI) bool {
// Only filter relative to the configured root directory.
- if source.InDir(v.folder.Filename(), uri.Filename()) {
- return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), filterer)
+ if source.InDir(s.view.folder.Filename(), uri.Filename()) {
+ return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), s.view.folder.Filename()), filterer)
}
return false
}
@@ -632,7 +632,12 @@ func (v *View) relevantChange(c source.FileModification) bool {
// had neither test nor associated issue, and cited only emacs behavior, this
// logic was deleted.
- return v.contains(c.URI)
+ snapshot, release, err := v.getSnapshot()
+ if err != nil {
+ return false // view was shut down
+ }
+ defer release()
+ return snapshot.contains(c.URI)
}
func (v *View) markKnown(uri span.URI) {
@@ -778,7 +783,6 @@ func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) {
}
s.loadWorkspace(ctx, firstAttempt)
- s.collectAllKnownSubdirs(ctx)
}
func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadErr error) {
@@ -911,7 +915,9 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadEr
//
// invalidateContent returns a non-nil snapshot for the new content, along with
// a callback which the caller must invoke to release that snapshot.
-func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) (*snapshot, func()) {
+//
+// newOptions may be nil, in which case options remain unchanged.
+func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]source.FileHandle, newOptions *source.Options, forceReloadMetadata bool) (*snapshot, func()) {
// Detach the context so that content invalidation cannot be canceled.
ctx = xcontext.Detach(ctx)
@@ -933,7 +939,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*file
prevSnapshot.AwaitInitialized(ctx)
// Save one lease of the cloned snapshot in the view.
- v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata)
+ v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes, newOptions, forceReloadMetadata)
prevReleaseSnapshot()
v.destroy(prevSnapshot, "View.invalidateContent")
@@ -947,7 +953,9 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI,
return workspaceInformation{}, fmt.Errorf("invalid workspace folder path: %w; check that the casing of the configured workspace folder path agrees with the casing reported by the operating system", err)
}
var err error
- var info workspaceInformation
+ info := workspaceInformation{
+ folder: folder,
+ }
inv := gocommand.Invocation{
WorkingDir: folder.Filename(),
Env: options.EnvSlice(),
@@ -985,6 +993,20 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI,
break
}
}
+
+ // Compute the "working directory", which is where we run go commands.
+ //
+ // Note: if gowork is in use, this will default to the workspace folder. In
+ // the past, we would instead use the folder containing go.work. This should
+ // not make a difference, and in fact may improve go list error messages.
+ //
+ // TODO(golang/go#57514): eliminate the expandWorkspaceToModule setting
+ // entirely.
+ if options.ExpandWorkspaceToModule && info.gomod != "" {
+ info.goCommandDir = span.URIFromPath(filepath.Dir(info.gomod.Filename()))
+ } else {
+ info.goCommandDir = folder
+ }
return info, nil
}
@@ -1026,24 +1048,6 @@ func findWorkspaceModFile(ctx context.Context, folderURI span.URI, fs source.Fil
return "", nil
}
-// workingDir returns the directory from which to run Go commands.
-//
-// The only case where this should matter is if we've narrowed the workspace to
-// a singular nested module. In that case, the go command won't be able to find
-// the module unless we tell it the nested directory.
-func (v *View) workingDir() span.URI {
- // Note: if gowork is in use, this will default to the workspace folder. In
- // the past, we would instead use the folder containing go.work. This should
- // not make a difference, and in fact may improve go list error messages.
- //
- // TODO(golang/go#57514): eliminate the expandWorkspaceToModule setting
- // entirely.
- if v.Options().ExpandWorkspaceToModule && v.gomod != "" {
- return span.URIFromPath(filepath.Dir(v.gomod.Filename()))
- }
- return v.folder
-}
-
// findRootPattern looks for files with the given basename in dir or any parent
// directory of dir, using the provided FileSource. It returns the first match,
// starting from dir and search parents.
@@ -1053,11 +1057,11 @@ func (v *View) workingDir() span.URI {
func findRootPattern(ctx context.Context, dir, basename string, fs source.FileSource) (string, error) {
for dir != "" {
target := filepath.Join(dir, basename)
- exists, err := fileExists(ctx, span.URIFromPath(target), fs)
+ fh, err := fs.ReadFile(ctx, span.URIFromPath(target))
if err != nil {
- return "", err // not readable or context cancelled
+ return "", err // context cancelled
}
- if exists {
+ if fileExists(fh) {
return target, nil
}
// Trailing separators must be trimmed, otherwise filepath.Split is a noop.
@@ -1121,8 +1125,8 @@ func (v *View) ClearModuleUpgrades(modfile span.URI) {
const maxGovulncheckResultAge = 1 * time.Hour // Invalidate results older than this limit.
var timeNow = time.Now // for testing
-func (v *View) Vulnerabilities(modfiles ...span.URI) map[span.URI]*govulncheck.Result {
- m := make(map[span.URI]*govulncheck.Result)
+func (v *View) Vulnerabilities(modfiles ...span.URI) map[span.URI]*vulncheck.Result {
+ m := make(map[span.URI]*vulncheck.Result)
now := timeNow()
v.vulnsMu.Lock()
defer v.vulnsMu.Unlock()
@@ -1143,7 +1147,7 @@ func (v *View) Vulnerabilities(modfiles ...span.URI) map[span.URI]*govulncheck.R
return m
}
-func (v *View) SetVulnerabilities(modfile span.URI, vulns *govulncheck.Result) {
+func (v *View) SetVulnerabilities(modfile span.URI, vulns *vulncheck.Result) {
v.vulnsMu.Lock()
defer v.vulnsMu.Unlock()
@@ -1232,7 +1236,7 @@ func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modConten
// No vendor directory?
// TODO(golang/go#57514): this is wrong if the working dir is not the module
// root.
- if fi, err := os.Stat(filepath.Join(s.view.workingDir().Filename(), "vendor")); err != nil || !fi.IsDir() {
+ if fi, err := os.Stat(filepath.Join(s.view.goCommandDir.Filename(), "vendor")); err != nil || !fi.IsDir() {
return false, nil
}
diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go
index 21b10b6a982..2b7249b69ab 100644
--- a/gopls/internal/lsp/cache/view_test.go
+++ b/gopls/internal/lsp/cache/view_test.go
@@ -12,10 +12,10 @@ import (
"time"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/fake"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/vulncheck"
)
func TestCaseInsensitiveFilesystem(t *testing.T) {
@@ -216,19 +216,19 @@ func TestView_Vulnerabilities(t *testing.T) {
now := time.Now()
view := &View{
- vulns: make(map[span.URI]*govulncheck.Result),
+ vulns: make(map[span.URI]*vulncheck.Result),
}
file1, file2 := span.URIFromPath("f1/go.mod"), span.URIFromPath("f2/go.mod")
- vuln1 := &govulncheck.Result{AsOf: now.Add(-(maxGovulncheckResultAge * 3) / 4)} // already ~3/4*maxGovulncheckResultAge old
+ vuln1 := &vulncheck.Result{AsOf: now.Add(-(maxGovulncheckResultAge * 3) / 4)} // already ~3/4*maxGovulncheckResultAge old
view.SetVulnerabilities(file1, vuln1)
- vuln2 := &govulncheck.Result{AsOf: now} // fresh.
+ vuln2 := &vulncheck.Result{AsOf: now} // fresh.
view.SetVulnerabilities(file2, vuln2)
t.Run("fresh", func(t *testing.T) {
got := view.Vulnerabilities()
- want := map[span.URI]*govulncheck.Result{
+ want := map[span.URI]*vulncheck.Result{
file1: vuln1,
file2: vuln2,
}
@@ -242,7 +242,7 @@ func TestView_Vulnerabilities(t *testing.T) {
timeNow = func() time.Time { return now.Add(maxGovulncheckResultAge / 2) }
t.Run("after30min", func(t *testing.T) {
got := view.Vulnerabilities()
- want := map[span.URI]*govulncheck.Result{
+ want := map[span.URI]*vulncheck.Result{
file1: nil, // expired.
file2: vuln2,
}
@@ -257,7 +257,7 @@ func TestView_Vulnerabilities(t *testing.T) {
t.Run("after1hr", func(t *testing.T) {
got := view.Vulnerabilities()
- want := map[span.URI]*govulncheck.Result{
+ want := map[span.URI]*vulncheck.Result{
file1: nil,
file2: nil,
}
diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go
index 28179f5a0b9..e344f4950cc 100644
--- a/gopls/internal/lsp/cache/workspace.go
+++ b/gopls/internal/lsp/cache/workspace.go
@@ -9,9 +9,7 @@ import (
"errors"
"fmt"
"io/fs"
- "os"
"path/filepath"
- "sort"
"strings"
"golang.org/x/mod/modfile"
@@ -60,38 +58,6 @@ func computeWorkspaceModFiles(ctx context.Context, gomod, gowork span.URI, go111
return nil, nil
}
-// dirs returns the workspace directories for the loaded modules.
-//
-// A workspace directory is, roughly speaking, a directory for which we care
-// about file changes. This is used for the purpose of registering file
-// watching patterns, and expanding directory modifications to their adjacent
-// files.
-//
-// TODO(rfindley): move this to snapshot.go.
-// TODO(rfindley): can we make this abstraction simpler and/or more accurate?
-func (s *snapshot) dirs(ctx context.Context) []span.URI {
- dirSet := make(map[span.URI]struct{})
-
- // Dirs should, at the very least, contain the working directory and folder.
- dirSet[s.view.workingDir()] = struct{}{}
- dirSet[s.view.folder] = struct{}{}
-
- // Additionally, if e.g. go.work indicates other workspace modules, we should
- // include their directories too.
- if s.workspaceModFilesErr == nil {
- for modFile := range s.workspaceModFiles {
- dir := filepath.Dir(modFile.Filename())
- dirSet[span.URIFromPath(dir)] = struct{}{}
- }
- }
- var dirs []span.URI
- for d := range dirSet {
- dirs = append(dirs, d)
- }
- sort.Slice(dirs, func(i, j int) bool { return dirs[i] < dirs[j] })
- return dirs
-}
-
// isGoMod reports if uri is a go.mod file.
func isGoMod(uri span.URI) bool {
return filepath.Base(uri.Filename()) == "go.mod"
@@ -102,25 +68,11 @@ func isGoWork(uri span.URI) bool {
return filepath.Base(uri.Filename()) == "go.work"
}
-// fileExists reports if the file uri exists within source.
-func fileExists(ctx context.Context, uri span.URI, source source.FileSource) (bool, error) {
- fh, err := source.ReadFile(ctx, uri)
- if err != nil {
- return false, err
- }
- return fileHandleExists(fh)
-}
-
-// fileHandleExists reports if the file underlying fh actually exists.
-func fileHandleExists(fh source.FileHandle) (bool, error) {
+// fileExists reports whether the file has a Content (which may be empty).
+// An overlay exists even if it is not reflected in the file system.
+func fileExists(fh source.FileHandle) bool {
_, err := fh.Content()
- if err == nil {
- return true, nil
- }
- if os.IsNotExist(err) {
- return false, nil
- }
- return false, err
+ return err == nil
}
// errExhausted is returned by findModules if the file scan limit is reached.
diff --git a/gopls/internal/lsp/cmd/capabilities_test.go b/gopls/internal/lsp/cmd/capabilities_test.go
index 6d4e32f0fe2..e952b0dcbe7 100644
--- a/gopls/internal/lsp/cmd/capabilities_test.go
+++ b/gopls/internal/lsp/cmd/capabilities_test.go
@@ -7,7 +7,6 @@ package cmd
import (
"context"
"fmt"
- "io/ioutil"
"os"
"path/filepath"
"testing"
@@ -15,6 +14,7 @@ import (
"golang.org/x/tools/gopls/internal/lsp"
"golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/testenv"
)
@@ -27,15 +27,15 @@ func TestCapabilities(t *testing.T) {
// Is there some missing error reporting somewhere?
testenv.NeedsTool(t, "go")
- tmpDir, err := ioutil.TempDir("", "fake")
+ tmpDir, err := os.MkdirTemp("", "fake")
if err != nil {
t.Fatal(err)
}
tmpFile := filepath.Join(tmpDir, "fake.go")
- if err := ioutil.WriteFile(tmpFile, []byte(""), 0775); err != nil {
+ if err := os.WriteFile(tmpFile, []byte(""), 0775); err != nil {
t.Fatal(err)
}
- if err := ioutil.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module fake\n\ngo 1.12\n"), 0775); err != nil {
+ if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module fake\n\ngo 1.12\n"), 0775); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
@@ -49,7 +49,8 @@ func TestCapabilities(t *testing.T) {
// Send an initialize request to the server.
ctx := context.Background()
client := newClient(app, nil)
- server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), client)
+ options := source.DefaultOptions(app.options)
+ server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil)), client, options)
result, err := server.Initialize(ctx, params)
if err != nil {
t.Fatal(err)
diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go
index 340073d8a5a..d3d4069a412 100644
--- a/gopls/internal/lsp/cmd/cmd.go
+++ b/gopls/internal/lsp/cmd/cmd.go
@@ -11,7 +11,6 @@ import (
"context"
"flag"
"fmt"
- "io/ioutil"
"log"
"os"
"reflect"
@@ -159,6 +158,12 @@ Command:
for _, c := range app.featureCommands() {
fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp())
}
+ if app.verbose() {
+ fmt.Fprint(w, "\t\nInternal Use Only\t\n")
+ for _, c := range app.internalCommands() {
+ fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp())
+ }
+ }
fmt.Fprint(w, "\nflags:\n")
printFlagDefaults(f)
}
@@ -264,6 +269,7 @@ func (app *Application) Commands() []tool.Application {
var commands []tool.Application
commands = append(commands, app.mainCommands()...)
commands = append(commands, app.featureCommands()...)
+ commands = append(commands, app.internalCommands()...)
return commands
}
@@ -278,6 +284,12 @@ func (app *Application) mainCommands() []tool.Application {
}
}
+func (app *Application) internalCommands() []tool.Application {
+ return []tool.Application{
+ &vulncheck{app: app},
+ }
+}
+
func (app *Application) featureCommands() []tool.Application {
return []tool.Application{
&callHierarchy{app: app},
@@ -299,8 +311,8 @@ func (app *Application) featureCommands() []tool.Application {
&stats{app: app},
&suggestedFix{app: app},
&symbols{app: app},
+
&workspaceSymbol{app: app},
- &vulncheck{app: app},
}
}
@@ -316,7 +328,8 @@ func (app *Application) connect(ctx context.Context, onProgress func(*protocol.P
switch {
case app.Remote == "":
client := newClient(app, onProgress)
- server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), client)
+ options := source.DefaultOptions(app.options)
+ server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil)), client, options)
conn := newConnection(server, client)
if err := conn.initialize(protocol.WithClient(ctx, client), app.options); err != nil {
return nil, err
@@ -326,10 +339,7 @@ func (app *Application) connect(ctx context.Context, onProgress func(*protocol.P
case strings.HasPrefix(app.Remote, "internal@"):
internalMu.Lock()
defer internalMu.Unlock()
- opts := source.DefaultOptions().Clone()
- if app.options != nil {
- app.options(opts)
- }
+ opts := source.DefaultOptions(app.options)
key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher)
if c := internalConnections[key]; c != nil {
return c, nil
@@ -376,10 +386,7 @@ func (c *connection) initialize(ctx context.Context, options func(*source.Option
params.Capabilities.Workspace.Configuration = true
// Make sure to respect configured options when sending initialize request.
- opts := source.DefaultOptions().Clone()
- if options != nil {
- options(opts)
- }
+ opts := source.DefaultOptions(options)
// If you add an additional option here, you must update the map key in connect.
params.Capabilities.TextDocument.Hover = &protocol.HoverClientCapabilities{
ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
@@ -708,7 +715,7 @@ func (c *cmdClient) getFile(uri span.URI) *cmdFile {
c.files[uri] = file
}
if file.mapper == nil {
- content, err := ioutil.ReadFile(uri.Filename())
+ content, err := os.ReadFile(uri.Filename())
if err != nil {
file.err = fmt.Errorf("getFile: %v: %v", uri, err)
return file
diff --git a/gopls/internal/lsp/cmd/help_test.go b/gopls/internal/lsp/cmd/help_test.go
index 6bd3c8c501f..6d8f10af46f 100644
--- a/gopls/internal/lsp/cmd/help_test.go
+++ b/gopls/internal/lsp/cmd/help_test.go
@@ -8,7 +8,7 @@ import (
"bytes"
"context"
"flag"
- "io/ioutil"
+ "os"
"path/filepath"
"testing"
@@ -41,12 +41,12 @@ func TestHelpFiles(t *testing.T) {
helpFile := filepath.Join("usage", name+".hlp")
got := buf.Bytes()
if *updateHelpFiles {
- if err := ioutil.WriteFile(helpFile, got, 0666); err != nil {
+ if err := os.WriteFile(helpFile, got, 0666); err != nil {
t.Errorf("Failed writing %v: %v", helpFile, err)
}
return
}
- want, err := ioutil.ReadFile(helpFile)
+ want, err := os.ReadFile(helpFile)
if err != nil {
t.Fatalf("Missing help file %q", helpFile)
}
@@ -56,3 +56,29 @@ func TestHelpFiles(t *testing.T) {
})
}
}
+
+func TestVerboseHelp(t *testing.T) {
+ testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code.
+ app := cmd.New(appName, "", nil, nil)
+ ctx := context.Background()
+ var buf bytes.Buffer
+ s := flag.NewFlagSet(appName, flag.ContinueOnError)
+ s.SetOutput(&buf)
+ tool.Run(ctx, s, app, []string{"-v", "-h"})
+ got := buf.Bytes()
+
+ helpFile := filepath.Join("usage", "usage-v.hlp")
+ if *updateHelpFiles {
+ if err := os.WriteFile(helpFile, got, 0666); err != nil {
+ t.Errorf("Failed writing %v: %v", helpFile, err)
+ }
+ return
+ }
+ want, err := os.ReadFile(helpFile)
+ if err != nil {
+ t.Fatalf("Missing help file %q", helpFile)
+ }
+ if diff := cmp.Diff(string(want), string(got)); diff != "" {
+ t.Errorf("Help file %q did not match, run with -update-help-files to fix (-want +got)\n%s", helpFile, diff)
+ }
+}
diff --git a/gopls/internal/lsp/cmd/info.go b/gopls/internal/lsp/cmd/info.go
index d63e24b6bfc..b0f08bbef67 100644
--- a/gopls/internal/lsp/cmd/info.go
+++ b/gopls/internal/lsp/cmd/info.go
@@ -299,8 +299,7 @@ gopls also includes software made available under these licenses:
`
func (l *licenses) Run(ctx context.Context, args ...string) error {
- opts := source.DefaultOptions()
- l.app.options(opts)
+ opts := source.DefaultOptions(l.app.options)
txt := licensePreamble
if opts.LicensesText == "" {
txt += "(development gopls, license information not available)"
diff --git a/gopls/internal/lsp/cmd/semantictokens.go b/gopls/internal/lsp/cmd/semantictokens.go
index 3d2ad9c58e2..1acd83a2ac0 100644
--- a/gopls/internal/lsp/cmd/semantictokens.go
+++ b/gopls/internal/lsp/cmd/semantictokens.go
@@ -11,7 +11,6 @@ import (
"fmt"
"go/parser"
"go/token"
- "io/ioutil"
"log"
"os"
"unicode/utf8"
@@ -87,7 +86,7 @@ func (c *semtok) Run(ctx context.Context, args ...string) error {
return err
}
- buf, err := ioutil.ReadFile(args[0])
+ buf, err := os.ReadFile(args[0])
if err != nil {
return err
}
@@ -162,7 +161,7 @@ func markLine(m mark, lines [][]byte) {
}
func decorate(file string, result []uint32) error {
- buf, err := ioutil.ReadFile(file)
+ buf, err := os.ReadFile(file)
if err != nil {
return err
}
diff --git a/gopls/internal/lsp/cmd/usage/usage-v.hlp b/gopls/internal/lsp/cmd/usage/usage-v.hlp
new file mode 100644
index 00000000000..0edb37e9300
--- /dev/null
+++ b/gopls/internal/lsp/cmd/usage/usage-v.hlp
@@ -0,0 +1,82 @@
+
+gopls is a Go language server.
+
+It is typically used with an editor to provide language features. When no
+command is specified, gopls will default to the 'serve' command. The language
+features can also be accessed via the gopls command-line interface.
+
+Usage:
+ gopls help []
+
+Command:
+
+Main
+ serve run a server for Go code using the Language Server Protocol
+ version print the gopls version information
+ bug report a bug in gopls
+ help print usage information for subcommands
+ api-json print json describing gopls API
+ licenses print licenses of included software
+
+Features
+ call_hierarchy display selected identifier's call hierarchy
+ check show diagnostic results for the specified file
+ definition show declaration of selected identifier
+ folding_ranges display selected file's folding ranges
+ format format the code according to the go standard
+ highlight display selected identifier's highlights
+ implementation display selected identifier's implementation
+ imports updates import statements
+ remote interact with the gopls daemon
+ inspect interact with the gopls daemon (deprecated: use 'remote')
+ links list links in a file
+ prepare_rename test validity of a rename operation at location
+ references display selected identifier's references
+ rename rename selected identifier
+ semtok show semantic tokens for the specified file
+ signature display selected identifier's signature
+ stats print workspace statistics
+ fix apply suggested fixes
+ symbols display selected file's symbols
+ workspace_symbol search symbols in workspace
+
+Internal Use Only
+ vulncheck run vulncheck analysis (internal-use only)
+
+flags:
+ -debug=string
+ serve debug information on the supplied address
+ -listen=string
+ address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used.
+ -listen.timeout=duration
+ when used with -listen, shut down the server when there are no connected clients for this duration
+ -logfile=string
+ 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
+ write alloc profile to this file
+ -profile.cpu=string
+ write CPU profile to this file
+ -profile.mem=string
+ write memory profile to this file
+ -profile.trace=string
+ write trace log to this file
+ -remote=string
+ forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment.
+ -remote.debug=string
+ when used with -remote=auto, the -debug value used to start the daemon
+ -remote.listen.timeout=duration
+ when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s)
+ -remote.logfile=string
+ when used with -remote=auto, the -logfile value used to start the daemon
+ -rpc.trace
+ print the full rpc trace in lsp inspector format
+ -v,-verbose
+ verbose output
+ -vv,-veryverbose
+ very verbose output
diff --git a/gopls/internal/lsp/cmd/usage/usage.hlp b/gopls/internal/lsp/cmd/usage/usage.hlp
index 7f1bfb46a87..c9cc12a943f 100644
--- a/gopls/internal/lsp/cmd/usage/usage.hlp
+++ b/gopls/internal/lsp/cmd/usage/usage.hlp
@@ -39,7 +39,6 @@ Features
fix apply suggested fixes
symbols display selected file's symbols
workspace_symbol search symbols in workspace
- vulncheck run experimental vulncheck analysis (experimental: under development)
flags:
-debug=string
diff --git a/gopls/internal/lsp/cmd/usage/vulncheck.hlp b/gopls/internal/lsp/cmd/usage/vulncheck.hlp
index 4fbe573e22a..d16cb130871 100644
--- a/gopls/internal/lsp/cmd/usage/vulncheck.hlp
+++ b/gopls/internal/lsp/cmd/usage/vulncheck.hlp
@@ -1,9 +1,9 @@
-run experimental vulncheck analysis (experimental: under development)
+run vulncheck analysis (internal-use only)
Usage:
gopls [flags] vulncheck
- WARNING: this command is experimental.
+ WARNING: this command is for internal-use only.
By default, the command outputs a JSON-encoded
golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult
@@ -11,7 +11,3 @@ Usage:
Example:
$ gopls vulncheck
- -config
- If true, the command reads a JSON-encoded package load configuration from stdin
- -summary
- If true, outputs a JSON-encoded govulnchecklib.Summary JSON
diff --git a/gopls/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go
index 5c851b66e78..855b9eef830 100644
--- a/gopls/internal/lsp/cmd/vulncheck.go
+++ b/gopls/internal/lsp/cmd/vulncheck.go
@@ -6,43 +6,28 @@ package cmd
import (
"context"
- "encoding/json"
"flag"
"fmt"
"os"
- "golang.org/x/tools/go/packages"
- vulnchecklib "golang.org/x/tools/gopls/internal/vulncheck"
- "golang.org/x/tools/internal/tool"
+ "golang.org/x/tools/gopls/internal/vulncheck/scan"
)
// vulncheck implements the vulncheck command.
+// TODO(hakim): hide from the public.
type vulncheck struct {
- Config bool `flag:"config" help:"If true, the command reads a JSON-encoded package load configuration from stdin"`
- AsSummary bool `flag:"summary" help:"If true, outputs a JSON-encoded govulnchecklib.Summary JSON"`
- app *Application
+ app *Application
}
-type pkgLoadConfig struct {
- // BuildFlags is a list of command-line flags to be passed through to
- // the build system's query tool.
- BuildFlags []string
-
- // If Tests is set, the loader includes related test packages.
- Tests bool
-}
-
-// TODO(hyangah): document pkgLoadConfig
-
func (v *vulncheck) Name() string { return "vulncheck" }
func (v *vulncheck) Parent() string { return v.app.Name() }
func (v *vulncheck) Usage() string { return "" }
func (v *vulncheck) ShortHelp() string {
- return "run experimental vulncheck analysis (experimental: under development)"
+ return "run vulncheck analysis (internal-use only)"
}
func (v *vulncheck) DetailedHelp(f *flag.FlagSet) {
fmt.Fprint(f.Output(), `
- WARNING: this command is experimental.
+ WARNING: this command is for internal-use only.
By default, the command outputs a JSON-encoded
golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult
@@ -51,32 +36,10 @@ func (v *vulncheck) DetailedHelp(f *flag.FlagSet) {
$ gopls vulncheck
`)
- printFlagDefaults(f)
}
func (v *vulncheck) Run(ctx context.Context, args ...string) error {
- if vulnchecklib.Main == nil {
- return fmt.Errorf("vulncheck command is available only in gopls compiled with go1.18 or newer")
- }
-
- // TODO(hyangah): what's wrong with allowing multiple targets?
- if len(args) > 1 {
- return tool.CommandLineErrorf("vulncheck accepts at most one package pattern")
- }
- var cfg pkgLoadConfig
- if v.Config {
- if err := json.NewDecoder(os.Stdin).Decode(&cfg); err != nil {
- return tool.CommandLineErrorf("failed to parse cfg: %v", err)
- }
- }
- loadCfg := packages.Config{
- Context: ctx,
- Tests: cfg.Tests,
- BuildFlags: cfg.BuildFlags,
- // inherit the current process's cwd and env.
- }
-
- if err := vulnchecklib.Main(loadCfg, args...); err != nil {
+ if err := scan.Main(ctx, args...); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go
index bef4e34d68f..555131ea796 100644
--- a/gopls/internal/lsp/code_action.go
+++ b/gopls/internal/lsp/code_action.go
@@ -299,7 +299,7 @@ func (s *Server) findMatchingDiagnostics(uri span.URI, pd protocol.Diagnostic) [
func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
- for _, kinds := range s.session.Options().SupportedCodeActions {
+ for _, kinds := range s.Options().SupportedCodeActions {
for kind := range kinds {
allCodeActionKinds[kind] = struct{}{}
}
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index 388030e4bcc..f4d4a9e4ba2 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -12,18 +12,15 @@ import (
"fmt"
"io"
"os"
- "os/exec"
"path/filepath"
"runtime"
"runtime/pprof"
"sort"
"strings"
- "time"
"golang.org/x/mod/modfile"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/bug"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/debug"
@@ -31,7 +28,9 @@ import (
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/vulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/scan"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/tokeninternal"
@@ -43,7 +42,7 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
defer done()
var found bool
- for _, name := range s.session.Options().SupportedCommands {
+ for _, name := range s.Options().SupportedCommands {
if name == params.Command {
found = true
break
@@ -65,6 +64,20 @@ type commandHandler struct {
params *protocol.ExecuteCommandParams
}
+func (h *commandHandler) MaybePromptForTelemetry(ctx context.Context) error {
+ go h.s.maybePromptForTelemetry(ctx, true)
+ return nil
+}
+
+func (*commandHandler) AddTelemetryCounters(_ context.Context, args command.AddTelemetryCountersArgs) error {
+ if len(args.Names) != len(args.Values) {
+ return fmt.Errorf("Names and Values must have the same length")
+ }
+ // invalid counter update requests will be silently dropped. (no audience)
+ telemetry.AddForwardedCounters(args.Names, args.Values)
+ return nil
+}
+
// commandConfig configures common command set-up and execution.
type commandConfig struct {
async bool // whether to run the command asynchronously. Async commands can only return errors.
@@ -96,7 +109,7 @@ func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run command
if cfg.requireSave {
var unsaved []string
for _, overlay := range c.s.session.Overlays() {
- if !overlay.Saved() {
+ if !overlay.SameContentsOnDisk() {
unsaved = append(unsaved, overlay.URI().Filename())
}
}
@@ -896,8 +909,8 @@ type pkgLoadConfig struct {
Tests bool
}
-func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*govulncheck.Result, error) {
- ret := map[protocol.DocumentURI]*govulncheck.Result{}
+func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) {
+ ret := map[protocol.DocumentURI]*vulncheck.Result{}
err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error {
if deps.snapshot.Options().Vulncheck == source.ModeVulncheckImports {
for _, modfile := range deps.snapshot.ModFiles() {
@@ -936,59 +949,22 @@ func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.Vulnch
}, func(ctx context.Context, deps commandDeps) error {
tokenChan <- deps.work.Token()
- opts := deps.snapshot.Options()
- // quickly test if gopls is compiled to support govulncheck
- // by checking vulncheck.Main. Alternatively, we can continue and
- // let the `gopls vulncheck` command fail. This is lighter-weight.
- if vulncheck.Main == nil {
- return errors.New("vulncheck feature is not available")
- }
-
- cmd := exec.CommandContext(ctx, os.Args[0], "vulncheck", "-config", args.Pattern)
- cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename())
-
- var viewEnv []string
- if e := opts.EnvSlice(); e != nil {
- viewEnv = append(os.Environ(), e...)
- }
- cmd.Env = viewEnv
+ workDoneWriter := progress.NewWorkDoneWriter(ctx, deps.work)
+ dir := filepath.Dir(args.URI.SpanURI().Filename())
+ pattern := args.Pattern
- // stdin: gopls vulncheck expects JSON-encoded configuration from STDIN when -config flag is set.
- var stdin bytes.Buffer
- cmd.Stdin = &stdin
-
- if err := json.NewEncoder(&stdin).Encode(pkgLoadConfig{
- BuildFlags: opts.BuildFlags,
- // TODO(hyangah): add `tests` flag in command.VulncheckArgs
- }); err != nil {
- return fmt.Errorf("failed to pass package load config: %v", err)
- }
-
- // stderr: stream gopls vulncheck's STDERR as progress reports
- er := progress.NewEventWriter(ctx, "vulncheck")
- stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work))
- cmd.Stderr = stderr
- // TODO: can we stream stdout?
- stdout, err := cmd.Output()
+ result, err := scan.RunGovulncheck(ctx, pattern, deps.snapshot, dir, workDoneWriter)
if err != nil {
- return fmt.Errorf("failed to run govulncheck: %v", err)
- }
-
- var result govulncheck.Result
- if err := json.Unmarshal(stdout, &result); err != nil {
- // TODO: for easy debugging, log the failed stdout somewhere?
- return fmt.Errorf("failed to parse govulncheck output: %v", err)
+ return err
}
- result.Mode = govulncheck.ModeGovulncheck
- result.AsOf = time.Now()
- deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), &result)
+ deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), result)
c.s.diagnoseSnapshot(deps.snapshot, nil, false, 0)
- vulns := result.Vulns
- affecting := make([]string, 0, len(vulns))
- for _, v := range vulns {
- if v.IsCalled() {
- affecting = append(affecting, v.OSV.ID)
+
+ affecting := make(map[string]bool, len(result.Entries))
+ for _, finding := range result.Findings {
+ if len(finding.Trace) > 1 { // at least 2 frames if callstack exists (vulnerability, entry)
+ affecting[finding.OSV] = true
}
}
if len(affecting) == 0 {
@@ -997,10 +973,14 @@ func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.Vulnch
Message: "No vulnerabilities found",
})
}
- sort.Strings(affecting)
+ affectingOSVs := make([]string, 0, len(affecting))
+ for id := range affecting {
+ affectingOSVs = append(affectingOSVs, id)
+ }
+ sort.Strings(affectingOSVs)
return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Warning,
- Message: fmt.Sprintf("Found %v", strings.Join(affecting, ", ")),
+ Message: fmt.Sprintf("Found %v", strings.Join(affectingOSVs, ", ")),
})
})
if err != nil {
@@ -1155,7 +1135,7 @@ func (c *commandHandler) RunGoWorkCommand(ctx context.Context, args command.RunG
if err != nil {
return fmt.Errorf("reading current go.work file: %v", err)
}
- if !fh.Saved() {
+ if !fh.SameContentsOnDisk() {
return fmt.Errorf("must save workspace file %s before running go work commands", goworkURI)
}
} else {
diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go
index 00b76579601..5dd2a9dd452 100644
--- a/gopls/internal/lsp/command/command_gen.go
+++ b/gopls/internal/lsp/command/command_gen.go
@@ -19,39 +19,42 @@ import (
)
const (
- AddDependency Command = "add_dependency"
- AddImport Command = "add_import"
- ApplyFix Command = "apply_fix"
- CheckUpgrades Command = "check_upgrades"
- EditGoDirective Command = "edit_go_directive"
- FetchVulncheckResult Command = "fetch_vulncheck_result"
- GCDetails Command = "gc_details"
- Generate Command = "generate"
- GoGetPackage Command = "go_get_package"
- ListImports Command = "list_imports"
- ListKnownPackages Command = "list_known_packages"
- MemStats Command = "mem_stats"
- RegenerateCgo Command = "regenerate_cgo"
- RemoveDependency Command = "remove_dependency"
- ResetGoModDiagnostics Command = "reset_go_mod_diagnostics"
- RunGoWorkCommand Command = "run_go_work_command"
- RunGovulncheck Command = "run_govulncheck"
- RunTests Command = "run_tests"
- StartDebugging Command = "start_debugging"
- StartProfile Command = "start_profile"
- StopProfile Command = "stop_profile"
- Test Command = "test"
- Tidy Command = "tidy"
- ToggleGCDetails Command = "toggle_gc_details"
- UpdateGoSum Command = "update_go_sum"
- UpgradeDependency Command = "upgrade_dependency"
- Vendor Command = "vendor"
- WorkspaceStats Command = "workspace_stats"
+ AddDependency Command = "add_dependency"
+ AddImport Command = "add_import"
+ AddTelemetryCounters Command = "add_telemetry_counters"
+ ApplyFix Command = "apply_fix"
+ CheckUpgrades Command = "check_upgrades"
+ EditGoDirective Command = "edit_go_directive"
+ FetchVulncheckResult Command = "fetch_vulncheck_result"
+ GCDetails Command = "gc_details"
+ Generate Command = "generate"
+ GoGetPackage Command = "go_get_package"
+ ListImports Command = "list_imports"
+ ListKnownPackages Command = "list_known_packages"
+ MaybePromptForTelemetry Command = "maybe_prompt_for_telemetry"
+ MemStats Command = "mem_stats"
+ RegenerateCgo Command = "regenerate_cgo"
+ RemoveDependency Command = "remove_dependency"
+ ResetGoModDiagnostics Command = "reset_go_mod_diagnostics"
+ RunGoWorkCommand Command = "run_go_work_command"
+ RunGovulncheck Command = "run_govulncheck"
+ RunTests Command = "run_tests"
+ StartDebugging Command = "start_debugging"
+ StartProfile Command = "start_profile"
+ StopProfile Command = "stop_profile"
+ Test Command = "test"
+ Tidy Command = "tidy"
+ ToggleGCDetails Command = "toggle_gc_details"
+ UpdateGoSum Command = "update_go_sum"
+ UpgradeDependency Command = "upgrade_dependency"
+ Vendor Command = "vendor"
+ WorkspaceStats Command = "workspace_stats"
)
var Commands = []Command{
AddDependency,
AddImport,
+ AddTelemetryCounters,
ApplyFix,
CheckUpgrades,
EditGoDirective,
@@ -61,6 +64,7 @@ var Commands = []Command{
GoGetPackage,
ListImports,
ListKnownPackages,
+ MaybePromptForTelemetry,
MemStats,
RegenerateCgo,
RemoveDependency,
@@ -94,6 +98,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte
return nil, err
}
return nil, s.AddImport(ctx, a0)
+ case "gopls.add_telemetry_counters":
+ var a0 AddTelemetryCountersArgs
+ if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+ return nil, err
+ }
+ return nil, s.AddTelemetryCounters(ctx, a0)
case "gopls.apply_fix":
var a0 ApplyFixArgs
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@@ -148,6 +158,8 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte
return nil, err
}
return s.ListKnownPackages(ctx, a0)
+ case "gopls.maybe_prompt_for_telemetry":
+ return nil, s.MaybePromptForTelemetry(ctx)
case "gopls.mem_stats":
return s.MemStats(ctx)
case "gopls.regenerate_cgo":
@@ -272,6 +284,18 @@ func NewAddImportCommand(title string, a0 AddImportArgs) (protocol.Command, erro
}, nil
}
+func NewAddTelemetryCountersCommand(title string, a0 AddTelemetryCountersArgs) (protocol.Command, error) {
+ args, err := MarshalArgs(a0)
+ if err != nil {
+ return protocol.Command{}, err
+ }
+ return protocol.Command{
+ Title: title,
+ Command: "gopls.add_telemetry_counters",
+ Arguments: args,
+ }, nil
+}
+
func NewApplyFixCommand(title string, a0 ApplyFixArgs) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {
@@ -380,6 +404,18 @@ func NewListKnownPackagesCommand(title string, a0 URIArg) (protocol.Command, err
}, nil
}
+func NewMaybePromptForTelemetryCommand(title string) (protocol.Command, error) {
+ args, err := MarshalArgs()
+ if err != nil {
+ return protocol.Command{}, err
+ }
+ return protocol.Command{
+ Title: title,
+ Command: "gopls.maybe_prompt_for_telemetry",
+ Arguments: args,
+ }, nil
+}
+
func NewMemStatsCommand(title string) (protocol.Command, error) {
args, err := MarshalArgs()
if err != nil {
diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go
index ef9d1fb5a96..c3bd921fcf1 100644
--- a/gopls/internal/lsp/command/interface.go
+++ b/gopls/internal/lsp/command/interface.go
@@ -17,8 +17,8 @@ package command
import (
"context"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/vulncheck"
)
// Interface defines the interface gopls exposes for the
@@ -37,6 +37,7 @@ type Interface interface {
//
// Applies a fix to a region of source code.
ApplyFix(context.Context, ApplyFixArgs) error
+
// Test: Run test(s) (legacy)
//
// Runs `go test` for a specific set of test or benchmark functions.
@@ -160,7 +161,7 @@ type Interface interface {
// runner.
StopProfile(context.Context, StopProfileArgs) (StopProfileResult, error)
- // RunGovulncheck: Run govulncheck.
+ // RunGovulncheck: Run vulncheck.
//
// Run vulnerability check (`govulncheck`).
RunGovulncheck(context.Context, VulncheckArgs) (RunVulncheckResult, error)
@@ -168,7 +169,7 @@ type Interface interface {
// FetchVulncheckResult: Get known vulncheck result
//
// Fetch the result of latest vulnerability check (`govulncheck`).
- FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*govulncheck.Result, error)
+ FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error)
// MemStats: fetch memory statistics
//
@@ -189,6 +190,17 @@ type Interface interface {
// RunGoWorkCommand: run `go work [args...]`, and apply the resulting go.work
// edits to the current go.work file.
RunGoWorkCommand(context.Context, RunGoWorkArgs) error
+
+ // AddTelemetryCounters: update the given telemetry counters.
+ //
+ // Gopls will prepend "fwd/" to all the counters updated using this command
+ // to avoid conflicts with other counters gopls collects.
+ AddTelemetryCounters(context.Context, AddTelemetryCountersArgs) error
+
+ // MaybePromptForTelemetry: checks for the right conditions, and then prompts
+ // the user to ask if they want to enable Go telemetry uploading. If the user
+ // responds 'Yes', the telemetry mode is set to "on".
+ MaybePromptForTelemetry(context.Context) error
}
type RunTestsArgs struct {
@@ -499,3 +511,11 @@ type RunGoWorkArgs struct {
InitFirst bool // Whether to run `go work init` first
Args []string // Args to pass to `go work`
}
+
+// AddTelemetryCountersArgs holds the arguments to the AddCounters command
+// that updates the telemetry counters.
+type AddTelemetryCountersArgs struct {
+ // Names and Values must have the same length.
+ Names []string // Name of counters.
+ Values []int64 // Values added to the corresponding counters. Must be non-negative.
+}
diff --git a/gopls/internal/lsp/command/interface_test.go b/gopls/internal/lsp/command/interface_test.go
index 2eb6f9ac819..f81a2aa22fd 100644
--- a/gopls/internal/lsp/command/interface_test.go
+++ b/gopls/internal/lsp/command/interface_test.go
@@ -5,7 +5,7 @@
package command_test
import (
- "io/ioutil"
+ "os"
"testing"
"github.com/google/go-cmp/cmp"
@@ -17,7 +17,7 @@ func TestGenerated(t *testing.T) {
testenv.NeedsGoPackages(t)
testenv.NeedsLocalXTools(t)
- onDisk, err := ioutil.ReadFile("command_gen.go")
+ onDisk, err := os.ReadFile("command_gen.go")
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go
index 209f26be3cb..66b3a3945bf 100644
--- a/gopls/internal/lsp/completion.go
+++ b/gopls/internal/lsp/completion.go
@@ -14,11 +14,17 @@ import (
"golang.org/x/tools/gopls/internal/lsp/source/completion"
"golang.org/x/tools/gopls/internal/lsp/template"
"golang.org/x/tools/gopls/internal/lsp/work"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
-func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
+func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (_ *protocol.CompletionList, rerr error) {
+ recordLatency := telemetry.StartLatencyTimer("completion")
+ defer func() {
+ recordLatency(ctx, rerr)
+ }()
+
ctx, done := event.Start(ctx, "lsp.Server.completion", tag.URI.Of(params.TextDocument.URI))
defer done()
diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go
index 1fc7304fc43..06a6a09aa1a 100644
--- a/gopls/internal/lsp/completion_test.go
+++ b/gopls/internal/lsp/completion_test.go
@@ -49,15 +49,6 @@ func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.C
}
}
-func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
- got := r.callCompletion(t, src, func(opts *source.Options) {})
- got = tests.FilterBuiltins(src, got)
- want := expected(t, test, items)
- if diff := tests.CheckCompletionOrder(want, got, false); diff != "" {
- t.Errorf("%s", diff)
- }
-}
-
func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
got := r.callCompletion(t, src, func(opts *source.Options) {
opts.DeepCompletion = true
@@ -144,20 +135,8 @@ func expected(t *testing.T, test tests.Completion, items tests.CompletionItems)
func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*source.Options)) []protocol.CompletionItem {
t.Helper()
-
- view, err := r.server.session.ViewOf(src.URI())
- if err != nil {
- t.Fatal(err)
- }
- original := view.Options()
- modified := view.Options().Clone()
- options(modified)
- view, err = r.server.session.SetViewOptions(r.ctx, view, modified)
- if err != nil {
- t.Error(err)
- return nil
- }
- defer r.server.session.SetViewOptions(r.ctx, view, original)
+ cleanup := r.toggleOptions(t, src.URI(), options)
+ defer cleanup()
list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
@@ -175,3 +154,20 @@ func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*sourc
}
return list.Items
}
+
+func (r *runner) toggleOptions(t *testing.T, uri span.URI, options func(*source.Options)) (reset func()) {
+ view, err := r.server.session.ViewOf(uri)
+ if err != nil {
+ t.Fatal(err)
+ }
+ folder := view.Folder()
+
+ modified := r.server.Options().Clone()
+ options(modified)
+ if err = r.server.session.SetFolderOptions(r.ctx, folder, modified); err != nil {
+ t.Fatal(err)
+ }
+ return func() {
+ r.server.session.SetFolderOptions(r.ctx, folder, r.server.Options())
+ }
+}
diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go
index 246a943e458..8aa2938c228 100644
--- a/gopls/internal/lsp/debug/serve.go
+++ b/gopls/internal/lsp/debug/serve.go
@@ -701,9 +701,10 @@ Unknown page
}
return s
},
- "options": func(s *cache.Session) []sessionOption {
- return showOptions(s.Options())
- },
+ // TODO(rfindley): re-enable option inspection.
+ // "options": func(s *cache.Session) []sessionOption {
+ // return showOptions(s.Options())
+ // },
})
var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
@@ -837,12 +838,6 @@ From: {{template "cachelink" .Cache.ID}}
{{.FileIdentity.URI}}
{{end}}
-Options
-{{range options .}}
-{{.Name}} {{.Type}}
-default: {{.Default}}
-{{if ne .Default .Current}}current: {{.Current}}
{{end}}
-{{end}}
{{end}}
`))
@@ -851,8 +846,6 @@ var ViewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "body"}}
Name: {{.Name}}
Folder: {{.Folder}}
-Environment
-{{range .Options.Env}}- {{.}}
{{end}}
{{end}}
`))
diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go
index fb691ef9d16..892e48d6377 100644
--- a/gopls/internal/lsp/definition.go
+++ b/gopls/internal/lsp/definition.go
@@ -6,17 +6,22 @@ package lsp
import (
"context"
- "errors"
"fmt"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/template"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
-func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) {
+func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) (_ []protocol.Location, rerr error) {
+ recordLatency := telemetry.StartLatencyTimer("definition")
+ defer func() {
+ recordLatency(ctx, rerr)
+ }()
+
ctx, done := event.Start(ctx, "lsp.Server.definition", tag.URI.Of(params.TextDocument.URI))
defer done()
@@ -30,11 +35,6 @@ func (s *Server) definition(ctx context.Context, params *protocol.DefinitionPara
case source.Tmpl:
return template.Definition(snapshot, fh, params.Position)
case source.Go:
- // Partial support for jumping from linkname directive (position at 2nd argument).
- locations, err := source.LinknameDefinition(ctx, snapshot, fh, params.Position)
- if !errors.Is(err, source.ErrNoLinkname) {
- return locations, err
- }
return source.Definition(ctx, snapshot, fh, params.Position)
default:
return nil, fmt.Errorf("can't find definitions for file type %s", kind)
diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go
index 2ae50586416..4fbfd0acec3 100644
--- a/gopls/internal/lsp/diagnostics.go
+++ b/gopls/internal/lsp/diagnostics.go
@@ -573,7 +573,7 @@ func (s *Server) diagnosePkgs(ctx context.Context, snapshot source.Snapshot, toD
fh := snapshot.FindFile(uri)
// Don't publish gc details for unsaved buffers, since the underlying
// logic operates on the file on disk.
- if fh == nil || !fh.Saved() {
+ if fh == nil || !fh.SameContentsOnDisk() {
continue
}
s.storeDiagnostics(snapshot, uri, gcDetailsSource, diags, true)
diff --git a/gopls/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go
index 1c073727109..cedd5884386 100644
--- a/gopls/internal/lsp/fake/client.go
+++ b/gopls/internal/lsp/fake/client.go
@@ -63,10 +63,10 @@ func (c *Client) ShowMessageRequest(ctx context.Context, params *protocol.ShowMe
return nil, err
}
}
- if len(params.Actions) == 0 || len(params.Actions) > 1 {
- return nil, fmt.Errorf("fake editor cannot handle multiple action items")
+ if c.editor.config.MessageResponder != nil {
+ return c.editor.config.MessageResponder(params)
}
- return ¶ms.Actions[0], nil
+ return nil, nil // don't choose, which is effectively dismissing the message
}
func (c *Client) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error {
diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go
index b6e507c291c..ccee51e6867 100644
--- a/gopls/internal/lsp/fake/editor.go
+++ b/gopls/internal/lsp/fake/editor.go
@@ -7,6 +7,7 @@ package fake
import (
"bytes"
"context"
+ "encoding/json"
"errors"
"fmt"
"os"
@@ -55,7 +56,7 @@ type Editor struct {
// CallCounts tracks the number of protocol notifications of different types.
type CallCounts struct {
- DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64
+ DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose, DidChangeConfiguration uint64
}
// buffer holds information about an open buffer in the editor.
@@ -109,6 +110,17 @@ type EditorConfig struct {
// Settings holds user-provided configuration for the LSP server.
Settings map[string]interface{}
+
+ // CapabilitiesJSON holds JSON client capabilities to overlay over the
+ // editor's default client capabilities.
+ //
+ // Specifically, this JSON string will be unmarshalled into the editor's
+ // client capabilities struct, before sending to the server.
+ CapabilitiesJSON []byte
+
+ // If non-nil, MessageResponder is used to respond to ShowMessageRequest
+ // messages.
+ MessageResponder func(params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error)
}
// NewEditor creates a new Editor.
@@ -223,9 +235,9 @@ func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{}
// asynchronous operations being completed (such as diagnosing a snapshot).
"verboseWorkDoneProgress": true,
- // Set a generous completion budget, so that tests don't flake because
+ // Set an unlimited completion budget, so that tests don't flake because
// completions are too slow.
- "completionBudget": "10s",
+ "completionBudget": "0s",
}
for k, v := range config.Settings {
@@ -249,15 +261,13 @@ func (e *Editor) initialize(ctx context.Context) error {
}
params.InitializationOptions = makeSettings(e.sandbox, config)
params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
+
+ // Set various client capabilities that are sought by gopls.
params.Capabilities.Workspace.Configuration = true // support workspace/configuration
params.Capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress
-
- // TODO(rfindley): set client capabilities (note from the future: why?)
-
params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
params.Capabilities.TextDocument.SemanticTokens.Requests.Full.Value = true
- // copied from lsp/semantic.go to avoid import cycle in tests
params.Capabilities.TextDocument.SemanticTokens.TokenTypes = []string{
"namespace", "type", "class", "enum", "interface",
"struct", "typeParameter", "parameter", "variable", "property", "enumMember",
@@ -268,14 +278,11 @@ func (e *Editor) initialize(ctx context.Context) error {
"declaration", "definition", "readonly", "static",
"deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary",
}
-
// The LSP tests have historically enabled this flag,
// but really we should test both ways for older editors.
params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true
-
// Glob pattern watching is enabled.
params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
-
// "rename" operations are used for package renaming.
//
// TODO(rfindley): add support for other resource operations (create, delete, ...)
@@ -284,6 +291,12 @@ func (e *Editor) initialize(ctx context.Context) error {
"rename",
},
}
+ // Apply capabilities overlay.
+ if config.CapabilitiesJSON != nil {
+ if err := json.Unmarshal(config.CapabilitiesJSON, ¶ms.Capabilities); err != nil {
+ return fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err)
+ }
+ }
trace := protocol.TraceValues("messages")
params.Trace = &trace
@@ -785,10 +798,7 @@ func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty
// GoToDefinition jumps to the definition of the symbol at the given position
// in an open buffer. It returns the location of the resulting jump.
-//
-// TODO(rfindley): rename to "Definition", to be consistent with LSP
-// terminology.
-func (e *Editor) GoToDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) {
+func (e *Editor) Definition(ctx context.Context, loc protocol.Location) (protocol.Location, error) {
if err := e.checkBufferLocation(loc); err != nil {
return protocol.Location{}, err
}
@@ -803,9 +813,9 @@ func (e *Editor) GoToDefinition(ctx context.Context, loc protocol.Location) (pro
return e.extractFirstLocation(ctx, resp)
}
-// GoToTypeDefinition jumps to the type definition of the symbol at the given location
-// in an open buffer.
-func (e *Editor) GoToTypeDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) {
+// TypeDefinition jumps to the type definition of the symbol at the given
+// location in an open buffer.
+func (e *Editor) TypeDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) {
if err := e.checkBufferLocation(loc); err != nil {
return protocol.Location{}, err
}
@@ -1358,6 +1368,9 @@ func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig
if err := e.Server.DidChangeConfiguration(ctx, ¶ms); err != nil {
return err
}
+ e.callsMu.Lock()
+ e.calls.DidChangeConfiguration++
+ e.callsMu.Unlock()
}
return nil
}
diff --git a/gopls/internal/lsp/fake/sandbox.go b/gopls/internal/lsp/fake/sandbox.go
index 41188af30fe..8218bc15bc5 100644
--- a/gopls/internal/lsp/fake/sandbox.go
+++ b/gopls/internal/lsp/fake/sandbox.go
@@ -8,7 +8,6 @@ import (
"context"
"errors"
"fmt"
- "io/ioutil"
"os"
"path/filepath"
"strings"
@@ -92,7 +91,7 @@ func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
rootDir := config.RootDir
if rootDir == "" {
- rootDir, err = ioutil.TempDir(config.RootDir, "gopls-sandbox-")
+ rootDir, err = os.MkdirTemp(config.RootDir, "gopls-sandbox-")
if err != nil {
return nil, fmt.Errorf("creating temporary workdir: %v", err)
}
@@ -150,7 +149,7 @@ func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
// is the responsibility of the caller to call os.RemoveAll on the returned
// file path when it is no longer needed.
func Tempdir(files map[string][]byte) (string, error) {
- dir, err := ioutil.TempDir("", "gopls-tempdir-")
+ dir, err := os.MkdirTemp("", "gopls-tempdir-")
if err != nil {
return "", err
}
diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go
index d5e8eb2af22..462d54821f1 100644
--- a/gopls/internal/lsp/fake/workdir.go
+++ b/gopls/internal/lsp/fake/workdir.go
@@ -10,7 +10,6 @@ import (
"crypto/sha256"
"fmt"
"io/fs"
- "io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -57,7 +56,7 @@ func writeFileData(path string, content []byte, rel RelativeTo) error {
}
backoff := 1 * time.Millisecond
for {
- err := ioutil.WriteFile(fp, []byte(content), 0644)
+ err := os.WriteFile(fp, []byte(content), 0644)
if err != nil {
// This lock file violation is not handled by the robustio package, as it
// indicates a real race condition that could be avoided.
@@ -160,7 +159,7 @@ func toURI(fp string) protocol.DocumentURI {
func (w *Workdir) ReadFile(path string) ([]byte, error) {
backoff := 1 * time.Millisecond
for {
- b, err := ioutil.ReadFile(w.AbsPath(path))
+ b, err := os.ReadFile(w.AbsPath(path))
if err != nil {
if runtime.GOOS == "plan9" && strings.HasSuffix(err.Error(), " exclusive use file already open") {
// Plan 9 enforces exclusive access to locked files.
@@ -370,7 +369,7 @@ func (w *Workdir) pollFiles() ([]protocol.FileEvent, error) {
// must read the file contents.
id := fileID{mtime: info.ModTime()}
if time.Since(info.ModTime()) < 2*time.Second {
- data, err := ioutil.ReadFile(fp)
+ data, err := os.ReadFile(fp)
if err != nil {
return err
}
@@ -397,7 +396,7 @@ func (w *Workdir) pollFiles() ([]protocol.FileEvent, error) {
// In this case, read the content to check whether the file actually
// changed.
if oldID.mtime.Equal(id.mtime) && oldID.hash != "" && id.hash == "" {
- data, err := ioutil.ReadFile(fp)
+ data, err := os.ReadFile(fp)
if err != nil {
return err
}
diff --git a/gopls/internal/lsp/fake/workdir_test.go b/gopls/internal/lsp/fake/workdir_test.go
index fe89fa72dc3..b45b5339991 100644
--- a/gopls/internal/lsp/fake/workdir_test.go
+++ b/gopls/internal/lsp/fake/workdir_test.go
@@ -6,7 +6,6 @@ package fake
import (
"context"
- "io/ioutil"
"os"
"sync"
"testing"
@@ -32,7 +31,7 @@ Hello World!
func newWorkdir(t *testing.T, txt string) (*Workdir, *eventBuffer, func()) {
t.Helper()
- tmpdir, err := ioutil.TempDir("", "goplstest-workdir-")
+ tmpdir, err := os.MkdirTemp("", "goplstest-workdir-")
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go
index 9c10d9377a1..7d9ed35325e 100644
--- a/gopls/internal/lsp/general.go
+++ b/gopls/internal/lsp/general.go
@@ -57,8 +57,10 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitializ
}
s.progress.SetSupportsWorkDoneProgress(params.Capabilities.Window.WorkDoneProgress)
- options := s.session.Options()
- defer func() { s.session.SetOptions(options) }()
+ options := s.Options().Clone()
+ // TODO(rfindley): remove the error return from handleOptionResults, and
+ // eliminate this defer.
+ defer func() { s.SetOptions(options) }()
if err := s.handleOptionResults(ctx, source.SetOptions(options, params.InitializationOptions)); err != nil {
return nil, err
@@ -170,8 +172,8 @@ See https://github.com/golang/go/issues/45732 for more information.`,
Range: &protocol.Or_SemanticTokensOptions_range{Value: true},
Full: &protocol.Or_SemanticTokensOptions_full{Value: true},
Legend: protocol.SemanticTokensLegend{
- TokenTypes: nonNilSliceString(s.session.Options().SemanticTypes),
- TokenModifiers: nonNilSliceString(s.session.Options().SemanticMods),
+ TokenTypes: nonNilSliceString(s.Options().SemanticTypes),
+ TokenModifiers: nonNilSliceString(s.Options().SemanticMods),
},
},
SignatureHelpProvider: &protocol.SignatureHelpOptions{
@@ -215,9 +217,7 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa
}
s.notifications = nil
- options := s.session.Options()
- defer func() { s.session.SetOptions(options) }()
-
+ options := s.Options()
if err := s.addFolders(ctx, s.pendingFolders); err != nil {
return err
}
@@ -238,6 +238,11 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa
return err
}
}
+
+ // Ask (maybe) about enabling telemetry. Do this asynchronously, as it's OK
+ // for users to ignore or dismiss the question.
+ go s.maybePromptForTelemetry(ctx, options.TelemetryPrompt)
+
return nil
}
@@ -348,7 +353,7 @@ func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol
viewErrors := make(map[span.URI]error)
var ndiagnose sync.WaitGroup // number of unfinished diagnose calls
- if s.session.Options().VerboseWorkDoneProgress {
+ if s.Options().VerboseWorkDoneProgress {
work := s.progress.Start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil)
defer func() {
go func() {
@@ -475,7 +480,7 @@ func equalURISet(m1, m2 map[string]struct{}) bool {
// registrations to the client and updates s.watchedDirectories.
// The caller must not subsequently mutate patterns.
func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error {
- if !s.session.Options().DynamicWatchedFilesSupported {
+ if !s.Options().DynamicWatchedFilesSupported {
return nil
}
s.watchedGlobPatterns = patterns
@@ -503,9 +508,27 @@ func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns
return nil
}
-func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, o *source.Options) error {
- if !s.session.Options().ConfigurationSupported {
- return nil
+// Options returns the current server options.
+//
+// The caller must not modify the result.
+func (s *Server) Options() *source.Options {
+ s.optionsMu.Lock()
+ defer s.optionsMu.Unlock()
+ return s.options
+}
+
+// SetOptions sets the current server options.
+//
+// The caller must not subsequently modify the options.
+func (s *Server) SetOptions(opts *source.Options) {
+ s.optionsMu.Lock()
+ defer s.optionsMu.Unlock()
+ s.options = opts
+}
+
+func (s *Server) fetchFolderOptions(ctx context.Context, folder span.URI) (*source.Options, error) {
+ if opts := s.Options(); !opts.ConfigurationSupported {
+ return opts, nil
}
configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{
Items: []protocol.ConfigurationItem{{
@@ -515,14 +538,16 @@ func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI,
},
)
if err != nil {
- return fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err)
+ return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err)
}
+
+ folderOpts := s.Options().Clone()
for _, config := range configs {
- if err := s.handleOptionResults(ctx, source.SetOptions(o, config)); err != nil {
- return err
+ if err := s.handleOptionResults(ctx, source.SetOptions(folderOpts, config)); err != nil {
+ return nil, err
}
}
- return nil
+ return folderOpts, nil
}
func (s *Server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMessageParams) error {
@@ -603,7 +628,7 @@ func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI
release()
return nil, nil, false, func() {}, err
}
- if expectKind != source.UnknownKind && view.FileKind(fh) != expectKind {
+ if expectKind != source.UnknownKind && snapshot.FileKind(fh) != expectKind {
// Wrong kind of file. Nothing to do.
release()
return nil, nil, false, func() {}, nil
diff --git a/gopls/internal/lsp/hover.go b/gopls/internal/lsp/hover.go
index eef59920ae4..263a1c8ac72 100644
--- a/gopls/internal/lsp/hover.go
+++ b/gopls/internal/lsp/hover.go
@@ -12,11 +12,17 @@ import (
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/template"
"golang.org/x/tools/gopls/internal/lsp/work"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
-func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
+func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (_ *protocol.Hover, rerr error) {
+ recordLatency := telemetry.StartLatencyTimer("hover")
+ defer func() {
+ recordLatency(ctx, rerr)
+ }()
+
ctx, done := event.Start(ctx, "lsp.Server.hover", tag.URI.Of(params.TextDocument.URI))
defer done()
diff --git a/gopls/internal/lsp/implementation.go b/gopls/internal/lsp/implementation.go
index e231d96bccb..bc527b3b58a 100644
--- a/gopls/internal/lsp/implementation.go
+++ b/gopls/internal/lsp/implementation.go
@@ -9,11 +9,17 @@ import (
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
-func (s *Server) implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) {
+func (s *Server) implementation(ctx context.Context, params *protocol.ImplementationParams) (_ []protocol.Location, rerr error) {
+ recordLatency := telemetry.StartLatencyTimer("implementation")
+ defer func() {
+ recordLatency(ctx, rerr)
+ }()
+
ctx, done := event.Start(ctx, "lsp.Server.implementation", tag.URI.Of(params.TextDocument.URI))
defer done()
diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go
index d7e1e33a8e0..0bda4e6a6d0 100644
--- a/gopls/internal/lsp/lsp_test.go
+++ b/gopls/internal/lsp/lsp_test.go
@@ -14,7 +14,6 @@ import (
"strings"
"testing"
- "github.com/google/go-cmp/cmp"
"golang.org/x/tools/gopls/internal/bug"
"golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/lsp/command"
@@ -35,8 +34,8 @@ func TestMain(m *testing.M) {
}
// TestLSP runs the marker tests in files beneath testdata/ using
-// implementations of each of the marker operations (e.g. @codelens) that
-// make LSP RPCs (e.g. textDocument/codeLens) to a gopls server.
+// implementations of each of the marker operations that make LSP RPCs to a
+// gopls server.
func TestLSP(t *testing.T) {
tests.RunTests(t, "testdata", true, testLSP)
}
@@ -51,10 +50,8 @@ func testLSP(t *testing.T, datum *tests.Data) {
// instrumentation.
ctx = debug.WithInstance(ctx, "", "off")
- session := cache.NewSession(ctx, cache.New(nil), nil)
- options := source.DefaultOptions().Clone()
- tests.DefaultOptions(options)
- session.SetOptions(options)
+ session := cache.NewSession(ctx, cache.New(nil))
+ options := source.DefaultOptions(tests.DefaultOptions)
options.SetEnvSlice(datum.Config.Env)
view, snapshot, release, err := session.NewView(ctx, datum.Config.Dir, span.URIFromPath(datum.Config.Dir), options)
if err != nil {
@@ -63,14 +60,6 @@ func testLSP(t *testing.T, datum *tests.Data) {
defer session.RemoveView(view)
- // Enable type error analyses for tests.
- // TODO(golang/go#38212): Delete this once they are enabled by default.
- tests.EnableAllAnalyzers(options)
- session.SetViewOptions(ctx, view, options)
-
- // Enable all inlay hints for tests.
- tests.EnableAllInlayHints(options)
-
// Only run the -modfile specific tests in module mode with Go 1.14 or above.
datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14
release()
@@ -121,7 +110,7 @@ func testLSP(t *testing.T, datum *tests.Data) {
editRecv: make(chan map[span.URI][]byte, 1),
}
- r.server = NewServer(session, testClient{runner: r})
+ r.server = NewServer(session, testClient{runner: r}, options)
tests.Run(t, r, datum)
}
@@ -220,169 +209,6 @@ func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests
}
}
-func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
- if !strings.HasSuffix(uri.Filename(), "go.mod") {
- return
- }
- got, err := r.server.codeLens(r.ctx, &protocol.CodeLensParams{
- TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.DocumentURI(uri),
- },
- })
- if err != nil {
- t.Fatal(err)
- }
- if diff := tests.DiffCodeLens(uri, want, got); diff != "" {
- t.Errorf("%s: %s", uri, diff)
- }
-}
-
-func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) {
- // Get the diagnostics for this view if we have not done it before.
- v := r.server.session.ViewByName(r.data.Config.Dir)
- r.collectDiagnostics(v)
- tests.CompareDiagnostics(t, uri, want, r.diagnostics[uri])
-}
-
-func (r *runner) FoldingRanges(t *testing.T, spn span.Span) {
- uri := spn.URI()
- view, err := r.server.session.ViewOf(uri)
- if err != nil {
- t.Fatal(err)
- }
- original := view.Options()
- modified := original
- defer r.server.session.SetViewOptions(r.ctx, view, original)
-
- for _, test := range []struct {
- lineFoldingOnly bool
- prefix string
- }{
- {false, "foldingRange"},
- {true, "foldingRange-lineFolding"},
- } {
- modified.LineFoldingOnly = test.lineFoldingOnly
- view, err = r.server.session.SetViewOptions(r.ctx, view, modified)
- if err != nil {
- t.Error(err)
- continue
- }
- ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{
- TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.URIFromSpanURI(uri),
- },
- })
- if err != nil {
- t.Error(err)
- continue
- }
- r.foldingRanges(t, test.prefix, uri, ranges)
- }
-}
-
-func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) {
- m, err := r.data.Mapper(uri)
- if err != nil {
- t.Fatal(err)
- }
- // Fold all ranges.
- nonOverlapping := nonOverlappingRanges(ranges)
- for i, rngs := range nonOverlapping {
- got, err := foldRanges(m, string(m.Content), rngs)
- if err != nil {
- t.Error(err)
- continue
- }
- tag := fmt.Sprintf("%s-%d", prefix, i)
- want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) {
- return []byte(got), nil
- }))
-
- if want != got {
- t.Errorf("%s: foldingRanges failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got)
- }
- }
-
- // Filter by kind.
- kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment}
- for _, kind := range kinds {
- var kindOnly []protocol.FoldingRange
- for _, fRng := range ranges {
- if fRng.Kind == string(kind) {
- kindOnly = append(kindOnly, fRng)
- }
- }
-
- nonOverlapping := nonOverlappingRanges(kindOnly)
- for i, rngs := range nonOverlapping {
- got, err := foldRanges(m, string(m.Content), rngs)
- if err != nil {
- t.Error(err)
- continue
- }
- tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i)
- want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) {
- return []byte(got), nil
- }))
-
- if want != got {
- t.Errorf("%s: foldingRanges failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got)
- }
- }
-
- }
-}
-
-func nonOverlappingRanges(ranges []protocol.FoldingRange) (res [][]protocol.FoldingRange) {
- for _, fRng := range ranges {
- setNum := len(res)
- for i := 0; i < len(res); i++ {
- canInsert := true
- for _, rng := range res[i] {
- if conflict(rng, fRng) {
- canInsert = false
- break
- }
- }
- if canInsert {
- setNum = i
- break
- }
- }
- if setNum == len(res) {
- res = append(res, []protocol.FoldingRange{})
- }
- res[setNum] = append(res[setNum], fRng)
- }
- return res
-}
-
-func conflict(a, b protocol.FoldingRange) bool {
- // a start position is <= b start positions
- return (a.StartLine < b.StartLine || (a.StartLine == b.StartLine && a.StartCharacter <= b.StartCharacter)) &&
- (a.EndLine > b.StartLine || (a.EndLine == b.StartLine && a.EndCharacter > b.StartCharacter))
-}
-
-func foldRanges(m *protocol.Mapper, contents string, ranges []protocol.FoldingRange) (string, error) {
- foldedText := "<>"
- res := contents
- // Apply the edits from the end of the file forward
- // to preserve the offsets
- // TODO(adonovan): factor to use diff.ApplyEdits, which validates the input.
- for i := len(ranges) - 1; i >= 0; i-- {
- r := ranges[i]
- start, end, err := m.RangeOffsets(protocol.Range{
- Start: protocol.Position{Line: r.StartLine, Character: r.StartCharacter},
- End: protocol.Position{Line: r.EndLine, Character: r.EndCharacter},
- })
- if err != nil {
- return "", err
- }
- res = res[:start] + foldedText + res[end:]
- }
- return res, nil
-}
-
func (r *runner) SemanticTokens(t *testing.T, spn span.Span) {
uri := spn.URI()
filename := uri.Filename()
@@ -566,129 +392,6 @@ func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span)
}
}
-// TODO(rfindley): This handler needs more work. The output is still a bit hard
-// to read (range diffs do not format nicely), and it is too entangled with hover.
-func (r *runner) Definition(t *testing.T, _ span.Span, d tests.Definition) {
- sm, err := r.data.Mapper(d.Src.URI())
- if err != nil {
- t.Fatal(err)
- }
- loc, err := sm.SpanLocation(d.Src)
- if err != nil {
- t.Fatalf("failed for %v: %v", d.Src, err)
- }
- tdpp := protocol.LocationTextDocumentPositionParams(loc)
- var got []protocol.Location
- var hover *protocol.Hover
- if d.IsType {
- params := &protocol.TypeDefinitionParams{
- TextDocumentPositionParams: tdpp,
- }
- got, err = r.server.TypeDefinition(r.ctx, params)
- } else {
- params := &protocol.DefinitionParams{
- TextDocumentPositionParams: tdpp,
- }
- got, err = r.server.Definition(r.ctx, params)
- if err != nil {
- t.Fatalf("failed for %v: %+v", d.Src, err)
- }
- v := &protocol.HoverParams{
- TextDocumentPositionParams: tdpp,
- }
- hover, err = r.server.Hover(r.ctx, v)
- }
- if err != nil {
- t.Fatalf("failed for %v: %v", d.Src, err)
- }
- dm, err := r.data.Mapper(d.Def.URI())
- if err != nil {
- t.Fatal(err)
- }
- def, err := dm.SpanLocation(d.Def)
- if err != nil {
- t.Fatal(err)
- }
- if !d.OnlyHover {
- want := []protocol.Location{def}
- if diff := cmp.Diff(want, got); diff != "" {
- t.Fatalf("Definition(%s) mismatch (-want +got):\n%s", d.Src, diff)
- }
- }
- didSomething := false
- if hover != nil {
- didSomething = true
- tag := fmt.Sprintf("%s-hoverdef", d.Name)
- want := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) {
- return []byte(hover.Contents.Value), nil
- }))
- got := hover.Contents.Value
- if diff := tests.DiffMarkdown(want, got); diff != "" {
- t.Errorf("%s: markdown mismatch:\n%s", d.Src, diff)
- }
- }
- if !d.OnlyHover {
- didSomething = true
- locURI := got[0].URI.SpanURI()
- lm, err := r.data.Mapper(locURI)
- if err != nil {
- t.Fatal(err)
- }
- if def, err := lm.LocationSpan(got[0]); err != nil {
- t.Fatalf("failed for %v: %v", got[0], err)
- } else if def != d.Def {
- t.Errorf("for %v got %v want %v", d.Src, def, d.Def)
- }
- }
- if !didSomething {
- t.Errorf("no tests ran for %s", d.Src.URI())
- }
-}
-
-func (r *runner) Highlight(t *testing.T, src span.Span, spans []span.Span) {
- m, err := r.data.Mapper(src.URI())
- if err != nil {
- t.Fatal(err)
- }
- loc, err := m.SpanLocation(src)
- if err != nil {
- t.Fatal(err)
- }
- params := &protocol.DocumentHighlightParams{
- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc),
- }
- highlights, err := r.server.DocumentHighlight(r.ctx, params)
- if err != nil {
- t.Fatalf("DocumentHighlight(%v) failed: %v", params, err)
- }
- var got []protocol.Range
- for _, h := range highlights {
- got = append(got, h.Range)
- }
-
- var want []protocol.Range
- for _, s := range spans {
- rng, err := m.SpanRange(s)
- if err != nil {
- t.Fatalf("Mapper.SpanRange(%v) failed: %v", s, err)
- }
- want = append(want, rng)
- }
-
- sortRanges := func(s []protocol.Range) {
- sort.Slice(s, func(i, j int) bool {
- return protocol.CompareRange(s[i], s[j]) < 0
- })
- }
-
- sortRanges(got)
- sortRanges(want)
-
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", src, diff)
- }
-}
-
func (r *runner) InlayHints(t *testing.T, spn span.Span) {
uri := spn.URI()
filename := uri.Filename()
diff --git a/gopls/internal/lsp/lsprpc/lsprpc.go b/gopls/internal/lsp/lsprpc/lsprpc.go
index 6b02cf5aa65..7dc709291fb 100644
--- a/gopls/internal/lsp/lsprpc/lsprpc.go
+++ b/gopls/internal/lsp/lsprpc/lsprpc.go
@@ -56,10 +56,11 @@ func NewStreamServer(cache *cache.Cache, daemon bool, optionsFunc func(*source.O
func (s *StreamServer) Binder() *ServerBinder {
newServer := func(ctx context.Context, client protocol.ClientCloser) protocol.Server {
- session := cache.NewSession(ctx, s.cache, s.optionsOverrides)
+ session := cache.NewSession(ctx, s.cache)
server := s.serverForTest
if server == nil {
- server = lsp.NewServer(session, client)
+ options := source.DefaultOptions(s.optionsOverrides)
+ server = lsp.NewServer(session, client, options)
if instance := debug.GetInstance(ctx); instance != nil {
instance.AddService(server, session)
}
@@ -73,10 +74,11 @@ func (s *StreamServer) Binder() *ServerBinder {
// incoming streams using a new lsp server.
func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error {
client := protocol.ClientDispatcher(conn)
- session := cache.NewSession(ctx, s.cache, s.optionsOverrides)
+ session := cache.NewSession(ctx, s.cache)
server := s.serverForTest
if server == nil {
- server = lsp.NewServer(session, client)
+ options := source.DefaultOptions(s.optionsOverrides)
+ server = lsp.NewServer(session, client, options)
if instance := debug.GetInstance(ctx); instance != nil {
instance.AddService(server, session)
}
diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go
index 43fc0a24481..9f901206988 100644
--- a/gopls/internal/lsp/mod/diagnostics.go
+++ b/gopls/internal/lsp/mod/diagnostics.go
@@ -17,13 +17,12 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
"golang.org/x/sync/errgroup"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/vulncheck/govulncheck"
"golang.org/x/tools/internal/event"
- "golang.org/x/vuln/osv"
)
// Diagnostics returns diagnostics from parsing the modules in the workspace.
@@ -61,7 +60,6 @@ func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (ma
}
func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[span.URI][]*source.Diagnostic, error) {
-
g, ctx := errgroup.WithContext(ctx)
cpulimit := runtime.GOMAXPROCS(0)
g.SetLimit(cpulimit)
@@ -199,7 +197,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
}
diagSource = source.Vulncheck
}
- if vs == nil || len(vs.Vulns) == 0 {
+ if vs == nil || len(vs.Findings) == 0 {
return nil, nil
}
@@ -208,20 +206,17 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
// must not happen
return nil, err // TODO: bug report
}
- type modVuln struct {
- mod *govulncheck.Module
- vuln *govulncheck.Vuln
- }
- vulnsByModule := make(map[string][]modVuln)
- for _, vuln := range vs.Vulns {
- for _, mod := range vuln.Modules {
- vulnsByModule[mod.Path] = append(vulnsByModule[mod.Path], modVuln{mod, vuln})
+ vulnsByModule := make(map[string][]*govulncheck.Finding)
+
+ for _, finding := range vs.Findings {
+ if vuln, typ := foundVuln(finding); typ == vulnCalled || typ == vulnImported {
+ vulnsByModule[vuln.Module] = append(vulnsByModule[vuln.Module], finding)
}
}
-
for _, req := range pm.File.Require {
- vulns := vulnsByModule[req.Mod.Path]
- if len(vulns) == 0 {
+ mod := req.Mod.Path
+ findings := vulnsByModule[mod]
+ if len(findings) == 0 {
continue
}
// note: req.Syntax is the line corresponding to 'require', which means
@@ -239,10 +234,8 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
// others to 'info' level diagnostics.
// Fixes will include only the upgrades for warning level diagnostics.
var warningFixes, infoFixes []source.SuggestedFix
- var warning, info []string
- var relatedInfo []protocol.DiagnosticRelatedInformation
- for _, mv := range vulns {
- mod, vuln := mv.mod, mv.vuln
+ var warningSet, infoSet = map[string]bool{}, map[string]bool{}
+ for _, finding := range findings {
// It is possible that the source code was changed since the last
// govulncheck run and information in the `vulns` info is stale.
// For example, imagine that a user is in the middle of updating
@@ -259,33 +252,42 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
// version in the require statement is equal to or higher than the
// fixed version, skip generating a diagnostic about the vulnerability.
// Eventually, the user has to rerun govulncheck.
- if mod.FixedVersion != "" && semver.IsValid(req.Mod.Version) && semver.Compare(mod.FixedVersion, req.Mod.Version) <= 0 {
+ if finding.FixedVersion != "" && semver.IsValid(req.Mod.Version) && semver.Compare(finding.FixedVersion, req.Mod.Version) <= 0 {
continue
}
- if !vuln.IsCalled() {
- info = append(info, vuln.OSV.ID)
- } else {
- warning = append(warning, vuln.OSV.ID)
- relatedInfo = append(relatedInfo, listRelatedInfo(ctx, snapshot, vuln)...)
+ switch _, typ := foundVuln(finding); typ {
+ case vulnImported:
+ infoSet[finding.OSV] = true
+ case vulnCalled:
+ warningSet[finding.OSV] = true
}
// Upgrade to the exact version we offer the user, not the most recent.
- if fixedVersion := mod.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 {
+ if fixedVersion := finding.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 {
cmd, err := getUpgradeCodeAction(fh, req, fixedVersion)
if err != nil {
return nil, err // TODO: bug report
}
sf := source.SuggestedFixFromCommand(cmd, protocol.QuickFix)
- if !vuln.IsCalled() {
+ switch _, typ := foundVuln(finding); typ {
+ case vulnImported:
infoFixes = append(infoFixes, sf)
- } else {
+ case vulnCalled:
warningFixes = append(warningFixes, sf)
}
}
}
- if len(warning) == 0 && len(info) == 0 {
+ if len(warningSet) == 0 && len(infoSet) == 0 {
continue
}
+ // Remove affecting osvs from the non-affecting osv list if any.
+ if len(warningSet) > 0 {
+ for k := range infoSet {
+ if warningSet[k] {
+ delete(infoSet, k)
+ }
+ }
+ }
// Add an upgrade for module@latest.
// TODO(suzmue): verify if latest is the same as fixedVersion.
latest, err := getUpgradeCodeAction(fh, req, "latest")
@@ -299,11 +301,8 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
if len(infoFixes) > 0 {
infoFixes = append(infoFixes, sf)
}
-
- sort.Strings(warning)
- sort.Strings(info)
-
- if len(warning) > 0 {
+ if len(warningSet) > 0 {
+ warning := sortedKeys(warningSet)
warningFixes = append(warningFixes, suggestRunOrResetGovulncheck)
vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{
URI: fh.URI(),
@@ -312,10 +311,10 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
Source: diagSource,
Message: getVulnMessage(req.Mod.Path, warning, true, diagSource == source.Govulncheck),
SuggestedFixes: warningFixes,
- Related: relatedInfo,
})
}
- if len(info) > 0 {
+ if len(infoSet) > 0 {
+ info := sortedKeys(infoSet)
infoFixes = append(infoFixes, suggestRunOrResetGovulncheck)
vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{
URI: fh.URI(),
@@ -350,41 +349,44 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
return vulnDiagnostics, nil // TODO: bug report
}
- stdlib := stdlibVulns[0].mod.FoundVersion
- var warning, info []string
- var relatedInfo []protocol.DiagnosticRelatedInformation
- for _, mv := range stdlibVulns {
- vuln := mv.vuln
- stdlib = mv.mod.FoundVersion
- if !vuln.IsCalled() {
- info = append(info, vuln.OSV.ID)
- } else {
- warning = append(warning, vuln.OSV.ID)
- relatedInfo = append(relatedInfo, listRelatedInfo(ctx, snapshot, vuln)...)
+ var warningSet, infoSet = map[string]bool{}, map[string]bool{}
+ for _, finding := range stdlibVulns {
+ switch _, typ := foundVuln(finding); typ {
+ case vulnImported:
+ infoSet[finding.OSV] = true
+ case vulnCalled:
+ warningSet[finding.OSV] = true
}
}
- if len(warning) > 0 {
+ if len(warningSet) > 0 {
+ warning := sortedKeys(warningSet)
fixes := []source.SuggestedFix{suggestRunOrResetGovulncheck}
vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{
URI: fh.URI(),
Range: rng,
Severity: protocol.SeverityWarning,
Source: diagSource,
- Message: getVulnMessage(stdlib, warning, true, diagSource == source.Govulncheck),
+ Message: getVulnMessage("go", warning, true, diagSource == source.Govulncheck),
SuggestedFixes: fixes,
- Related: relatedInfo,
})
+
+ // remove affecting osvs from the non-affecting osv list if any.
+ for k := range infoSet {
+ if warningSet[k] {
+ delete(infoSet, k)
+ }
+ }
}
- if len(info) > 0 {
+ if len(infoSet) > 0 {
+ info := sortedKeys(infoSet)
fixes := []source.SuggestedFix{suggestRunOrResetGovulncheck}
vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{
URI: fh.URI(),
Range: rng,
Severity: protocol.SeverityInformation,
Source: diagSource,
- Message: getVulnMessage(stdlib, info, false, diagSource == source.Govulncheck),
+ Message: getVulnMessage("go", info, false, diagSource == source.Govulncheck),
SuggestedFixes: fixes,
- Related: relatedInfo,
})
}
}
@@ -392,6 +394,46 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
return vulnDiagnostics, nil
}
+type vulnFindingType int
+
+const (
+ vulnUnknown vulnFindingType = iota
+ vulnCalled
+ vulnImported
+ vulnRequired
+)
+
+// foundVuln returns the frame info describing discovered vulnerable symbol/package/module
+// and how this vulnerability affects the analyzed package or module.
+func foundVuln(finding *govulncheck.Finding) (*govulncheck.Frame, vulnFindingType) {
+ // finding.Trace is sorted from the imported vulnerable symbol to
+ // the entry point in the callstack.
+ // If Function is set, then Package must be set. Module will always be set.
+ // If Function is set it was found in the call graph, otherwise if Package is set
+ // it was found in the import graph, otherwise it was found in the require graph.
+ // See the documentation of govulncheck.Finding.
+ if len(finding.Trace) == 0 { // this shouldn't happen, but just in case...
+ return nil, vulnUnknown
+ }
+ vuln := finding.Trace[0]
+ if vuln.Package == "" {
+ return vuln, vulnRequired
+ }
+ if vuln.Function == "" {
+ return vuln, vulnImported
+ }
+ return vuln, vulnCalled
+}
+
+func sortedKeys(m map[string]bool) []string {
+ ret := make([]string, 0, len(m))
+ for k := range m {
+ ret = append(ret, k)
+ }
+ sort.Strings(ret)
+ return ret
+}
+
// suggestGovulncheckAction returns a code action that suggests either run govulncheck
// for more accurate investigation (if the present vulncheck diagnostics are based on
// analysis less accurate than govulncheck) or reset the existing govulncheck result
@@ -446,66 +488,12 @@ func getVulnMessage(mod string, vulns []string, used, fromGovulncheck bool) stri
return b.String()
}
-func listRelatedInfo(ctx context.Context, snapshot source.Snapshot, vuln *govulncheck.Vuln) []protocol.DiagnosticRelatedInformation {
- var ri []protocol.DiagnosticRelatedInformation
- for _, m := range vuln.Modules {
- for _, p := range m.Packages {
- for _, c := range p.CallStacks {
- if len(c.Frames) == 0 {
- continue
- }
- entry := c.Frames[0]
- pos := entry.Position
- if pos.Filename == "" {
- continue // token.Position Filename is an optional field.
- }
- uri := span.URIFromPath(pos.Filename)
- startPos := protocol.Position{
- Line: uint32(pos.Line) - 1,
- // We need to read the file contents to precisesly map
- // token.Position (pos) to the UTF16-based column offset
- // protocol.Position requires. That can be expensive.
- // We need this related info to just help users to open
- // the entry points of the callstack and once the file is
- // open, we will compute the precise location based on the
- // open file contents. So, use the beginning of the line
- // as the position here instead of precise UTF16-based
- // position computation.
- Character: 0,
- }
- ri = append(ri, protocol.DiagnosticRelatedInformation{
- Location: protocol.Location{
- URI: protocol.URIFromSpanURI(uri),
- Range: protocol.Range{
- Start: startPos,
- End: startPos,
- },
- },
- Message: fmt.Sprintf("[%v] %v -> %v.%v", vuln.OSV.ID, entry.Name(), p.Path, c.Symbol),
- })
- }
- }
- }
- return ri
-}
-
-func formatMessage(v *govulncheck.Vuln) string {
- details := []byte(v.OSV.Details)
- // Remove any new lines that are not preceded or followed by a new line.
- for i, r := range details {
- if r == '\n' && i > 0 && details[i-1] != '\n' && i+1 < len(details) && details[i+1] != '\n' {
- details[i] = ' '
- }
- }
- return strings.TrimSpace(strings.Replace(string(details), "\n\n", "\n\n ", -1))
-}
-
// href returns the url for the vulnerability information.
// Eventually we should retrieve the url embedded in the osv.Entry.
// While vuln.go.dev is under development, this always returns
// the page in pkg.go.dev.
-func href(vuln *osv.Entry) string {
- return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vuln.ID)
+func href(vulnID string) string {
+ return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vulnID)
}
func getUpgradeCodeAction(fh source.FileHandle, req *modfile.Require, version string) (protocol.Command, error) {
diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go
index bc754dcb911..b39993b2924 100644
--- a/gopls/internal/lsp/mod/hover.go
+++ b/gopls/internal/lsp/mod/hover.go
@@ -13,9 +13,11 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/vulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/govulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
"golang.org/x/tools/internal/event"
)
@@ -90,7 +92,7 @@ func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offse
}
fromGovulncheck = false
}
- affecting, nonaffecting := lookupVulns(vs, req.Mod.Path, req.Mod.Version)
+ affecting, nonaffecting, osvs := lookupVulns(vs, req.Mod.Path, req.Mod.Version)
// Get the `go mod why` results for the given file.
why, err := snapshot.ModWhy(ctx, fh)
@@ -113,7 +115,7 @@ func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offse
isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path)
header := formatHeader(req.Mod.Path, options)
explanation = formatExplanation(explanation, req, options, isPrivate)
- vulns := formatVulnerabilities(req.Mod.Path, affecting, nonaffecting, options, fromGovulncheck)
+ vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck)
return &protocol.Hover{
Contents: protocol.MarkupContent{
@@ -149,9 +151,9 @@ func hoverOnModuleStatement(ctx context.Context, pm *source.ParsedModule, offset
}
modpath := "stdlib"
goVersion := snapshot.View().GoVersionString()
- affecting, nonaffecting := lookupVulns(vs, modpath, goVersion)
+ affecting, nonaffecting, osvs := lookupVulns(vs, modpath, goVersion)
options := snapshot.Options()
- vulns := formatVulnerabilities(modpath, affecting, nonaffecting, options, fromGovulncheck)
+ vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck)
return &protocol.Hover{
Contents: protocol.MarkupContent{
@@ -174,50 +176,86 @@ func formatHeader(modpath string, options *source.Options) string {
return b.String()
}
-func lookupVulns(vulns *govulncheck.Result, modpath, version string) (affecting, nonaffecting []*govulncheck.Vuln) {
- if vulns == nil {
- return nil, nil
+func lookupVulns(vulns *vulncheck.Result, modpath, version string) (affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry) {
+ if vulns == nil || len(vulns.Entries) == 0 {
+ return nil, nil, nil
}
- for _, vuln := range vulns.Vulns {
- for _, mod := range vuln.Modules {
- if mod.Path != modpath {
- continue
- }
- // It is possible that the source code was changed since the last
- // govulncheck run and information in the `vulns` info is stale.
- // For example, imagine that a user is in the middle of updating
- // problematic modules detected by the govulncheck run by applying
- // quick fixes. Stale diagnostics can be confusing and prevent the
- // user from quickly locating the next module to fix.
- // Ideally we should rerun the analysis with the updated module
- // dependencies or any other code changes, but we are not yet
- // in the position of automatically triggering the analysis
- // (govulncheck can take a while). We also don't know exactly what
- // part of source code was changed since `vulns` was computed.
- // As a heuristic, we assume that a user upgrades the affecting
- // module to the version with the fix or the latest one, and if the
- // version in the require statement is equal to or higher than the
- // fixed version, skip the vulnerability information in the hover.
- // Eventually, the user has to rerun govulncheck.
- if mod.FixedVersion != "" && semver.IsValid(version) && semver.Compare(mod.FixedVersion, version) <= 0 {
- continue
- }
- if vuln.IsCalled() {
- affecting = append(affecting, vuln)
- } else {
- nonaffecting = append(nonaffecting, vuln)
+ for _, finding := range vulns.Findings {
+ vuln, typ := foundVuln(finding)
+ if vuln.Module != modpath {
+ continue
+ }
+ // It is possible that the source code was changed since the last
+ // govulncheck run and information in the `vulns` info is stale.
+ // For example, imagine that a user is in the middle of updating
+ // problematic modules detected by the govulncheck run by applying
+ // quick fixes. Stale diagnostics can be confusing and prevent the
+ // user from quickly locating the next module to fix.
+ // Ideally we should rerun the analysis with the updated module
+ // dependencies or any other code changes, but we are not yet
+ // in the position of automatically triggering the analysis
+ // (govulncheck can take a while). We also don't know exactly what
+ // part of source code was changed since `vulns` was computed.
+ // As a heuristic, we assume that a user upgrades the affecting
+ // module to the version with the fix or the latest one, and if the
+ // version in the require statement is equal to or higher than the
+ // fixed version, skip the vulnerability information in the hover.
+ // Eventually, the user has to rerun govulncheck.
+ if finding.FixedVersion != "" && semver.IsValid(version) && semver.Compare(finding.FixedVersion, version) <= 0 {
+ continue
+ }
+ switch typ {
+ case vulnCalled:
+ affecting = append(affecting, finding)
+ case vulnImported:
+ nonaffecting = append(nonaffecting, finding)
+ }
+ }
+
+ // Remove affecting elements from nonaffecting.
+ // An OSV entry can appear in both lists if an OSV entry covers
+ // multiple packages imported but not all vulnerable symbols are used.
+ // The current wording of hover message doesn't clearly
+ // present this case well IMO, so let's skip reporting nonaffecting.
+ if len(affecting) > 0 && len(nonaffecting) > 0 {
+ affectingSet := map[string]bool{}
+ for _, f := range affecting {
+ affectingSet[f.OSV] = true
+ }
+ n := 0
+ for _, v := range nonaffecting {
+ if !affectingSet[v.OSV] {
+ nonaffecting[n] = v
+ n++
}
}
+ nonaffecting = nonaffecting[:n]
+ }
+ sort.Slice(nonaffecting, func(i, j int) bool { return nonaffecting[i].OSV < nonaffecting[j].OSV })
+ sort.Slice(affecting, func(i, j int) bool { return affecting[i].OSV < affecting[j].OSV })
+ return affecting, nonaffecting, vulns.Entries
+}
+
+func fixedVersion(fixed string) string {
+ if fixed == "" {
+ return "No fix is available."
}
- sort.Slice(nonaffecting, func(i, j int) bool { return nonaffecting[i].OSV.ID < nonaffecting[j].OSV.ID })
- sort.Slice(affecting, func(i, j int) bool { return affecting[i].OSV.ID < affecting[j].OSV.ID })
- return affecting, nonaffecting
+ return "Fixed in " + fixed + "."
}
-func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulncheck.Vuln, options *source.Options, fromGovulncheck bool) string {
- if len(affecting) == 0 && len(nonaffecting) == 0 {
+func formatVulnerabilities(affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry, options *source.Options, fromGovulncheck bool) string {
+ if len(osvs) == 0 || (len(affecting) == 0 && len(nonaffecting) == 0) {
return ""
}
+ byOSV := func(findings []*govulncheck.Finding) map[string][]*govulncheck.Finding {
+ m := make(map[string][]*govulncheck.Finding)
+ for _, f := range findings {
+ m[f.OSV] = append(m[f.OSV], f)
+ }
+ return m
+ }
+ affectingByOSV := byOSV(affecting)
+ nonaffectingByOSV := byOSV(nonaffecting)
// TODO(hyangah): can we use go templates to generate hover messages?
// Then, we can use a different template for markdown case.
@@ -225,22 +263,23 @@ func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulnchec
var b strings.Builder
- if len(affecting) > 0 {
+ if len(affectingByOSV) > 0 {
// TODO(hyangah): make the message more eyecatching (icon/codicon/color)
- if len(affecting) == 1 {
- b.WriteString(fmt.Sprintf("\n**WARNING:** Found %d reachable vulnerability.\n", len(affecting)))
+ if len(affectingByOSV) == 1 {
+ fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerability.\n", len(affectingByOSV))
} else {
- b.WriteString(fmt.Sprintf("\n**WARNING:** Found %d reachable vulnerabilities.\n", len(affecting)))
+ fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerabilities.\n", len(affectingByOSV))
}
}
- for _, v := range affecting {
- fix := fixedVersionInfo(v, modPath)
- pkgs := vulnerablePkgsInfo(v, modPath, useMarkdown)
+ for id, findings := range affectingByOSV {
+ fix := fixedVersion(findings[0].FixedVersion)
+ pkgs := vulnerablePkgsInfo(findings, useMarkdown)
+ osvEntry := osvs[id]
if useMarkdown {
- fmt.Fprintf(&b, "- [**%v**](%v) %v%v%v\n", v.OSV.ID, href(v.OSV), formatMessage(v), pkgs, fix)
+ fmt.Fprintf(&b, "- [**%v**](%v) %v%v\n%v\n", id, href(id), osvEntry.Summary, pkgs, fix)
} else {
- fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", v.OSV.ID, formatMessage(v), href(v.OSV), pkgs, fix)
+ fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", id, osvEntry.Summary, href(id), pkgs, fix)
}
}
if len(nonaffecting) > 0 {
@@ -250,60 +289,41 @@ func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulnchec
fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities. Use `govulncheck` to check if the project uses vulnerable symbols.\n")
}
}
- for _, v := range nonaffecting {
- fix := fixedVersionInfo(v, modPath)
- pkgs := vulnerablePkgsInfo(v, modPath, useMarkdown)
+ for k, findings := range nonaffectingByOSV {
+ fix := fixedVersion(findings[0].FixedVersion)
+ pkgs := vulnerablePkgsInfo(findings, useMarkdown)
+ osvEntry := osvs[k]
+
if useMarkdown {
- fmt.Fprintf(&b, "- [%v](%v) %v%v%v\n", v.OSV.ID, href(v.OSV), formatMessage(v), pkgs, fix)
+ fmt.Fprintf(&b, "- [%v](%v) %v%v\n%v\n", k, href(k), osvEntry.Summary, pkgs, fix)
} else {
- fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", v.OSV.ID, formatMessage(v), href(v.OSV), pkgs, fix)
+ fmt.Fprintf(&b, " - [%v] %v (%v) %v\n%v\n", k, osvEntry.Summary, href(k), pkgs, fix)
}
}
b.WriteString("\n")
return b.String()
}
-func vulnerablePkgsInfo(v *govulncheck.Vuln, modPath string, useMarkdown bool) string {
- var b bytes.Buffer
- for _, m := range v.Modules {
- if m.Path != modPath {
- continue
- }
- if c := len(m.Packages); c == 1 {
- b.WriteString("\n Vulnerable package is:")
- } else if c > 1 {
- b.WriteString("\n Vulnerable packages are:")
- }
- for _, pkg := range m.Packages {
+func vulnerablePkgsInfo(findings []*govulncheck.Finding, useMarkdown bool) string {
+ var b strings.Builder
+ seen := map[string]bool{}
+ for _, f := range findings {
+ p := f.Trace[0].Package
+ if !seen[p] {
+ seen[p] = true
if useMarkdown {
b.WriteString("\n * `")
} else {
b.WriteString("\n ")
}
- b.WriteString(pkg.Path)
+ b.WriteString(p)
if useMarkdown {
b.WriteString("`")
}
}
}
- if b.Len() == 0 {
- return ""
- }
return b.String()
}
-func fixedVersionInfo(v *govulncheck.Vuln, modPath string) string {
- fix := "\n\n **No fix is available.**"
- for _, m := range v.Modules {
- if m.Path != modPath {
- continue
- }
- if m.FixedVersion != "" {
- fix = "\n\n Fixed in " + m.FixedVersion + "."
- }
- break
- }
- return fix
-}
func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string {
text = strings.TrimSuffix(text, "\n")
diff --git a/gopls/internal/lsp/prompt.go b/gopls/internal/lsp/prompt.go
new file mode 100644
index 00000000000..976f7c6e09f
--- /dev/null
+++ b/gopls/internal/lsp/prompt.go
@@ -0,0 +1,317 @@
+// 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 lsp
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/telemetry"
+ "golang.org/x/tools/internal/event"
+)
+
+// promptTimeout is the amount of time we wait for an ongoing prompt before
+// prompting again. This gives the user time to reply. However, at some point
+// we must assume that the client is not displaying the prompt, the user is
+// ignoring it, or the prompt has been disrupted in some way (e.g. by a gopls
+// crash).
+const promptTimeout = 24 * time.Hour
+
+// The following constants are used for testing telemetry integration.
+const (
+ TelemetryPromptWorkTitle = "Checking telemetry prompt" // progress notification title, for awaiting in tests
+ GoplsConfigDirEnvvar = "GOPLS_CONFIG_DIR" // overridden for testing
+ FakeTelemetryModefileEnvvar = "GOPLS_FAKE_TELEMETRY_MODEFILE" // overridden for testing
+ TelemetryYes = "Yes, I'd like to help."
+ TelemetryNo = "No, thanks."
+)
+
+// getenv returns the effective environment variable value for the provided
+// key, looking up the key in the session environment before falling back on
+// the process environment.
+func (s *Server) getenv(key string) string {
+ if v, ok := s.Options().Env[key]; ok {
+ return v
+ }
+ return os.Getenv(key)
+}
+
+// configDir returns the root of the gopls configuration dir. By default this
+// is os.UserConfigDir/gopls, but it may be overridden for tests.
+func (s *Server) configDir() (string, error) {
+ if d := s.getenv(GoplsConfigDirEnvvar); d != "" {
+ return d, nil
+ }
+ userDir, err := os.UserConfigDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(userDir, "gopls"), nil
+}
+
+// telemetryMode returns the current effective telemetry mode.
+// By default this is x/telemetry.Mode(), but it may be overridden for tests.
+func (s *Server) telemetryMode() string {
+ if fake := s.getenv(FakeTelemetryModefileEnvvar); fake != "" {
+ if data, err := os.ReadFile(fake); err == nil {
+ return string(data)
+ }
+ return "off"
+ }
+ return telemetry.Mode()
+}
+
+// setTelemetryMode sets the current telemetry mode.
+// By default this calls x/telemetry.SetMode, but it may be overridden for
+// tests.
+func (s *Server) setTelemetryMode(mode string) error {
+ if fake := s.getenv(FakeTelemetryModefileEnvvar); fake != "" {
+ return os.WriteFile(fake, []byte(mode), 0666)
+ }
+ return telemetry.SetMode(mode)
+}
+
+// maybePromptForTelemetry checks for the right conditions, and then prompts
+// the user to ask if they want to enable Go telemetry uploading. If the user
+// responds 'Yes', the telemetry mode is set to "on".
+//
+// The actual conditions for prompting are defensive, erring on the side of not
+// prompting.
+// If enabled is false, this will not prompt the user in any condition,
+// but will send work progress reports to help testing.
+func (s *Server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
+ if s.Options().VerboseWorkDoneProgress {
+ work := s.progress.Start(ctx, TelemetryPromptWorkTitle, "Checking if gopls should prompt about telemetry...", nil, nil)
+ defer work.End(ctx, "Done.")
+ }
+
+ if !enabled { // check this after the work progress message for testing.
+ return // prompt is disabled
+ }
+
+ if s.telemetryMode() == "on" {
+ // Telemetry is already on -- nothing to ask about.
+ return
+ }
+
+ errorf := func(format string, args ...any) {
+ err := fmt.Errorf(format, args...)
+ event.Error(ctx, "telemetry prompt failed", err)
+ }
+
+ // Only prompt if we can read/write the prompt config file.
+ configDir, err := s.configDir()
+ if err != nil {
+ errorf("unable to determine config dir: %v", err)
+ return
+ }
+
+ var (
+ promptDir = filepath.Join(configDir, "prompt") // prompt configuration directory
+ promptFile = filepath.Join(promptDir, "telemetry") // telemetry prompt file
+ )
+
+ // prompt states, to be written to the prompt file
+ const (
+ pYes = "yes" // user said yes
+ pNo = "no" // user said no
+ pPending = "pending" // current prompt is still pending
+ pFailed = "failed" // prompt was asked but failed
+ )
+ validStates := map[string]bool{
+ pYes: true,
+ pNo: true,
+ pPending: true,
+ pFailed: true,
+ }
+
+ // parse the current prompt file
+ var (
+ state string
+ attempts = 0 // number of times we've asked already
+ )
+ if content, err := os.ReadFile(promptFile); err == nil {
+ if _, err := fmt.Sscanf(string(content), "%s %d", &state, &attempts); err == nil && validStates[state] {
+ if state == pYes || state == pNo {
+ // Prompt has been answered. Nothing to do.
+ return
+ }
+ } else {
+ state, attempts = "", 0
+ errorf("malformed prompt result %q", string(content))
+ }
+ } else if !os.IsNotExist(err) {
+ errorf("reading prompt file: %v", err)
+ // Something went wrong. Since we don't know how many times we've asked the
+ // prompt, err on the side of not spamming.
+ return
+ }
+
+ if attempts >= 5 {
+ // We've tried asking enough; give up.
+ return
+ }
+ if attempts == 0 {
+ // First time asking the prompt; we may need to make the prompt dir.
+ if err := os.MkdirAll(promptDir, 0777); err != nil {
+ errorf("creating prompt dir: %v", err)
+ return
+ }
+ }
+
+ // Acquire the lock and write "pending" to the prompt file before actually
+ // prompting.
+ //
+ // This ensures that the prompt file is writeable, and that we increment the
+ // attempt counter before we prompt, so that we don't end up in a failure
+ // mode where we keep prompting and then failing to record the response.
+
+ release, ok, err := acquireLockFile(promptFile)
+ if err != nil {
+ errorf("acquiring prompt: %v", err)
+ return
+ }
+ if !ok {
+ // Another prompt is currently pending.
+ return
+ }
+ defer release()
+
+ attempts++
+
+ pendingContent := []byte(fmt.Sprintf("%s %d", pPending, attempts))
+ if err := os.WriteFile(promptFile, pendingContent, 0666); err != nil {
+ errorf("writing pending state: %v", err)
+ return
+ }
+
+ var prompt = `Go telemetry helps us improve Go by periodically sending anonymous metrics and crash reports to the Go team. Learn more at https://telemetry.go.dev/privacy.
+
+Would you like to enable Go telemetry?
+`
+ if s.Options().LinkifyShowMessage {
+ prompt = `Go telemetry helps us improve Go by periodically sending anonymous metrics and crash reports to the Go team. Learn more at [telemetry.go.dev/privacy](https://telemetry.go.dev/privacy).
+
+Would you like to enable Go telemetry?
+`
+ }
+ // TODO(rfindley): investigate a "tell me more" action in combination with ShowDocument.
+ params := &protocol.ShowMessageRequestParams{
+ Type: protocol.Info,
+ Message: prompt,
+ Actions: []protocol.MessageActionItem{
+ {Title: TelemetryYes},
+ {Title: TelemetryNo},
+ },
+ }
+
+ item, err := s.client.ShowMessageRequest(ctx, params)
+ if err != nil {
+ errorf("ShowMessageRequest failed: %v", err)
+ // Defensive: ensure item == nil for the logic below.
+ item = nil
+ }
+
+ message := func(typ protocol.MessageType, msg string) {
+ if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+ Type: typ,
+ Message: msg,
+ }); err != nil {
+ errorf("ShowMessage(unrecognize) failed: %v", err)
+ }
+ }
+
+ result := pFailed
+ if item == nil {
+ // e.g. dialog was dismissed
+ errorf("no response")
+ } else {
+ // Response matches MessageActionItem.Title.
+ switch item.Title {
+ case TelemetryYes:
+ result = pYes
+ if err := s.setTelemetryMode("on"); err == nil {
+ message(protocol.Info, telemetryOnMessage(s.Options().LinkifyShowMessage))
+ } else {
+ errorf("enabling telemetry failed: %v", err)
+ msg := fmt.Sprintf("Failed to enable Go telemetry: %v\nTo enable telemetry manually, please run `go run golang.org/x/telemetry/cmd/gotelemetry@latest on`", err)
+ message(protocol.Error, msg)
+ }
+
+ case TelemetryNo:
+ result = pNo
+ default:
+ errorf("unrecognized response %q", item.Title)
+ message(protocol.Error, fmt.Sprintf("Unrecognized response %q", item.Title))
+ }
+ }
+ resultContent := []byte(fmt.Sprintf("%s %d", result, attempts))
+ if err := os.WriteFile(promptFile, resultContent, 0666); err != nil {
+ errorf("error writing result state to prompt file: %v", err)
+ }
+}
+
+func telemetryOnMessage(linkify bool) string {
+ format := `Thank you. Telemetry uploading is now enabled.
+
+To disable telemetry uploading, run %s.
+`
+ var runCmd = "`go run golang.org/x/telemetry/cmd/gotelemetry@latest off`"
+ if linkify {
+ runCmd = "[gotelemetry off](https://golang.org/x/telemetry/cmd/gotelemetry)"
+ }
+ return fmt.Sprintf(format, runCmd)
+}
+
+// acquireLockFile attempts to "acquire a lock" for writing to path.
+//
+// This is achieved by creating an exclusive lock file at .lock. Lock
+// files expire after a period, at which point acquireLockFile will remove and
+// recreate the lock file.
+//
+// acquireLockFile fails if path is in a directory that doesn't exist.
+func acquireLockFile(path string) (func(), bool, error) {
+ lockpath := path + ".lock"
+ fi, err := os.Stat(lockpath)
+ if err == nil {
+ if time.Since(fi.ModTime()) > promptTimeout {
+ _ = os.Remove(lockpath) // ignore error
+ } else {
+ return nil, false, nil
+ }
+ } else if !os.IsNotExist(err) {
+ return nil, false, fmt.Errorf("statting lockfile: %v", err)
+ }
+
+ f, err := os.OpenFile(lockpath, os.O_CREATE|os.O_EXCL, 0666)
+ if err != nil {
+ if os.IsExist(err) {
+ return nil, false, nil
+ }
+ return nil, false, fmt.Errorf("creating lockfile: %v", err)
+ }
+ fi, err = f.Stat()
+ if err != nil {
+ return nil, false, err
+ }
+ release := func() {
+ _ = f.Close() // ignore error
+ fi2, err := os.Stat(lockpath)
+ if err == nil && os.SameFile(fi, fi2) {
+ // Only clean up the lockfile if it's the same file we created.
+ // Otherwise, our lock has expired and something else has the lock.
+ //
+ // There's a race here, in that the file could have changed since the
+ // stat above; but given that we've already waited 24h this is extremely
+ // unlikely, and acceptable.
+ _ = os.Remove(lockpath)
+ }
+ }
+ return release, true, nil
+}
diff --git a/gopls/internal/lsp/prompt_test.go b/gopls/internal/lsp/prompt_test.go
new file mode 100644
index 00000000000..d268d1f3a0c
--- /dev/null
+++ b/gopls/internal/lsp/prompt_test.go
@@ -0,0 +1,82 @@
+// 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 lsp
+
+import (
+ "path/filepath"
+ "sync"
+ "sync/atomic"
+ "testing"
+)
+
+func TestAcquireFileLock(t *testing.T) {
+ name := filepath.Join(t.TempDir(), "config.json")
+
+ const concurrency = 100
+ var acquired int32
+ var releasers [concurrency]func()
+ defer func() {
+ for _, r := range releasers {
+ if r != nil {
+ r()
+ }
+ }
+ }()
+
+ var wg sync.WaitGroup
+ for i := range releasers {
+ i := i
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ release, ok, err := acquireLockFile(name)
+ if err != nil {
+ t.Errorf("Acquire failed: %v", err)
+ return
+ }
+ if ok {
+ atomic.AddInt32(&acquired, 1)
+ releasers[i] = release
+ }
+ }()
+ }
+
+ wg.Wait()
+
+ if acquired != 1 {
+ t.Errorf("Acquire succeeded %d times, expected exactly 1", acquired)
+ }
+}
+
+func TestReleaseAndAcquireFileLock(t *testing.T) {
+ name := filepath.Join(t.TempDir(), "config.json")
+
+ acquire := func() (func(), bool) {
+ t.Helper()
+ release, ok, err := acquireLockFile(name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return release, ok
+ }
+
+ release, ok := acquire()
+ if !ok {
+ t.Fatal("failed to Acquire")
+ }
+ if release2, ok := acquire(); ok {
+ release()
+ release2()
+ t.Fatalf("Acquire succeeded unexpectedly")
+ }
+
+ release()
+ release3, ok := acquire()
+ release3()
+ if !ok {
+ t.Fatalf("failed to Acquire")
+ }
+}
diff --git a/gopls/internal/lsp/references.go b/gopls/internal/lsp/references.go
index cc89b381088..d3d36235697 100644
--- a/gopls/internal/lsp/references.go
+++ b/gopls/internal/lsp/references.go
@@ -10,11 +10,17 @@ import (
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/template"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
-func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) {
+func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) (_ []protocol.Location, rerr error) {
+ recordLatency := telemetry.StartLatencyTimer("references")
+ defer func() {
+ recordLatency(ctx, rerr)
+ }()
+
ctx, done := event.Start(ctx, "lsp.Server.references", tag.URI.Of(params.TextDocument.URI))
defer done()
diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go
index 0136870bc3a..7238eb0e832 100644
--- a/gopls/internal/lsp/regtest/expectation.go
+++ b/gopls/internal/lsp/regtest/expectation.go
@@ -212,21 +212,6 @@ func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Exp
}
}
-// NoOutstandingWork asserts that there is no work initiated using the LSP
-// $/progress API that has not completed.
-func NoOutstandingWork() Expectation {
- check := func(s State) Verdict {
- if len(s.outstandingWork()) == 0 {
- return Met
- }
- return Unmet
- }
- return Expectation{
- Check: check,
- Description: "no outstanding work",
- }
-}
-
// ShownDocument asserts that the client has received a
// ShowDocumentRequest for the given URI.
func ShownDocument(uri protocol.URI) Expectation {
@@ -277,26 +262,24 @@ func ShownMessage(containing string) Expectation {
}
}
-// ShowMessageRequest asserts that the editor has received a ShowMessageRequest
-// with an action item that has the given title.
-func ShowMessageRequest(title 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 {
if len(s.showMessageRequest) == 0 {
return Unmet
}
- // Only check the most recent one.
- m := s.showMessageRequest[len(s.showMessageRequest)-1]
- if len(m.Actions) == 0 || len(m.Actions) > 1 {
- return Unmet
- }
- if m.Actions[0].Title == title {
- return Met
+ for _, m := range s.showMessageRequest {
+ if msgRE.MatchString(m.Message) {
+ return Met
+ }
}
return Unmet
}
return Expectation{
Check: check,
- Description: "received ShowMessageRequest",
+ Description: fmt.Sprintf("ShowMessageRequest matching %q", messageRegexp),
}
}
@@ -312,11 +295,12 @@ func ShowMessageRequest(title string) Expectation {
func (e *Env) DoneDiagnosingChanges() Expectation {
stats := e.Editor.Stats()
statsBySource := map[lsp.ModificationSource]uint64{
- lsp.FromDidOpen: stats.DidOpen,
- lsp.FromDidChange: stats.DidChange,
- lsp.FromDidSave: stats.DidSave,
- lsp.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles,
- lsp.FromDidClose: stats.DidClose,
+ lsp.FromDidOpen: stats.DidOpen,
+ lsp.FromDidChange: stats.DidChange,
+ lsp.FromDidSave: stats.DidSave,
+ lsp.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles,
+ lsp.FromDidClose: stats.DidClose,
+ lsp.FromDidChangeConfiguration: stats.DidChangeConfiguration,
}
var expected []lsp.ModificationSource
@@ -495,6 +479,46 @@ func OutstandingWork(title, msg string) Expectation {
}
}
+// NoOutstandingWork asserts that there is no work initiated using the LSP
+// $/progress API that has not completed.
+//
+// If non-nil, the ignore func is used to ignore certain work items for the
+// purpose of this check.
+//
+// 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 {
+ for _, w := range s.work {
+ if w.complete {
+ continue
+ }
+ if w.title == "" {
+ // A token that has been created but not yet used.
+ //
+ // TODO(rfindley): this should be separated in the data model: until
+ // the "begin" notification, work should not be in progress.
+ continue
+ }
+ if ignore(w.title, w.msg) {
+ continue
+ }
+ return Unmet
+ }
+ return Met
+ }
+ return Expectation{
+ Check: check,
+ Description: "no outstanding work",
+ }
+}
+
+// IgnoreTelemetryPromptWork may be used in conjunction with NoOutStandingWork
+// to ignore the telemetry prompt.
+func IgnoreTelemetryPromptWork(title, msg string) bool {
+ return title == lsp.TelemetryPromptWorkTitle
+}
+
// NoErrorLogs asserts that the client has not received any log messages of
// error severity.
func NoErrorLogs() Expectation {
diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go
index 36dcda39647..f6c1d4c6077 100644
--- a/gopls/internal/lsp/regtest/marker.go
+++ b/gopls/internal/lsp/regtest/marker.go
@@ -11,6 +11,7 @@ import (
"flag"
"fmt"
"go/token"
+ "go/types"
"io/fs"
"log"
"os"
@@ -110,11 +111,16 @@ var update = flag.Bool("update", false, "if set, update test data during marker
// in these directories before running the test.
// -skip_goos=a,b,c instructs the test runner to skip the test for the
// listed GOOS values.
+// -ignore_extra_diags suppresses errors for unmatched diagnostics
// TODO(rfindley): using build constraint expressions for -skip_goos would
// be clearer.
// TODO(rfindley): support flag values containing whitespace.
// - "settings.json": this file is parsed as JSON, and used as the
// session configuration (see gopls/doc/settings.md)
+// - "capabilities.json": this file is parsed as JSON client capabilities,
+// and applied as an overlay over the default editor client capabilities.
+// see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities
+// for more details.
// - "env": this file is parsed as a list of VAR=VALUE fields specifying the
// editor environment.
// - Golden files: Within the archive, file names starting with '@' are
@@ -130,6 +136,12 @@ var update = flag.Bool("update", false, "if set, update test data during marker
//
// # Marker types
//
+// Markers are of two kinds. A few are "value markers" (e.g. @item), which are
+// processed in a first pass and each computes a value that may be referred to
+// by name later. Most are "action markers", which are processed in a second
+// pass and take some action such as testing an LSP operation; they may refer
+// to values computed by value markers.
+//
// The following markers are supported within marker tests:
//
// - acceptcompletion(location, label, golden): specifies that accepting the
@@ -144,8 +156,16 @@ var update = flag.Bool("update", false, "if set, update test data during marker
// - codeactionerr(kind, start, end, wantError): specifies a codeaction that
// fails with an error that matches the expectation.
//
-// - complete(location, ...labels): specifies expected completion results at
-// the given location.
+// - codelens(location, title): specifies that a codelens is expected at the
+// given location, with given title. Must be used in conjunction with
+// @codelenses.
+//
+// - codelenses(): specifies that textDocument/codeLens should be run for the
+// current document, with results compared to the @codelens annotations in
+// the current document.
+//
+// - complete(location, ...items): specifies expected completion results at
+// the given location. Must be used in conjunction with @item.
//
// - diag(location, regexp): specifies an expected diagnostic matching the
// given regexp at the given location. The test runner requires
@@ -161,12 +181,21 @@ var update = flag.Bool("update", false, "if set, update test data during marker
// - def(src, dst location): perform a textDocument/definition request at
// the src location, and check the result points to the dst location.
//
+// - foldingrange(golden): perform a textDocument/foldingRange for the
+// current document, and compare with the golden content, which is the
+// original source annotated with numbered tags delimiting the resulting
+// ranges (e.g. <1 kind="..."> ... 1>).
+//
// - format(golden): perform a textDocument/format request for the enclosing
// file, and compare against the named golden file. If the formatting
// request succeeds, the golden file must contain the resulting formatted
// source. If the formatting request fails, the golden file must contain
// the error message.
//
+// - highlight(src location, dsts ...location): makes a
+// textDocument/highlight request at the given src location, which should
+// highlight the provided dst locations.
+//
// - hover(src, dst location, g Golden): perform a textDocument/hover at the
// src location, and checks that the result is the dst location, with hover
// content matching "hover.md" in the golden data g.
@@ -175,6 +204,14 @@ var update = flag.Bool("update", false, "if set, update test data during marker
// textDocument/implementation query at the src location and
// checks that the resulting set of locations matches want.
//
+// - item(label, details, kind): defines a completion item with the provided
+// fields. This information is not positional, and therefore @item markers
+// may occur anywhere in the source. Used in conjunction with @complete,
+// snippet, or rank.
+//
+// TODO(rfindley): rethink whether floating @item annotations are the best
+// way to specify completion results.
+//
// - loc(name, location): specifies the name for a location in the source. These
// locations may be referenced by other markers.
//
@@ -191,9 +228,26 @@ var update = flag.Bool("update", false, "if set, update test data during marker
// This action is executed for its editing effects on the source files.
// Like rename, the golden directory contains the expected transformed files.
//
-// - refs(location, want ...location): executes a 'references' query at the
-// first location and asserts that the result is the set of 'want' locations.
-// The first want location must be the declaration (assumedly unique).
+// - rank(location, ...completionItem): executes a textDocument/completion
+// request at the given location, and verifies that each expected
+// completion item occurs in the results, in the expected order. Other
+// unexpected completion items may occur in the results.
+// TODO(rfindley): this exists for compatibility with the old marker tests.
+// Replace this with rankl, and rename.
+//
+// - rankl(location, ...label): like rank, but only cares about completion
+// item labels.
+//
+// - refs(location, want ...location): executes a textDocument/references
+// request at the first location and asserts that the result is the set of
+// 'want' locations. The first want location must be the declaration
+// (assumedly unique).
+//
+// - snippet(location, completionItem, snippet): executes a
+// textDocument/completion request at the location, and searches for a
+// result with label matching that of the provided completion item
+// (TODO(rfindley): accept a label rather than a completion item). Check
+// the the result snippet matches the provided snippet.
//
// - symbol(golden): makes a textDocument/documentSymbol request
// for the enclosing file, formats the response with one symbol
@@ -300,7 +354,7 @@ var update = flag.Bool("update", false, "if set, update test data during marker
// internal/lsp/tests.
//
// Remaining TODO:
-// - parallelize/optimize test execution
+// - optimize test execution
// - reorganize regtest packages (and rename to just 'test'?)
// - Rename the files .txtar.
// - Provide some means by which locations in the standard library
@@ -309,23 +363,19 @@ var update = flag.Bool("update", false, "if set, update test data during marker
//
// Existing marker tests (in ../testdata) to port:
// - CallHierarchy
-// - CodeLens
// - Diagnostics
// - CompletionItems
// - Completions
// - CompletionSnippets
-// - UnimportedCompletions
// - DeepCompletions
// - FuzzyCompletions
// - CaseSensitiveCompletions
// - RankCompletions
-// - FoldingRanges
// - Formats
// - Imports
// - SemanticTokens
// - FunctionExtractions
// - MethodExtractions
-// - Highlights
// - Renames
// - PrepareRenames
// - InlayHints
@@ -348,7 +398,9 @@ func RunMarkerTests(t *testing.T, dir string) {
cache := cache.New(nil)
for _, test := range tests {
+ test := test
t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
if test.skipReason != "" {
t.Skip(test.skipReason)
}
@@ -371,21 +423,25 @@ func RunMarkerTests(t *testing.T, dir string) {
testenv.NeedsTool(t, "cgo")
}
config := fake.EditorConfig{
- Settings: test.settings,
- Env: test.env,
+ Settings: test.settings,
+ CapabilitiesJSON: test.capabilities,
+ Env: test.env,
}
if _, ok := config.Settings["diagnosticsDelay"]; !ok {
if config.Settings == nil {
- config.Settings = make(map[string]interface{})
+ config.Settings = make(map[string]any)
}
config.Settings["diagnosticsDelay"] = "10ms"
}
+ // inv: config.Settings != nil
run := &markerTestRun{
- test: test,
- env: newEnv(t, cache, test.files, test.proxyFiles, test.writeGoSum, config),
- locations: make(map[expect.Identifier]protocol.Location),
- diags: make(map[protocol.Location][]protocol.Diagnostic),
+ test: test,
+ env: newEnv(t, cache, test.files, test.proxyFiles, test.writeGoSum, config),
+ settings: config.Settings,
+ values: make(map[expect.Identifier]any),
+ diags: make(map[protocol.Location][]protocol.Diagnostic),
+ extraNotes: make(map[protocol.DocumentURI]map[string][]*expect.Note),
}
// TODO(rfindley): make it easier to clean up the regtest environment.
defer run.env.Editor.Shutdown(context.Background()) // ignore error
@@ -399,19 +455,6 @@ func RunMarkerTests(t *testing.T, dir string) {
for file := range test.files {
run.env.OpenFile(file)
}
-
- // Pre-process locations.
- var markers []marker
- for _, note := range test.notes {
- mark := marker{run: run, note: note}
- switch note.Name {
- case "loc":
- mark.execute()
- default:
- markers = append(markers, mark)
- }
- }
-
// Wait for the didOpen notifications to be processed, then collect
// diagnostics.
var diags map[string]*protocol.PublishDiagnosticsParams
@@ -430,15 +473,42 @@ func RunMarkerTests(t *testing.T, dir string) {
}
}
+ var markers []marker
+ for _, note := range test.notes {
+ mark := marker{run: run, note: note}
+ if fn, ok := valueMarkerFuncs[note.Name]; ok {
+ fn(mark)
+ } else if _, ok := actionMarkerFuncs[note.Name]; ok {
+ markers = append(markers, mark) // save for later
+ } else {
+ uri := mark.uri()
+ if run.extraNotes[uri] == nil {
+ run.extraNotes[uri] = make(map[string][]*expect.Note)
+ }
+ run.extraNotes[uri][note.Name] = append(run.extraNotes[uri][note.Name], note)
+ }
+ }
+
// Invoke each remaining marker in the test.
for _, mark := range markers {
- mark.execute()
+ actionMarkerFuncs[mark.note.Name](mark)
}
// Any remaining (un-eliminated) diagnostics are an error.
- for loc, diags := range run.diags {
- for _, diag := range diags {
- t.Errorf("%s: unexpected diagnostic: %q", run.fmtLoc(loc), diag.Message)
+ if !test.ignoreExtraDiags {
+ for loc, diags := range run.diags {
+ for _, diag := range diags {
+ t.Errorf("%s: unexpected diagnostic: %q", run.fmtLoc(loc), diag.Message)
+ }
+ }
+ }
+
+ // TODO(rfindley): use these for whole-file marker tests.
+ for uri, extras := range run.extraNotes {
+ for name, extra := range extras {
+ if len(extra) > 0 {
+ t.Errorf("%s: %d unused %q markers", run.env.Sandbox.Workdir.URIToPath(uri), len(extra), name)
+ }
}
}
@@ -488,7 +558,7 @@ func (m marker) server() protocol.Server {
// errorf reports an error with a prefix indicating the position of the marker note.
//
// It formats the error message using mark.sprintf.
-func (mark marker) errorf(format string, args ...interface{}) {
+func (mark marker) errorf(format string, args ...any) {
msg := mark.sprintf(format, args...)
// TODO(adonovan): consider using fmt.Fprintf(os.Stderr)+t.Fail instead of
// t.Errorf to avoid reporting uninteresting positions in the Go source of
@@ -497,75 +567,164 @@ func (mark marker) errorf(format string, args ...interface{}) {
mark.run.env.T.Errorf("%s: %s", mark.run.fmtPos(mark.note.Pos), msg)
}
-// execute invokes the marker's function with the arguments from note.
-func (mark marker) execute() {
- fn, ok := markerFuncs[mark.note.Name]
- if !ok {
- mark.errorf("no marker function named %s", mark.note.Name)
- return
+// valueMarkerFunc returns a wrapper around a function that allows it to be
+// called during the processing of value markers (e.g. @value(v, 123)) with marker
+// arguments converted to function parameters. The provided function's first
+// parameter must be of type 'marker', and it must return a value.
+//
+// Unlike action markers, which are executed for actions such as test
+// assertions, value markers are all evaluated first, and each computes
+// a value that is recorded by its identifier, which is the marker's first
+// argument. These values may be referred to from an action marker by
+// this identifier, e.g. @action(... , v, ...).
+//
+// For example, given a fn with signature
+//
+// func(mark marker, label, details, kind string) CompletionItem
+//
+// The result of valueMarkerFunc can associated with @item notes, and invoked
+// as follows:
+//
+// //@item(FooCompletion, "Foo", "func() int", "func")
+//
+// The provided fn should not mutate the test environment.
+func valueMarkerFunc(fn any) func(marker) {
+ ftype := reflect.TypeOf(fn)
+ if ftype.NumIn() == 0 || ftype.In(0) != markerType {
+ panic(fmt.Sprintf("value marker function %#v must accept marker as its first argument", ftype))
+ }
+ if ftype.NumOut() != 1 {
+ panic(fmt.Sprintf("value marker function %#v must have exactly 1 result", ftype))
}
- // The first converter corresponds to the *Env argument.
- // All others must be converted from the marker syntax.
- args := []reflect.Value{reflect.ValueOf(mark)}
- var convert converter
- for i, in := range mark.note.Args {
- if i < len(fn.converters) {
- convert = fn.converters[i]
- } else if !fn.variadic {
- goto arity // too many args
+ return func(mark marker) {
+ if len(mark.note.Args) == 0 || !is[expect.Identifier](mark.note.Args[0]) {
+ mark.errorf("first argument to a value marker function must be an identifier")
+ return
}
-
- // Special handling for the blank identifier: treat it as the zero value.
- if ident, ok := in.(expect.Identifier); ok && ident == "_" {
- zero := reflect.Zero(fn.paramTypes[i])
- args = append(args, zero)
- continue
+ id := mark.note.Args[0].(expect.Identifier)
+ if alt, ok := mark.run.values[id]; ok {
+ mark.errorf("%s already declared as %T", id, alt)
+ return
}
+ args := append([]any{mark}, mark.note.Args[1:]...)
+ argValues, err := convertArgs(mark, ftype, args)
+ if err != nil {
+ mark.errorf("converting args: %v", err)
+ return
+ }
+ results := reflect.ValueOf(fn).Call(argValues)
+ mark.run.values[id] = results[0].Interface()
+ }
+}
- out, err := convert(mark, in)
+// actionMarkerFunc returns a wrapper around a function that allows it to be
+// called during the processing of action markers (e.g. @action("abc", 123))
+// with marker arguments converted to function parameters. The provided
+// function's first parameter must be of type 'marker', and it must not return
+// any values.
+//
+// The provided fn should not mutate the test environment.
+func actionMarkerFunc(fn any) func(marker) {
+ ftype := reflect.TypeOf(fn)
+ if ftype.NumIn() == 0 || ftype.In(0) != markerType {
+ panic(fmt.Sprintf("action marker function %#v must accept marker as its first argument", ftype))
+ }
+ if ftype.NumOut() != 0 {
+ panic(fmt.Sprintf("action marker function %#v cannot have results", ftype))
+ }
+
+ return func(mark marker) {
+ args := append([]any{mark}, mark.note.Args...)
+ argValues, err := convertArgs(mark, ftype, args)
if err != nil {
- mark.errorf("converting argument #%d of %s (%v): %v", i, mark.note.Name, in, err)
+ mark.errorf("converting args: %v", err)
return
}
- args = append(args, reflect.ValueOf(out))
+ reflect.ValueOf(fn).Call(argValues)
}
- if len(args) < len(fn.converters) {
- goto arity // too few args
+}
+
+func convertArgs(mark marker, ftype reflect.Type, args []any) ([]reflect.Value, error) {
+ var (
+ argValues []reflect.Value
+ pnext int // next param index
+ p reflect.Type // current param
+ )
+ for i, arg := range args {
+ if i < ftype.NumIn() {
+ p = ftype.In(pnext)
+ pnext++
+ } else if p == nil || !ftype.IsVariadic() {
+ // The actual number of arguments expected by the mark varies, depending
+ // on whether this is a value marker or an action marker.
+ //
+ // Since this error indicates a bug, probably OK to have an imprecise
+ // error message here.
+ return nil, fmt.Errorf("too many arguments to %s", mark.note.Name)
+ }
+ elemType := p
+ if ftype.IsVariadic() && pnext == ftype.NumIn() {
+ elemType = p.Elem()
+ }
+ var v reflect.Value
+ if id, ok := arg.(expect.Identifier); ok && id == "_" {
+ v = reflect.Zero(elemType)
+ } else {
+ a, err := convert(mark, arg, elemType)
+ if err != nil {
+ return nil, err
+ }
+ v = reflect.ValueOf(a)
+ }
+ argValues = append(argValues, v)
+ }
+ // Check that we have sufficient arguments. If the function is variadic, we
+ // do not need arguments for the final parameter.
+ if pnext < ftype.NumIn()-1 || pnext == ftype.NumIn()-1 && !ftype.IsVariadic() {
+ // Same comment as above: OK to be vague here.
+ return nil, fmt.Errorf("not enough arguments to %s", mark.note.Name)
}
+ return argValues, nil
+}
- fn.fn.Call(args)
- return
+// is reports whether arg is a T.
+func is[T any](arg any) bool {
+ _, ok := arg.(T)
+ return ok
+}
-arity:
- mark.errorf("got %d arguments to %s, want %d",
- len(mark.note.Args), mark.note.Name, len(fn.converters))
+// Supported value marker functions. See [valueMarkerFunc] for more details.
+var valueMarkerFuncs = map[string]func(marker){
+ "loc": valueMarkerFunc(locMarker),
+ "item": valueMarkerFunc(completionItemMarker),
}
-// Supported marker functions.
-//
-// Each marker function must accept a marker as its first argument, with
-// subsequent arguments converted from the marker arguments.
-//
-// Marker funcs should not mutate the test environment (e.g. via opening files
-// or applying edits in the editor).
-var markerFuncs = map[string]markerFunc{
- "acceptcompletion": makeMarkerFunc(acceptCompletionMarker),
- "codeaction": makeMarkerFunc(codeActionMarker),
- "codeactionerr": makeMarkerFunc(codeActionErrMarker),
- "complete": makeMarkerFunc(completeMarker),
- "def": makeMarkerFunc(defMarker),
- "diag": makeMarkerFunc(diagMarker),
- "hover": makeMarkerFunc(hoverMarker),
- "format": makeMarkerFunc(formatMarker),
- "implementation": makeMarkerFunc(implementationMarker),
- "loc": makeMarkerFunc(locMarker),
- "rename": makeMarkerFunc(renameMarker),
- "renameerr": makeMarkerFunc(renameErrMarker),
- "suggestedfix": makeMarkerFunc(suggestedfixMarker),
- "symbol": makeMarkerFunc(symbolMarker),
- "refs": makeMarkerFunc(refsMarker),
- "workspacesymbol": makeMarkerFunc(workspaceSymbolMarker),
+// Supported action marker functions. See [actionMarkerFunc] for more details.
+var actionMarkerFuncs = map[string]func(marker){
+ "acceptcompletion": actionMarkerFunc(acceptCompletionMarker),
+ "codeaction": actionMarkerFunc(codeActionMarker),
+ "codeactionerr": actionMarkerFunc(codeActionErrMarker),
+ "codelenses": actionMarkerFunc(codeLensesMarker),
+ "complete": actionMarkerFunc(completeMarker),
+ "def": actionMarkerFunc(defMarker),
+ "diag": actionMarkerFunc(diagMarker),
+ "foldingrange": actionMarkerFunc(foldingRangeMarker),
+ "format": actionMarkerFunc(formatMarker),
+ "highlight": actionMarkerFunc(highlightMarker),
+ "hover": actionMarkerFunc(hoverMarker),
+ "implementation": actionMarkerFunc(implementationMarker),
+ "rank": actionMarkerFunc(rankMarker),
+ "rankl": actionMarkerFunc(ranklMarker),
+ "refs": actionMarkerFunc(refsMarker),
+ "rename": actionMarkerFunc(renameMarker),
+ "renameerr": actionMarkerFunc(renameErrMarker),
+ "signature": actionMarkerFunc(signatureMarker),
+ "snippet": actionMarkerFunc(snippetMarker),
+ "suggestedfix": actionMarkerFunc(suggestedfixMarker),
+ "symbol": actionMarkerFunc(symbolMarker),
+ "typedef": actionMarkerFunc(typedefMarker),
+ "workspacesymbol": actionMarkerFunc(workspaceSymbolMarker),
}
// markerTest holds all the test data extracted from a test txtar archive.
@@ -573,25 +732,27 @@ var markerFuncs = map[string]markerFunc{
// See the documentation for RunMarkerTests for more information on the archive
// format.
type markerTest struct {
- name string // relative path to the txtar file in the testdata dir
- fset *token.FileSet // fileset used for parsing notes
- content []byte // raw test content
- archive *txtar.Archive // original test archive
- settings map[string]interface{} // gopls settings
- env map[string]string // editor environment
- proxyFiles map[string][]byte // proxy content
- files map[string][]byte // data files from the archive (excluding special files)
- notes []*expect.Note // extracted notes from data files
- golden map[string]*Golden // extracted golden content, by identifier name
+ name string // relative path to the txtar file in the testdata dir
+ fset *token.FileSet // fileset used for parsing notes
+ content []byte // raw test content
+ archive *txtar.Archive // original test archive
+ settings map[string]any // gopls settings
+ capabilities []byte // content of capabilities.json file
+ env map[string]string // editor environment
+ proxyFiles map[string][]byte // proxy content
+ files map[string][]byte // data files from the archive (excluding special files)
+ notes []*expect.Note // extracted notes from data files
+ golden map[expect.Identifier]*Golden // extracted golden content, by identifier name
skipReason string // the skip reason extracted from the "skip" archive file
flags []string // flags extracted from the special "flags" archive file.
// Parsed flags values.
- minGoVersion string
- cgo bool
- writeGoSum []string // comma separated dirs to write go sum for
- skipGOOS []string // comma separated GOOS values to skip
+ minGoVersion string
+ cgo bool
+ writeGoSum []string // comma separated dirs to write go sum for
+ skipGOOS []string // comma separated GOOS values to skip
+ ignoreExtraDiags bool
}
// flagSet returns the flagset used for parsing the special "flags" file in the
@@ -602,6 +763,7 @@ func (t *markerTest) flagSet() *flag.FlagSet {
flags.BoolVar(&t.cgo, "cgo", false, "if set, requires cgo (both the cgo tool and CGO_ENABLED=1)")
flags.Var((*stringListValue)(&t.writeGoSum), "write_sumfile", "if set, write the sumfile for these directories")
flags.Var((*stringListValue)(&t.skipGOOS), "skip_goos", "if set, skip this test on these GOOS values")
+ flags.BoolVar(&t.ignoreExtraDiags, "ignore_extra_diags", false, "if set, suppress errors for unmatched diagnostics")
return flags
}
@@ -621,7 +783,7 @@ func (l stringListValue) String() string {
return strings.Join([]string(l), ",")
}
-func (t *markerTest) getGolden(id string) *Golden {
+func (t *markerTest) getGolden(id expect.Identifier) *Golden {
golden, ok := t.golden[id]
// If there was no golden content for this identifier, we must create one
// to handle the case where -update is set: we need a place to store
@@ -643,7 +805,7 @@ func (t *markerTest) getGolden(id string) *Golden {
// When -update is set, golden captures the updated golden contents for later
// writing.
type Golden struct {
- id string
+ id expect.Identifier
data map[string][]byte // key "" => @id itself
updated map[string][]byte
}
@@ -722,7 +884,7 @@ func loadMarkerTest(name string, content []byte) (*markerTest, error) {
content: content,
archive: archive,
files: make(map[string][]byte),
- golden: make(map[string]*Golden),
+ golden: make(map[expect.Identifier]*Golden),
}
for _, file := range archive.Files {
switch {
@@ -742,6 +904,9 @@ func loadMarkerTest(name string, content []byte) (*markerTest, error) {
return nil, err
}
+ case file.Name == "capabilities.json":
+ test.capabilities = file.Data // lazily unmarshalled by the editor
+
case file.Name == "env":
test.env = make(map[string]string)
fields := strings.Fields(string(file.Data))
@@ -754,7 +919,8 @@ func loadMarkerTest(name string, content []byte) (*markerTest, error) {
}
case strings.HasPrefix(file.Name, "@"): // golden content
- id, name, _ := strings.Cut(file.Name[len("@"):], "/")
+ idstring, name, _ := strings.Cut(file.Name[len("@"):], "/")
+ id := expect.Identifier(idstring)
// Note that a file.Name of just "@id" gives (id, name) = ("id", "").
if _, ok := test.golden[id]; !ok {
test.golden[id] = &Golden{
@@ -808,7 +974,7 @@ func formatTest(test *markerTest) ([]byte, error) {
updatedGolden := make(map[string][]byte)
for id, g := range test.golden {
for name, data := range g.updated {
- filename := "@" + path.Join(id, name) // name may be ""
+ filename := "@" + path.Join(string(id), name) // name may be ""
updatedGolden[filename] = data
}
}
@@ -818,7 +984,7 @@ func formatTest(test *markerTest) ([]byte, error) {
switch file.Name {
// Preserve configuration files exactly as they were. They must have parsed
// if we got this far.
- case "skip", "flags", "settings.json", "env":
+ case "skip", "flags", "settings.json", "capabilities.json", "env":
arch.Files = append(arch.Files, file)
default:
if _, ok := test.files[file.Name]; ok { // ordinary file
@@ -893,34 +1059,32 @@ func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byt
}
}
-// A markerFunc is a reflectively callable @mark implementation function.
-type markerFunc struct {
- fn reflect.Value // the func to invoke
- paramTypes []reflect.Type // parameter types, for zero values
- converters []converter // to convert non-blank arguments
- variadic bool
-}
-
// A markerTestRun holds the state of one run of a marker test archive.
type markerTestRun struct {
- test *markerTest
- env *Env
+ test *markerTest
+ env *Env
+ settings map[string]any
// Collected information.
// Each @diag/@suggestedfix marker eliminates an entry from diags.
- locations map[expect.Identifier]protocol.Location
- diags map[protocol.Location][]protocol.Diagnostic // diagnostics by position; location end == start
+ values map[expect.Identifier]any
+ diags map[protocol.Location][]protocol.Diagnostic // diagnostics by position; location end == start
+
+ // Notes that weren't associated with a top-level marker func. They may be
+ // consumed by another marker (e.g. @codelenses collects @codelens markers).
+ // Any notes that aren't consumed are flagged as an error.
+ extraNotes map[protocol.DocumentURI]map[string][]*expect.Note
}
// sprintf returns a formatted string after applying pre-processing to
// arguments of the following types:
// - token.Pos: formatted using (*markerTestRun).fmtPos
// - protocol.Location: formatted using (*markerTestRun).fmtLoc
-func (c *marker) sprintf(format string, args ...interface{}) string {
+func (c *marker) sprintf(format string, args ...any) string {
if false {
_ = fmt.Sprintf(format, args...) // enable vet printf checker
}
- var args2 []interface{}
+ var args2 []any
for _, arg := range args {
switch arg := arg.(type) {
case token.Pos:
@@ -939,6 +1103,11 @@ func (mark marker) uri() protocol.DocumentURI {
return mark.run.env.Sandbox.Workdir.URI(mark.run.test.fset.File(mark.note.Pos).Name())
}
+// path returns the relative path to the file containing the marker.
+func (mark marker) path() string {
+ return mark.run.env.Sandbox.Workdir.RelPath(mark.run.test.fset.File(mark.note.Pos).Name())
+}
+
// fmtLoc formats the given pos in the context of the test, using
// archive-relative paths for files and including the line number in the full
// archive file.
@@ -1024,36 +1193,11 @@ func (run *markerTestRun) fmtLocDetails(loc protocol.Location, includeTxtPos boo
}
}
-// makeMarkerFunc uses reflection to create a markerFunc for the given func value.
-func makeMarkerFunc(fn interface{}) markerFunc {
- mi := markerFunc{
- fn: reflect.ValueOf(fn),
- }
- mtyp := mi.fn.Type()
- mi.variadic = mtyp.IsVariadic()
- if mtyp.NumIn() == 0 || mtyp.In(0) != markerType {
- panic(fmt.Sprintf("marker function %#v must accept marker as its first argument", mi.fn))
- }
- if mtyp.NumOut() != 0 {
- panic(fmt.Sprintf("marker function %#v must not have results", mi.fn))
- }
- for a := 1; a < mtyp.NumIn(); a++ {
- in := mtyp.In(a)
- if mi.variadic && a == mtyp.NumIn()-1 {
- in = in.Elem() // for ...T, convert to T
- }
- mi.paramTypes = append(mi.paramTypes, in)
- c := makeConverter(in)
- mi.converters = append(mi.converters, c)
- }
- return mi
-}
-
// ---- converters ----
// converter is the signature of argument converters.
// A converter should return an error rather than calling marker.errorf().
-type converter func(marker, interface{}) (interface{}, error)
+type converter func(marker, any) (any, error)
// Types with special conversions.
var (
@@ -1064,28 +1208,39 @@ var (
wantErrorType = reflect.TypeOf(wantError{})
)
-func makeConverter(paramType reflect.Type) converter {
+func convert(mark marker, arg any, paramType reflect.Type) (any, error) {
+ if paramType == goldenType {
+ id, ok := arg.(expect.Identifier)
+ if !ok {
+ return nil, fmt.Errorf("invalid input type %T: golden key must be an identifier", arg)
+ }
+ return mark.run.test.getGolden(id), nil
+ }
+ if id, ok := arg.(expect.Identifier); ok {
+ if arg, ok := mark.run.values[id]; ok {
+ if !reflect.TypeOf(arg).AssignableTo(paramType) {
+ return nil, fmt.Errorf("cannot convert %v to %s", arg, paramType)
+ }
+ return arg, nil
+ }
+ }
+ if reflect.TypeOf(arg).AssignableTo(paramType) {
+ return arg, nil // no conversion required
+ }
switch paramType {
- case goldenType:
- return goldenConverter
case locationType:
- return locationConverter
+ return convertLocation(mark, arg)
case wantErrorType:
- return wantErrorConverter
+ return convertWantError(mark, arg)
default:
- return func(_ marker, arg interface{}) (interface{}, error) {
- if argType := reflect.TypeOf(arg); argType != paramType {
- return nil, fmt.Errorf("cannot convert type %s to %s", argType, paramType)
- }
- return arg, nil
- }
+ return nil, fmt.Errorf("cannot convert %v to %s", arg, paramType)
}
}
-// locationConverter converts a string argument into the protocol location
-// corresponding to the first position of the string in the line preceding the
-// note.
-func locationConverter(mark marker, arg interface{}) (interface{}, error) {
+// convertLocation converts a string or regexp argument into the protocol
+// location corresponding to the first position of the string (or first match
+// of the regexp) in the line preceding the note.
+func convertLocation(mark marker, arg any) (protocol.Location, error) {
switch arg := arg.(type) {
case string:
startOff, preceding, m, err := linePreceding(mark.run, mark.note.Pos)
@@ -1094,20 +1249,14 @@ func locationConverter(mark marker, arg interface{}) (interface{}, error) {
}
idx := bytes.Index(preceding, []byte(arg))
if idx < 0 {
- return nil, fmt.Errorf("substring %q not found in %q", arg, preceding)
+ return protocol.Location{}, fmt.Errorf("substring %q not found in %q", arg, preceding)
}
off := startOff + idx
return m.OffsetLocation(off, off+len(arg))
case *regexp.Regexp:
return findRegexpInLine(mark.run, mark.note.Pos, arg)
- case expect.Identifier:
- loc, ok := mark.run.locations[arg]
- if !ok {
- return nil, fmt.Errorf("no location named %q", arg)
- }
- return loc, nil
default:
- return nil, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg)
+ return protocol.Location{}, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg)
}
}
@@ -1155,22 +1304,22 @@ func linePreceding(run *markerTestRun, pos token.Pos) (int, []byte, *protocol.Ma
return startOff, m.Content[startOff:endOff], m, nil
}
-// wantErrorConverter converts a string, regexp, or identifier
+// convertWantError converts a string, regexp, or identifier
// argument into a wantError. The string is a substring of the
// expected error, the regexp is a pattern than matches the expected
// error, and the identifier is a golden file containing the expected
// error.
-func wantErrorConverter(mark marker, arg interface{}) (interface{}, error) {
+func convertWantError(mark marker, arg any) (wantError, error) {
switch arg := arg.(type) {
case string:
return wantError{substr: arg}, nil
case *regexp.Regexp:
return wantError{pattern: arg}, nil
case expect.Identifier:
- golden := mark.run.test.getGolden(string(arg))
+ golden := mark.run.test.getGolden(arg)
return wantError{golden: golden}, nil
default:
- return nil, fmt.Errorf("cannot convert %T to wantError (want: string, regexp, or identifier)", arg)
+ return wantError{}, fmt.Errorf("cannot convert %T to wantError (want: string, regexp, or identifier)", arg)
}
}
@@ -1230,17 +1379,6 @@ func (we wantError) check(mark marker, err error) {
}
}
-// goldenConverter converts an identifier into the Golden directory of content
-// prefixed by @ in the test archive file.
-func goldenConverter(mark marker, arg interface{}) (interface{}, error) {
- switch arg := arg.(type) {
- case expect.Identifier:
- return mark.run.test.getGolden(string(arg)), nil
- default:
- return nil, fmt.Errorf("invalid input type %T: golden key must be an identifier", arg)
- }
-}
-
// checkChangedFiles compares the files changed by an operation with their expected (golden) state.
func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) {
// Check changed files match expectations.
@@ -1268,26 +1406,157 @@ func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) {
// ---- marker functions ----
+// TODO(rfindley): consolidate documentation of these markers. They are already
+// documented above, so much of the documentation here is redundant.
+
+// completionItem is a simplified summary of a completion item.
+type completionItem struct {
+ Label, Detail, Kind, Documentation string
+}
+
+func completionItemMarker(mark marker, label string, other ...string) completionItem {
+ if len(other) > 3 {
+ mark.errorf("too many arguments to @item: expect at most 4")
+ }
+ item := completionItem{
+ Label: label,
+ }
+ if len(other) > 0 {
+ item.Detail = other[0]
+ }
+ if len(other) > 1 {
+ item.Kind = other[1]
+ }
+ if len(other) > 2 {
+ item.Documentation = other[2]
+ }
+ return item
+}
+
+func rankMarker(mark marker, src protocol.Location, items ...completionItem) {
+ list := mark.run.env.Completion(src)
+ var got []string
+ // Collect results that are present in items, preserving their order.
+ for _, g := range list.Items {
+ for _, w := range items {
+ if g.Label == w.Label {
+ got = append(got, g.Label)
+ break
+ }
+ }
+ }
+ var want []string
+ for _, w := range items {
+ want = append(want, w.Label)
+ }
+ if diff := cmp.Diff(want, got); diff != "" {
+ mark.errorf("completion rankings do not match (-want +got):\n%s", diff)
+ }
+}
+
+func ranklMarker(mark marker, src protocol.Location, labels ...string) {
+ list := mark.run.env.Completion(src)
+ var got []string
+ // Collect results that are present in items, preserving their order.
+ for _, g := range list.Items {
+ for _, label := range labels {
+ if g.Label == label {
+ got = append(got, g.Label)
+ break
+ }
+ }
+ }
+ if diff := cmp.Diff(labels, got); diff != "" {
+ mark.errorf("completion rankings do not match (-want +got):\n%s", diff)
+ }
+}
+
+func snippetMarker(mark marker, src protocol.Location, item completionItem, want string) {
+ list := mark.run.env.Completion(src)
+ var (
+ found bool
+ got string
+ all []string // for errors
+ )
+ items := filterBuiltinsAndKeywords(list.Items)
+ for _, i := range items {
+ all = append(all, i.Label)
+ if i.Label == item.Label {
+ found = true
+ if i.TextEdit != nil {
+ got = i.TextEdit.NewText
+ }
+ break
+ }
+ }
+ if !found {
+ mark.errorf("no completion item found matching %s (got: %v)", item.Label, all)
+ return
+ }
+ if got != want {
+ mark.errorf("snippets do not match: got %q, want %q", got, want)
+ }
+}
+
// completeMarker implements the @complete marker, running
// textDocument/completion at the given src location and asserting that the
// results match the expected results.
-//
-// TODO(rfindley): for now, this is just a quick check against the expected
-// completion labels. We could do more by assembling richer completion items,
-// as is done in the old marker tests. Does that add value? If so, perhaps we
-// should support a variant form of the argument, labelOrItem, which allows the
-// string form or item form.
-func completeMarker(mark marker, src protocol.Location, want ...string) {
+func completeMarker(mark marker, src protocol.Location, want ...completionItem) {
list := mark.run.env.Completion(src)
- var got []string
- for _, item := range list.Items {
- got = append(got, item.Label)
+ items := filterBuiltinsAndKeywords(list.Items)
+ var got []completionItem
+ for i, item := range items {
+ simplified := completionItem{
+ Label: item.Label,
+ Detail: item.Detail,
+ Kind: fmt.Sprint(item.Kind),
+ }
+ if item.Documentation != nil {
+ switch v := item.Documentation.Value.(type) {
+ case string:
+ simplified.Documentation = v
+ case protocol.MarkupContent:
+ simplified.Documentation = strings.TrimSpace(v.Value) // trim newlines
+ }
+ }
+ // Support short-hand notation: if Detail, Kind, or Documentation are omitted from the
+ // item, don't match them.
+ if i < len(want) {
+ if want[i].Detail == "" {
+ simplified.Detail = ""
+ }
+ if want[i].Kind == "" {
+ simplified.Kind = ""
+ }
+ if want[i].Documentation == "" {
+ simplified.Documentation = ""
+ }
+ }
+ got = append(got, simplified)
+ }
+ if len(want) == 0 {
+ want = nil // got is nil if empty
}
if diff := cmp.Diff(want, got); diff != "" {
mark.errorf("Completion(...) returned unexpect results (-want +got):\n%s", diff)
}
}
+// filterBuiltinsAndKeywords filters out builtins and keywords from completion
+// results.
+//
+// It over-approximates, and does not detect if builtins are shadowed.
+func filterBuiltinsAndKeywords(items []protocol.CompletionItem) []protocol.CompletionItem {
+ keep := 0
+ for _, item := range items {
+ if types.Universe.Lookup(item.Label) == nil && token.Lookup(item.Label) == token.IDENT {
+ items[keep] = item
+ keep++
+ }
+ }
+ return items[:keep]
+}
+
// acceptCompletionMarker implements the @acceptCompletion marker, running
// textDocument/completion at the given src location and accepting the
// candidate with the given label. The resulting source must match the provided
@@ -1305,7 +1574,7 @@ func acceptCompletionMarker(mark marker, src protocol.Location, label string, go
mark.errorf("Completion(...) did not return an item labeled %q", label)
return
}
- filename := mark.run.env.Sandbox.Workdir.URIToPath(mark.uri())
+ filename := mark.path()
mapper, err := mark.run.env.Editor.Mapper(filename)
if err != nil {
mark.errorf("Editor.Mapper(%s) failed: %v", filename, err)
@@ -1338,6 +1607,55 @@ func defMarker(mark marker, src, dst protocol.Location) {
}
}
+func typedefMarker(mark marker, src, dst protocol.Location) {
+ got := mark.run.env.TypeDefinition(src)
+ if got != dst {
+ mark.errorf("type definition location does not match:\n\tgot: %s\n\twant %s",
+ mark.run.fmtLoc(got), mark.run.fmtLoc(dst))
+ }
+}
+
+func foldingRangeMarker(mark marker, g *Golden) {
+ env := mark.run.env
+ ranges, err := mark.server().FoldingRange(env.Ctx, &protocol.FoldingRangeParams{
+ TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()},
+ })
+ if err != nil {
+ mark.errorf("foldingRange failed: %v", err)
+ return
+ }
+ var edits []protocol.TextEdit
+ insert := func(line, char uint32, text string) {
+ pos := protocol.Position{Line: line, Character: char}
+ edits = append(edits, protocol.TextEdit{
+ Range: protocol.Range{
+ Start: pos,
+ End: pos,
+ },
+ NewText: text,
+ })
+ }
+ for i, rng := range ranges {
+ insert(rng.StartLine, rng.StartCharacter, fmt.Sprintf("<%d kind=%q>", i, rng.Kind))
+ insert(rng.EndLine, rng.EndCharacter, fmt.Sprintf("%d>", i))
+ }
+ filename := mark.path()
+ mapper, err := env.Editor.Mapper(filename)
+ if err != nil {
+ mark.errorf("Editor.Mapper(%s) failed: %v", filename, err)
+ return
+ }
+ got, _, err := source.ApplyProtocolEdits(mapper, edits)
+ if err != nil {
+ mark.errorf("ApplyProtocolEdits failed: %v", err)
+ return
+ }
+ want, _ := g.Get(mark.run.env.T, "", got)
+ if diff := compare.Bytes(want, got); diff != "" {
+ mark.errorf("foldingRange mismatch:\n%s", diff)
+ }
+}
+
// formatMarker implements the @format marker.
func formatMarker(mark marker, golden *Golden) {
edits, err := mark.server().Formatting(mark.run.env.Ctx, &protocol.DocumentFormattingParams{
@@ -1348,7 +1666,7 @@ func formatMarker(mark marker, golden *Golden) {
got = []byte(err.Error() + "\n") // all golden content is newline terminated
} else {
env := mark.run.env
- filename := env.Sandbox.Workdir.URIToPath(mark.uri())
+ filename := mark.path()
mapper, err := env.Editor.Mapper(filename)
if err != nil {
mark.errorf("Editor.Mapper(%s) failed: %v", filename, err)
@@ -1372,6 +1690,32 @@ func formatMarker(mark marker, golden *Golden) {
}
}
+func highlightMarker(mark marker, src protocol.Location, dsts ...protocol.Location) {
+ highlights := mark.run.env.DocumentHighlight(src)
+ var got []protocol.Range
+ for _, h := range highlights {
+ got = append(got, h.Range)
+ }
+
+ var want []protocol.Range
+ for _, d := range dsts {
+ want = append(want, d.Range)
+ }
+
+ sortRanges := func(s []protocol.Range) {
+ sort.Slice(s, func(i, j int) bool {
+ return protocol.CompareRange(s[i], s[j]) < 0
+ })
+ }
+
+ sortRanges(got)
+ sortRanges(want)
+
+ if diff := cmp.Diff(want, got); diff != "" {
+ mark.errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", src, diff)
+ }
+}
+
// hoverMarker implements the @hover marker, running textDocument/hover at the
// given src location and asserting that the resulting hover is over the dst
// location (typically a span surrounding src), and that the markdown content
@@ -1402,14 +1746,7 @@ func hoverMarker(mark marker, src, dst protocol.Location, golden *Golden) {
// locMarker implements the @loc marker. It is executed before other
// markers, so that locations are available.
-func locMarker(mark marker, name expect.Identifier, loc protocol.Location) {
- if prev, dup := mark.run.locations[name]; dup {
- mark.errorf("location %q already declared at %s",
- name, mark.run.fmtLoc(prev))
- return
- }
- mark.run.locations[name] = loc
-}
+func locMarker(mark marker, loc protocol.Location) protocol.Location { return loc }
// diagMarker implements the @diag marker. It eliminates diagnostics from
// the observed set in mark.test.
@@ -1438,8 +1775,8 @@ func removeDiagnostic(mark marker, loc protocol.Location, re *regexp.Regexp) (pr
}
// renameMarker implements the @rename(location, new, golden) marker.
-func renameMarker(mark marker, loc protocol.Location, newName expect.Identifier, golden *Golden) {
- changed, err := rename(mark.run.env, loc, string(newName))
+func renameMarker(mark marker, loc protocol.Location, newName string, golden *Golden) {
+ changed, err := rename(mark.run.env, loc, newName)
if err != nil {
mark.errorf("rename failed: %v. (Use @renameerr for expected errors.)", err)
return
@@ -1448,11 +1785,22 @@ func renameMarker(mark marker, loc protocol.Location, newName expect.Identifier,
}
// renameErrMarker implements the @renamererr(location, new, error) marker.
-func renameErrMarker(mark marker, loc protocol.Location, newName expect.Identifier, wantErr wantError) {
- _, err := rename(mark.run.env, loc, string(newName))
+func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr wantError) {
+ _, err := rename(mark.run.env, loc, newName)
wantErr.check(mark, err)
}
+func signatureMarker(mark marker, src protocol.Location, want string) {
+ got := mark.run.env.SignatureHelp(src)
+ if got == nil || len(got.Signatures) != 1 {
+ mark.errorf("signatureHelp = %v, want exactly 1 signature", got)
+ return
+ }
+ if got := got.Signatures[0].Label; got != want {
+ mark.errorf("signatureHelp: got %q, want %q", got, want)
+ }
+}
+
// rename returns the new contents of the files that would be modified
// by renaming the identifier at loc to newName.
func rename(env *Env, loc protocol.Location, newName string) (map[string][]byte, error) {
@@ -1541,6 +1889,57 @@ func codeActionErrMarker(mark marker, actionKind string, start, end protocol.Loc
wantErr.check(mark, err)
}
+// codeLensesMarker runs the @codelenses() marker, collecting @codelens marks
+// in the current file and comparing with the result of the
+// textDocument/codeLens RPC.
+func codeLensesMarker(mark marker) {
+ type codeLens struct {
+ Range protocol.Range
+ Title string
+ }
+
+ lenses := mark.run.env.CodeLens(mark.path())
+ var got []codeLens
+ for _, lens := range lenses {
+ title := ""
+ if lens.Command != nil {
+ title = lens.Command.Title
+ }
+ got = append(got, codeLens{lens.Range, title})
+ }
+
+ var want []codeLens
+ mark.consumeExtraNotes("codelens", actionMarkerFunc(func(mark marker, loc protocol.Location, title string) {
+ want = append(want, codeLens{loc.Range, title})
+ }))
+
+ for _, s := range [][]codeLens{got, want} {
+ sort.Slice(s, func(i, j int) bool {
+ li, lj := s[i], s[j]
+ if c := protocol.CompareRange(li.Range, lj.Range); c != 0 {
+ return c < 0
+ }
+ return li.Title < lj.Title
+ })
+ }
+
+ if diff := cmp.Diff(want, got); diff != "" {
+ mark.errorf("codelenses: unexpected diff (-want +got):\n%s", diff)
+ }
+}
+
+// consumeExtraNotes runs the provided func for each extra note with the given
+// name, and deletes all matching notes.
+func (mark marker) consumeExtraNotes(name string, f func(marker)) {
+ uri := mark.uri()
+ notes := mark.run.extraNotes[uri][name]
+ delete(mark.run.extraNotes[uri], name)
+
+ for _, note := range notes {
+ f(marker{run: mark.run, note: note})
+ }
+}
+
// suggestedfixMarker implements the @suggestedfix(location, regexp,
// kind, golden) marker. It acts like @diag(location, regexp), to set
// the expectation of a diagnostic, but then it applies the first code
@@ -1727,7 +2126,7 @@ func symbolMarker(mark marker, golden *Golden) {
if err != nil {
mark.run.env.T.Fatal(err)
}
- if _, ok := symbol.(map[string]interface{})["location"]; ok {
+ if _, ok := symbol.(map[string]any)["location"]; ok {
// This case is not reached because Editor initialization
// enables HierarchicalDocumentSymbolSupport.
// TODO(adonovan): test this too.
diff --git a/gopls/internal/lsp/regtest/options.go b/gopls/internal/lsp/regtest/options.go
index f55fd5b1150..7084d621f81 100644
--- a/gopls/internal/lsp/regtest/options.go
+++ b/gopls/internal/lsp/regtest/options.go
@@ -4,7 +4,10 @@
package regtest
-import "golang.org/x/tools/gopls/internal/lsp/fake"
+import (
+ "golang.org/x/tools/gopls/internal/lsp/fake"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+)
type runConfig struct {
editor fake.EditorConfig
@@ -121,3 +124,11 @@ func InGOPATH() RunOption {
opts.sandbox.InGoPath = true
})
}
+
+// MessageResponder configures the editor to respond to
+// window/showMessageRequest messages using the provided function.
+func MessageResponder(f func(*protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error)) RunOption {
+ return optionSetter(func(opts *runConfig) {
+ opts.editor.MessageResponder = f
+ })
+}
diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go
index d0df0869718..0220d30d390 100644
--- a/gopls/internal/lsp/regtest/wrappers.go
+++ b/gopls/internal/lsp/regtest/wrappers.go
@@ -155,9 +155,20 @@ func (e *Env) SaveBufferWithoutActions(name string) {
// GoToDefinition goes to definition in the editor, calling t.Fatal on any
// error. It returns the path and position of the resulting jump.
+//
+// TODO(rfindley): rename this to just 'Definition'.
func (e *Env) GoToDefinition(loc protocol.Location) protocol.Location {
e.T.Helper()
- loc, err := e.Editor.GoToDefinition(e.Ctx, loc)
+ loc, err := e.Editor.Definition(e.Ctx, loc)
+ if err != nil {
+ e.T.Fatal(err)
+ }
+ return loc
+}
+
+func (e *Env) TypeDefinition(loc protocol.Location) protocol.Location {
+ e.T.Helper()
+ loc, err := e.Editor.TypeDefinition(e.Ctx, loc)
if err != nil {
e.T.Fatal(err)
}
@@ -245,7 +256,7 @@ func (e *Env) RunGenerate(dir string) {
if err := e.Editor.RunGenerate(e.Ctx, dir); err != nil {
e.T.Fatal(err)
}
- e.Await(NoOutstandingWork())
+ e.Await(NoOutstandingWork(IgnoreTelemetryPromptWork))
// Ideally the fake.Workspace would handle all synthetic file watching, but
// we help it out here as we need to wait for the generate command to
// complete before checking the filesystem.
diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go
index 12ee8dae903..825e654c2cc 100644
--- a/gopls/internal/lsp/semantic.go
+++ b/gopls/internal/lsp/semantic.go
@@ -76,8 +76,8 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu
ctx: ctx,
metadataSource: snapshot,
rng: rng,
- tokTypes: s.session.Options().SemanticTypes,
- tokMods: s.session.Options().SemanticMods,
+ tokTypes: snapshot.Options().SemanticTypes,
+ tokMods: snapshot.Options().SemanticMods,
}
add := func(line, start uint32, len uint32) {
e.add(line, start, len, tokMacro, nil)
@@ -108,8 +108,8 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu
ti: pkg.GetTypesInfo(),
pkg: pkg,
fset: pkg.FileSet(),
- tokTypes: s.session.Options().SemanticTypes,
- tokMods: s.session.Options().SemanticMods,
+ tokTypes: snapshot.Options().SemanticTypes,
+ tokMods: snapshot.Options().SemanticMods,
noStrings: snapshot.Options().NoSemanticString,
noNumbers: snapshot.Options().NoSemanticNumber,
}
diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go
index 94275b96343..a236779962f 100644
--- a/gopls/internal/lsp/server.go
+++ b/gopls/internal/lsp/server.go
@@ -26,7 +26,7 @@ const concurrentAnalyses = 1
// NewServer creates an LSP server and binds it to handle incoming client
// messages on the supplied stream.
-func NewServer(session *cache.Session, client protocol.ClientCloser) *Server {
+func NewServer(session *cache.Session, client protocol.ClientCloser, options *source.Options) *Server {
return &Server{
diagnostics: map[span.URI]*fileReports{},
gcOptimizationDetails: make(map[source.PackageID]struct{}),
@@ -36,6 +36,7 @@ func NewServer(session *cache.Session, client protocol.ClientCloser) *Server {
client: client,
diagnosticsSema: make(chan struct{}, concurrentAnalyses),
progress: progress.NewTracker(client),
+ options: options,
}
}
@@ -115,6 +116,10 @@ type Server struct {
// terminated with the StopProfile command.
ongoingProfileMu sync.Mutex
ongoingProfile *os.File // if non-nil, an ongoing profile is writing to this file
+
+ // Track most recently requested options.
+ optionsMu sync.Mutex
+ options *source.Options
}
func (s *Server) workDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error {
diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go
index 60425db2c5c..0fd6b07c54b 100644
--- a/gopls/internal/lsp/source/api_json.go
+++ b/gopls/internal/lsp/source/api_json.go
@@ -146,6 +146,13 @@ var GeneratedAPIJSON = &APIJSON{
Status: "experimental",
Hierarchy: "ui.completion",
},
+ {
+ Name: "completeFunctionCalls",
+ Type: "bool",
+ Doc: "completeFunctionCalls enables function call completion.\n\nWhen completing a statement, or when a function return type matches the\nexpected of the expression being completed, completion may suggest call\nexpressions (i.e. may include parentheses).\n",
+ Default: "true",
+ Hierarchy: "ui.completion",
+ },
{
Name: "importShortcut",
Type: "enum",
@@ -218,6 +225,11 @@ var GeneratedAPIJSON = &APIJSON{
EnumKeys: EnumKeys{
ValueType: "bool",
Keys: []EnumKey{
+ {
+ 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: "\"asmdecl\"",
Doc: "report mismatches between assembly files and Go declarations",
@@ -269,8 +281,8 @@ var GeneratedAPIJSON = &APIJSON{
Default: "true",
},
{
- Name: "\"defer\"",
- Doc: "report common mistakes in defer statements\n\nThe defer 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)) }()",
+ 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",
},
{
@@ -709,6 +721,12 @@ var GeneratedAPIJSON = &APIJSON{
Doc: "Ask the server to add an import path to a given Go file. The method will\ncall applyEdit on the client so that clients don't have to apply the edit\nthemselves.",
ArgDoc: "{\n\t// ImportPath is the target import path that should\n\t// be added to the URI file\n\t\"ImportPath\": string,\n\t// URI is the file that the ImportPath should be\n\t// added to\n\t\"URI\": string,\n}",
},
+ {
+ Command: "gopls.add_telemetry_counters",
+ Title: "update the given telemetry counters.",
+ Doc: "Gopls will prepend \"fwd/\" to all the counters updated using this command\nto avoid conflicts with other counters gopls collects.",
+ ArgDoc: "{\n\t// Names and Values must have the same length.\n\t\"Names\": []string,\n\t\"Values\": []int64,\n}",
+ },
{
Command: "gopls.apply_fix",
Title: "Apply a fix",
@@ -732,7 +750,7 @@ var GeneratedAPIJSON = &APIJSON{
Title: "Get known vulncheck result",
Doc: "Fetch the result of latest vulnerability check (`govulncheck`).",
ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}",
- ResultDoc: "map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/govulncheck.Result",
+ ResultDoc: "map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result",
},
{
Command: "gopls.gc_details",
@@ -766,6 +784,11 @@ var GeneratedAPIJSON = &APIJSON{
ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}",
ResultDoc: "{\n\t// Packages is a list of packages relative\n\t// to the URIArg passed by the command request.\n\t// In other words, it omits paths that are already\n\t// imported or cannot be imported due to compiler\n\t// restrictions.\n\t\"Packages\": []string,\n}",
},
+ {
+ Command: "gopls.maybe_prompt_for_telemetry",
+ Title: "checks for the right conditions, and then prompts",
+ Doc: "the user to ask if they want to enable Go telemetry uploading. If the user\nresponds 'Yes', the telemetry mode is set to \"on\".",
+ },
{
Command: "gopls.mem_stats",
Title: "fetch memory statistics",
@@ -798,7 +821,7 @@ var GeneratedAPIJSON = &APIJSON{
},
{
Command: "gopls.run_govulncheck",
- Title: "Run govulncheck.",
+ Title: "Run vulncheck.",
Doc: "Run vulnerability check (`govulncheck`).",
ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}",
ResultDoc: "{\n\t// Token holds the progress token for LSP workDone reporting of the vulncheck\n\t// invocation.\n\t\"Token\": interface{},\n}",
@@ -891,7 +914,7 @@ var GeneratedAPIJSON = &APIJSON{
},
{
Lens: "run_govulncheck",
- Title: "Run govulncheck.",
+ Title: "Run vulncheck.",
Doc: "Run vulnerability check (`govulncheck`).",
},
{
@@ -916,6 +939,12 @@ var GeneratedAPIJSON = &APIJSON{
},
},
Analyzers: []*AnalyzerJSON{
+ {
+ 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.",
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appends",
+ Default: true,
+ },
{
Name: "asmdecl",
Doc: "report mismatches between assembly files and Go declarations",
@@ -977,8 +1006,9 @@ var GeneratedAPIJSON = &APIJSON{
Default: true,
},
{
- Name: "defer",
- Doc: "report common mistakes in defer statements\n\nThe defer 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)) }()",
+ 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)) }()",
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers",
Default: true,
},
{
diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go
index e0a221f6017..4044d8446fd 100644
--- a/gopls/internal/lsp/source/completion/completion.go
+++ b/gopls/internal/lsp/source/completion/completion.go
@@ -104,15 +104,16 @@ type CompletionItem struct {
// completionOptions holds completion specific configuration.
type completionOptions struct {
- unimported bool
- documentation bool
- fullDocumentation bool
- placeholders bool
- literal bool
- snippets bool
- postfix bool
- matcher source.Matcher
- budget time.Duration
+ unimported bool
+ documentation bool
+ fullDocumentation bool
+ placeholders bool
+ literal bool
+ snippets bool
+ postfix bool
+ matcher source.Matcher
+ budget time.Duration
+ completeFunctionCalls bool
}
// Snippet is a convenience returns the snippet if available, otherwise
@@ -232,6 +233,15 @@ type completer struct {
// mapper converts the positions in the file from which the completion originated.
mapper *protocol.Mapper
+ // startTime is when we started processing this completion request. It does
+ // not include any time the request spent in the queue.
+ //
+ // Note: in CL 503016, startTime move to *after* type checking, but it was
+ // subsequently determined that it was better to keep setting it *before*
+ // type checking, so that the completion budget best approximates the user
+ // experience. See golang/go#62665 for more details.
+ startTime time.Time
+
// scopes contains all scopes defined by nodes in our path,
// including nil values for nodes that don't defined a scope. It
// also includes our package scope and the universal scope at the
@@ -437,6 +447,8 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
ctx, done := event.Start(ctx, "completion.Completion")
defer done()
+ startTime := time.Now()
+
pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil || pgf.File.Package == token.NoPos {
// If we can't parse this file or find position for the package
@@ -451,6 +463,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
}
return items, surrounding, nil
}
+
pos, err := pgf.PositionPos(protoPos)
if err != nil {
return nil, nil, err
@@ -531,24 +544,27 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
enabled: opts.DeepCompletion,
},
opts: &completionOptions{
- matcher: opts.Matcher,
- unimported: opts.CompleteUnimported,
- documentation: opts.CompletionDocumentation && opts.HoverKind != source.NoDocumentation,
- fullDocumentation: opts.HoverKind == source.FullDocumentation,
- placeholders: opts.UsePlaceholders,
- literal: opts.LiteralCompletions && opts.InsertTextFormat == protocol.SnippetTextFormat,
- budget: opts.CompletionBudget,
- snippets: opts.InsertTextFormat == protocol.SnippetTextFormat,
- postfix: opts.ExperimentalPostfixCompletions,
+ matcher: opts.Matcher,
+ unimported: opts.CompleteUnimported,
+ documentation: opts.CompletionDocumentation && opts.HoverKind != source.NoDocumentation,
+ fullDocumentation: opts.HoverKind == source.FullDocumentation,
+ placeholders: opts.UsePlaceholders,
+ literal: opts.LiteralCompletions && opts.InsertTextFormat == protocol.SnippetTextFormat,
+ budget: opts.CompletionBudget,
+ snippets: opts.InsertTextFormat == protocol.SnippetTextFormat,
+ postfix: opts.ExperimentalPostfixCompletions,
+ completeFunctionCalls: opts.CompleteFunctionCalls,
},
// default to a matcher that always matches
matcher: prefixMatcher(""),
methodSetCache: make(map[methodSetKey]*types.MethodSet),
mapper: pgf.Mapper,
+ startTime: startTime,
scopes: scopes,
}
ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
// Compute the deadline for this operation. Deadline is relative to the
// search operation, not the entire completion RPC, as the work up until this
@@ -562,15 +578,12 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
// Don't overload the context with this deadline, as we don't want to
// conflate user cancellation (=fail the operation) with our time limit
// (=stop searching and succeed with partial results).
- start := time.Now()
var deadline *time.Time
if c.opts.budget > 0 {
- d := start.Add(c.opts.budget)
+ d := startTime.Add(c.opts.budget)
deadline = &d
}
- defer cancel()
-
if surrounding := c.containingIdent(pgf.Src); surrounding != nil {
c.setSurrounding(surrounding)
}
@@ -583,17 +596,30 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
}
// Deep search collected candidates and their members for more candidates.
- c.deepSearch(ctx, start, deadline)
+ c.deepSearch(ctx, 1, deadline)
+
+ // At this point we have a sufficiently complete set of results, and want to
+ // return as close to the completion budget as possible. Previously, we
+ // avoided cancelling the context because it could result in partial results
+ // for e.g. struct fields. At this point, we have a minimal valid set of
+ // candidates, and so truncating due to context cancellation is acceptable.
+ if c.opts.budget > 0 {
+ timeoutDuration := time.Until(c.startTime.Add(c.opts.budget))
+ ctx, cancel = context.WithTimeout(ctx, timeoutDuration)
+ defer cancel()
+ }
for _, callback := range c.completionCallbacks {
- if err := c.snapshot.RunProcessEnvFunc(ctx, callback); err != nil {
- return nil, nil, err
+ if deadline == nil || time.Now().Before(*deadline) {
+ if err := c.snapshot.RunProcessEnvFunc(ctx, callback); err != nil {
+ return nil, nil, err
+ }
}
}
// Search candidates populated by expensive operations like
// unimportedMembers etc. for more completion items.
- c.deepSearch(ctx, start, deadline)
+ c.deepSearch(ctx, 0, deadline)
// Statement candidates offer an entire statement in certain contexts, as
// opposed to a single object. Add statement candidates last because they
@@ -1270,7 +1296,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
Label: id.Name,
Detail: fmt.Sprintf("%s (from %q)", strings.ToLower(tok.String()), m.PkgPath),
InsertText: id.Name,
- Score: unimportedScore(relevances[path]),
+ Score: float64(score) * unimportedScore(relevances[path]),
}
switch tok {
case token.FUNC:
@@ -1294,32 +1320,18 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
// For functions, add a parameter snippet.
if fn != nil {
- var sn snippet.Builder
- sn.WriteText(id.Name)
-
- paramList := func(open, close string, list *ast.FieldList) {
+ paramList := func(list *ast.FieldList) []string {
+ var params []string
if list != nil {
var cfg printer.Config // slight overkill
- var nparams int
param := func(name string, typ ast.Expr) {
- if nparams > 0 {
- sn.WriteText(", ")
- }
- nparams++
- if c.opts.placeholders {
- sn.WritePlaceholder(func(b *snippet.Builder) {
- var buf strings.Builder
- buf.WriteString(name)
- buf.WriteByte(' ')
- cfg.Fprint(&buf, token.NewFileSet(), typ)
- b.WriteText(buf.String())
- })
- } else {
- sn.WriteText(name)
- }
+ var buf strings.Builder
+ buf.WriteString(name)
+ buf.WriteByte(' ')
+ cfg.Fprint(&buf, token.NewFileSet(), typ)
+ params = append(params, buf.String())
}
- sn.WriteText(open)
for _, field := range list.List {
if field.Names != nil {
for _, name := range field.Names {
@@ -1329,13 +1341,14 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
param("_", field.Type)
}
}
- sn.WriteText(close)
}
+ return params
}
- paramList("[", "]", typeparams.ForFuncType(fn.Type))
- paramList("(", ")", fn.Type.Params)
-
+ tparams := paramList(fn.Type.TypeParams)
+ params := paramList(fn.Type.Params)
+ var sn snippet.Builder
+ c.functionCallSnippet(id.Name, tparams, params, &sn)
item.snippet = &sn
}
@@ -1368,6 +1381,10 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
ctx, cancel := context.WithCancel(ctx)
var mu sync.Mutex
add := func(pkgExport imports.PackageExport) {
+ if ignoreUnimportedCompletion(pkgExport.Fix) {
+ return
+ }
+
mu.Lock()
defer mu.Unlock()
// TODO(adonovan): what if the actual package has a vendor/ prefix?
@@ -1419,6 +1436,13 @@ func (c *completer) packageMembers(pkg *types.Package, score float64, imp *impor
}
}
+// ignoreUnimportedCompletion reports whether an unimported completion
+// resulting in the given import should be ignored.
+func ignoreUnimportedCompletion(fix *imports.ImportFix) bool {
+ // golang/go#60062: don't add unimported completion to golang.org/toolchain.
+ return fix != nil && strings.HasPrefix(fix.StmtInfo.ImportPath, "golang.org/toolchain")
+}
+
func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *importInfo, cb func(candidate)) {
mset := c.methodSetCache[methodSetKey{typ, addressable}]
if mset == nil {
@@ -1731,6 +1755,9 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru
var mu sync.Mutex
add := func(pkg imports.ImportFix) {
+ if ignoreUnimportedCompletion(&pkg) {
+ return
+ }
mu.Lock()
defer mu.Unlock()
if _, ok := seen[pkg.IdentName]; ok {
diff --git a/gopls/internal/lsp/source/completion/deep_completion.go b/gopls/internal/lsp/source/completion/deep_completion.go
index 66309530e73..fac11bf4117 100644
--- a/gopls/internal/lsp/source/completion/deep_completion.go
+++ b/gopls/internal/lsp/source/completion/deep_completion.go
@@ -113,7 +113,7 @@ func (s *deepCompletionState) newPath(cand candidate, obj types.Object) []types.
// deepSearch searches a candidate and its subordinate objects for completion
// items if deep completion is enabled and adds the valid candidates to
// completion items.
-func (c *completer) deepSearch(ctx context.Context, start time.Time, deadline *time.Time) {
+func (c *completer) deepSearch(ctx context.Context, minDepth int, deadline *time.Time) {
defer func() {
// We can return early before completing the search, so be sure to
// clear out our queues to not impact any further invocations.
@@ -121,9 +121,25 @@ func (c *completer) deepSearch(ctx context.Context, start time.Time, deadline *t
c.deepState.nextQueue = c.deepState.nextQueue[:0]
}()
- first := true // always fully process the first set of candidates
- for len(c.deepState.nextQueue) > 0 && (first || deadline == nil || time.Now().Before(*deadline)) {
- first = false
+ depth := 0 // current depth being processed
+ // Stop reports whether we should stop the search immediately.
+ stop := func() bool {
+ // Context cancellation indicates that the actual completion operation was
+ // cancelled, so ignore minDepth and deadline.
+ select {
+ case <-ctx.Done():
+ return true
+ default:
+ }
+ // Otherwise, only stop if we've searched at least minDepth and reached the deadline.
+ return depth > minDepth && deadline != nil && time.Now().After(*deadline)
+ }
+
+ for len(c.deepState.nextQueue) > 0 {
+ depth++
+ if stop() {
+ return
+ }
c.deepState.thisQueue, c.deepState.nextQueue = c.deepState.nextQueue, c.deepState.thisQueue[:0]
outer:
@@ -172,17 +188,15 @@ func (c *completer) deepSearch(ctx context.Context, start time.Time, deadline *t
c.deepState.candidateCount++
if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 {
- spent := float64(time.Since(start)) / float64(c.opts.budget)
- select {
- case <-ctx.Done():
+ if stop() {
return
- default:
- // If we are almost out of budgeted time, no further elements
- // should be added to the queue. This ensures remaining time is
- // used for processing current queue.
- if !c.deepState.queueClosed && spent >= 0.85 {
- c.deepState.queueClosed = true
- }
+ }
+ spent := float64(time.Since(c.startTime)) / float64(c.opts.budget)
+ // If we are almost out of budgeted time, no further elements
+ // should be added to the queue. This ensures remaining time is
+ // used for processing current queue.
+ if !c.deepState.queueClosed && spent >= 0.85 {
+ c.deepState.queueClosed = true
}
}
diff --git a/gopls/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go
index f4ea767e9dc..2be485f6d85 100644
--- a/gopls/internal/lsp/source/completion/snippet.go
+++ b/gopls/internal/lsp/source/completion/snippet.go
@@ -51,6 +51,11 @@ func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snip
// functionCallSnippet calculates the snippet for function calls.
func (c *completer) functionCallSnippet(name string, tparams, params []string, snip *snippet.Builder) {
+ if !c.opts.completeFunctionCalls {
+ snip.WriteText(name)
+ return
+ }
+
// If there is no suffix then we need to reuse existing call parens
// "()" if present. If there is an identifier suffix then we always
// need to include "()" since we don't overwrite the suffix.
diff --git a/gopls/internal/lsp/source/definition.go b/gopls/internal/lsp/source/definition.go
index 90a432966b8..dd3feda70a2 100644
--- a/gopls/internal/lsp/source/definition.go
+++ b/gopls/internal/lsp/source/definition.go
@@ -6,6 +6,7 @@ package source
import (
"context"
+ "errors"
"fmt"
"go/ast"
"go/token"
@@ -58,6 +59,18 @@ func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position
return []protocol.Location{loc}, nil
}
+ // Handle the case where the cursor is in a linkname directive.
+ locations, err := LinknameDefinition(ctx, snapshot, fh, position)
+ if !errors.Is(err, ErrNoLinkname) {
+ return locations, err
+ }
+
+ // Handle the case where the cursor is in an embed directive.
+ locations, err = EmbedDefinition(pgf.Mapper, position)
+ if !errors.Is(err, ErrNoEmbed) {
+ return locations, err
+ }
+
// The general case: the cursor is on an identifier.
_, obj, _ := referencedObject(pkg, pgf, pos)
if obj == nil {
diff --git a/gopls/internal/lsp/source/embeddirective.go b/gopls/internal/lsp/source/embeddirective.go
new file mode 100644
index 00000000000..d4e85d7add2
--- /dev/null
+++ b/gopls/internal/lsp/source/embeddirective.go
@@ -0,0 +1,195 @@
+// 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 source
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+)
+
+// ErrNoEmbed is returned by EmbedDefinition when no embed
+// directive is found at a particular position.
+// As such it indicates that other definitions could be worth checking.
+var ErrNoEmbed = errors.New("no embed directive found")
+
+var errStopWalk = errors.New("stop walk")
+
+// EmbedDefinition finds a file matching the embed directive at pos in the mapped file.
+// If there is no embed directive at pos, returns ErrNoEmbed.
+// If multiple files match the embed pattern, one is picked at random.
+func EmbedDefinition(m *protocol.Mapper, pos protocol.Position) ([]protocol.Location, error) {
+ pattern, _ := parseEmbedDirective(m, pos)
+ if pattern == "" {
+ return nil, ErrNoEmbed
+ }
+
+ // Find the first matching file.
+ var match string
+ dir := filepath.Dir(m.URI.Filename())
+ err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
+ if e != nil {
+ return e
+ }
+ rel, err := filepath.Rel(dir, abs)
+ if err != nil {
+ return err
+ }
+ ok, err := filepath.Match(pattern, rel)
+ if err != nil {
+ return err
+ }
+ if ok && !d.IsDir() {
+ match = abs
+ return errStopWalk
+ }
+ return nil
+ })
+ if err != nil && !errors.Is(err, errStopWalk) {
+ return nil, err
+ }
+ if match == "" {
+ return nil, fmt.Errorf("%q does not match any files in %q", pattern, dir)
+ }
+
+ loc := protocol.Location{
+ URI: protocol.URIFromPath(match),
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 0, Character: 0},
+ },
+ }
+ return []protocol.Location{loc}, nil
+}
+
+// parseEmbedDirective attempts to parse a go:embed directive argument at pos.
+// If successful it return the directive argument and its range, else zero values are returned.
+func parseEmbedDirective(m *protocol.Mapper, pos protocol.Position) (string, protocol.Range) {
+ lineStart, err := m.PositionOffset(protocol.Position{Line: pos.Line, Character: 0})
+ if err != nil {
+ return "", protocol.Range{}
+ }
+ lineEnd, err := m.PositionOffset(protocol.Position{Line: pos.Line + 1, Character: 0})
+ if err != nil {
+ return "", protocol.Range{}
+ }
+
+ text := string(m.Content[lineStart:lineEnd])
+ if !strings.HasPrefix(text, "//go:embed") {
+ return "", protocol.Range{}
+ }
+ text = text[len("//go:embed"):]
+ offset := lineStart + len("//go:embed")
+
+ // Find the first pattern in text that covers the offset of the pos we are looking for.
+ findOffset, err := m.PositionOffset(pos)
+ if err != nil {
+ return "", protocol.Range{}
+ }
+ patterns, err := parseGoEmbed(text, offset)
+ if err != nil {
+ return "", protocol.Range{}
+ }
+ for _, p := range patterns {
+ if p.startOffset <= findOffset && findOffset <= p.endOffset {
+ // Found our match.
+ rng, err := m.OffsetRange(p.startOffset, p.endOffset)
+ if err != nil {
+ return "", protocol.Range{}
+ }
+ return p.pattern, rng
+ }
+ }
+
+ return "", protocol.Range{}
+}
+
+type fileEmbed struct {
+ pattern string
+ startOffset int
+ endOffset int
+}
+
+// parseGoEmbed patterns that come after the directive.
+//
+// Copied and adapted from go/build/read.go.
+// Replaced token.Position with start/end offset (including quotes if present).
+func parseGoEmbed(args string, offset int) ([]fileEmbed, error) {
+ trimBytes := func(n int) {
+ offset += n
+ args = args[n:]
+ }
+ trimSpace := func() {
+ trim := strings.TrimLeftFunc(args, unicode.IsSpace)
+ trimBytes(len(args) - len(trim))
+ }
+
+ var list []fileEmbed
+ for trimSpace(); args != ""; trimSpace() {
+ var path string
+ pathOffset := offset
+ Switch:
+ switch args[0] {
+ default:
+ i := len(args)
+ for j, c := range args {
+ if unicode.IsSpace(c) {
+ i = j
+ break
+ }
+ }
+ path = args[:i]
+ trimBytes(i)
+
+ case '`':
+ var ok bool
+ path, _, ok = strings.Cut(args[1:], "`")
+ if !ok {
+ return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
+ }
+ trimBytes(1 + len(path) + 1)
+
+ case '"':
+ i := 1
+ for ; i < len(args); i++ {
+ if args[i] == '\\' {
+ i++
+ continue
+ }
+ if args[i] == '"' {
+ q, err := strconv.Unquote(args[:i+1])
+ if err != nil {
+ return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1])
+ }
+ path = q
+ trimBytes(i + 1)
+ break Switch
+ }
+ }
+ if i >= len(args) {
+ return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
+ }
+ }
+
+ if args != "" {
+ r, _ := utf8.DecodeRuneInString(args)
+ if !unicode.IsSpace(r) {
+ return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
+ }
+ }
+ list = append(list, fileEmbed{
+ pattern: path,
+ startOffset: pathOffset,
+ endOffset: offset,
+ })
+ }
+ return list, nil
+}
diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go
index a6830751a91..95317833489 100644
--- a/gopls/internal/lsp/source/hover.go
+++ b/gopls/internal/lsp/source/hover.go
@@ -14,6 +14,8 @@ import (
"go/format"
"go/token"
"go/types"
+ "io/fs"
+ "path/filepath"
"strconv"
"strings"
"time"
@@ -121,6 +123,12 @@ func hover(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Po
}
}
+ // Handle hovering over embed directive argument.
+ pattern, embedRng := parseEmbedDirective(pgf.Mapper, pp)
+ if pattern != "" {
+ return hoverEmbed(fh, embedRng, pattern)
+ }
+
// Handle linkname directive by overriding what to look for.
var linkedRange *protocol.Range // range referenced by linkname directive, or nil
if pkgPath, name, offset := parseLinkname(ctx, snapshot, fh, pp); pkgPath != "" && name != "" {
@@ -625,6 +633,48 @@ func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Ran
}, nil
}
+// hoverEmbed computes hover information for a filepath.Match pattern.
+// Assumes that the pattern is relative to the location of fh.
+func hoverEmbed(fh FileHandle, rng protocol.Range, pattern string) (protocol.Range, *HoverJSON, error) {
+ s := &strings.Builder{}
+
+ dir := filepath.Dir(fh.URI().Filename())
+ var matches []string
+ err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
+ if e != nil {
+ return e
+ }
+ rel, err := filepath.Rel(dir, abs)
+ if err != nil {
+ return err
+ }
+ ok, err := filepath.Match(pattern, rel)
+ if err != nil {
+ return err
+ }
+ if ok && !d.IsDir() {
+ matches = append(matches, rel)
+ }
+ return nil
+ })
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+
+ for _, m := range matches {
+ // TODO: Renders each file as separate markdown paragraphs.
+ // If forcing (a single) newline is possible it might be more clear.
+ fmt.Fprintf(s, "%s\n\n", m)
+ }
+
+ json := &HoverJSON{
+ Signature: fmt.Sprintf("Embedding %q", pattern),
+ Synopsis: s.String(),
+ FullDocumentation: s.String(),
+ }
+ return rng, json, nil
+}
+
// inferredSignatureString is a wrapper around the types.ObjectString function
// that adds more information to inferred signatures. It will return an empty string
// if the passed types.Object is not a signature.
diff --git a/gopls/internal/lsp/source/inline.go b/gopls/internal/lsp/source/inline.go
index 6a8d57d412a..4e6e16f9159 100644
--- a/gopls/internal/lsp/source/inline.go
+++ b/gopls/internal/lsp/source/inline.go
@@ -12,14 +12,17 @@ import (
"go/ast"
"go/token"
"go/types"
+ "runtime/debug"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/gopls/internal/bug"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/diff"
+ "golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/refactor/inline"
)
@@ -56,7 +59,7 @@ loop:
return call, fn, nil
}
-func inlineCall(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) {
+func inlineCall(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (_ *token.FileSet, _ *analysis.SuggestedFix, err error) {
// Find enclosing static call.
callerPkg, callerPGF, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil {
@@ -86,7 +89,28 @@ func inlineCall(ctx context.Context, snapshot Snapshot, fh FileHandle, rng proto
if calleeDecl == nil {
return nil, nil, fmt.Errorf("can't find callee")
}
- callee, err := inline.AnalyzeCallee(calleePkg.FileSet(), calleePkg.GetTypes(), calleePkg.GetTypesInfo(), calleeDecl, calleePGF.Src)
+
+ // The inliner assumes that input is well-typed,
+ // but that is frequently not the case within gopls.
+ // Until we are able to harden the inliner,
+ // report panics as errors to avoid crashing the server.
+ bad := func(p Package) bool { return len(p.GetParseErrors())+len(p.GetTypeErrors()) > 0 }
+ if bad(calleePkg) || bad(callerPkg) {
+ defer func() {
+ if x := recover(); x != nil {
+ err = bug.Errorf("inlining failed unexpectedly: %v\nstack: %v",
+ x, debug.Stack())
+ }
+ }()
+ }
+
+ // Users can consult the gopls event log to see
+ // why a particular inlining strategy was chosen.
+ logf := func(format string, args ...any) {
+ event.Log(ctx, "inliner: "+fmt.Sprintf(format, args...))
+ }
+
+ callee, err := inline.AnalyzeCallee(logf, calleePkg.FileSet(), calleePkg.GetTypes(), calleePkg.GetTypesInfo(), calleeDecl, calleePGF.Src)
if err != nil {
return nil, nil, err
}
@@ -100,7 +124,8 @@ func inlineCall(ctx context.Context, snapshot Snapshot, fh FileHandle, rng proto
Call: call,
Content: callerPGF.Src,
}
- got, err := inline.Inline(caller, callee)
+
+ got, err := inline.Inline(logf, caller, callee)
if err != nil {
return nil, nil, err
}
diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go
index 2b91f834d6a..ec544fc31b1 100644
--- a/gopls/internal/lsp/source/options.go
+++ b/gopls/internal/lsp/source/options.go
@@ -16,6 +16,7 @@ import (
"time"
"golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/appends"
"golang.org/x/tools/go/analysis/passes/asmdecl"
"golang.org/x/tools/go/analysis/passes/assign"
"golang.org/x/tools/go/analysis/passes/atomic"
@@ -81,7 +82,7 @@ var (
// DefaultOptions is the options that are used for Gopls execution independent
// of any externally provided configuration (LSP initialization, command
// invocation, etc.).
-func DefaultOptions() *Options {
+func DefaultOptions(overrides ...func(*Options)) *Options {
optionsOnce.Do(func() {
var commands []string
for _, c := range command.Commands {
@@ -154,6 +155,7 @@ func DefaultOptions() *Options {
Matcher: Fuzzy,
CompletionBudget: 100 * time.Millisecond,
ExperimentalPostfixCompletions: true,
+ CompleteFunctionCalls: true,
},
Codelenses: map[string]bool{
string(command.Generate): true,
@@ -176,6 +178,8 @@ func DefaultOptions() *Options {
NewDiff: "new",
SubdirWatchPatterns: SubdirWatchPatternsAuto,
ReportAnalysisProgressAfter: 5 * time.Second,
+ TelemetryPrompt: false,
+ LinkifyShowMessage: false,
},
Hooks: Hooks{
// TODO(adonovan): switch to new diff.Strings implementation.
@@ -189,7 +193,13 @@ func DefaultOptions() *Options {
},
}
})
- return defaultOptions
+ options := defaultOptions.Clone()
+ for _, override := range overrides {
+ if override != nil {
+ override(options)
+ }
+ }
+ return options
}
// Options holds various configuration that affects Gopls execution, organized
@@ -379,6 +389,13 @@ type CompletionOptions struct {
// ExperimentalPostfixCompletions enables artificial method snippets
// such as "someSlice.sort!".
ExperimentalPostfixCompletions bool `status:"experimental"`
+
+ // CompleteFunctionCalls enables function call completion.
+ //
+ // When completing a statement, or when a function return type matches the
+ // expected of the expression being completed, completion may suggest call
+ // expressions (i.e. may include parentheses).
+ CompleteFunctionCalls bool
}
type DocumentationOptions struct {
@@ -669,6 +686,16 @@ type InternalOptions struct {
//
// It is intended to be used for testing only.
ReportAnalysisProgressAfter time.Duration
+
+ // TelemetryPrompt controls whether gopls prompts about enabling Go telemetry.
+ //
+ // Once the prompt is answered, gopls doesn't ask again, but TelemetryPrompt
+ // can prevent the question from ever being asked in the first place.
+ TelemetryPrompt bool
+
+ // LinkifyShowMessage controls whether the client wants gopls
+ // to linkify links in showMessage. e.g. [go.dev](https://go.dev).
+ LinkifyShowMessage bool
}
type SubdirWatchPatterns string
@@ -1167,6 +1194,8 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{})
" rebuild gopls with a more recent version of Go", result.Name, runtime.Version())
}
}
+ case "completeFunctionCalls":
+ result.setBool(&o.CompleteFunctionCalls)
case "semanticTokens":
result.setBool(&o.SemanticTokens)
@@ -1253,6 +1282,11 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{})
case "reportAnalysisProgressAfter":
result.setDuration(&o.ReportAnalysisProgressAfter)
+ case "telemetryPrompt":
+ result.setBool(&o.TelemetryPrompt)
+ case "linkifyShowMessage":
+ result.setBool(&o.LinkifyShowMessage)
+
// Replaced settings.
case "experimentalDisabledAnalyses":
result.deprecated("analyses")
@@ -1531,6 +1565,7 @@ func convenienceAnalyzers() map[string]*Analyzer {
func defaultAnalyzers() map[string]*Analyzer {
return map[string]*Analyzer{
// The traditional vet suite:
+ appends.Analyzer.Name: {Analyzer: appends.Analyzer, Enabled: true},
asmdecl.Analyzer.Name: {Analyzer: asmdecl.Analyzer, Enabled: true},
assign.Analyzer.Name: {Analyzer: assign.Analyzer, Enabled: true},
atomic.Analyzer.Name: {Analyzer: atomic.Analyzer, Enabled: true},
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index fe51cf0e5b6..f4a81922b2b 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -22,12 +22,12 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/types/objectpath"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/progress"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/lsp/source/methodsets"
"golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/internal/event/label"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/internal/gocommand"
@@ -148,7 +148,7 @@ type Snapshot interface {
// ModVuln returns import vulnerability analysis for the given go.mod URI.
// Concurrent requests are combined into a single command.
- ModVuln(ctx context.Context, modURI span.URI) (*govulncheck.Result, error)
+ ModVuln(ctx context.Context, modURI span.URI) (*vulncheck.Result, error)
// GoModForFile returns the URI of the go.mod file for the given URI.
GoModForFile(uri span.URI) span.URI
@@ -391,11 +391,11 @@ type View interface {
// Vulnerabilities returns known vulnerabilities for the given modfile.
// TODO(suzmue): replace command.Vuln with a different type, maybe
// https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary?
- Vulnerabilities(modfile ...span.URI) map[span.URI]*govulncheck.Result
+ Vulnerabilities(modfile ...span.URI) map[span.URI]*vulncheck.Result
// SetVulnerabilities resets the list of vulnerabilities that exists for the given modules
// required by modfile.
- SetVulnerabilities(modfile span.URI, vulncheckResult *govulncheck.Result)
+ SetVulnerabilities(modfile span.URI, vulncheckResult *vulncheck.Result)
// GoVersion returns the configured Go version for this view.
GoVersion() int
@@ -409,6 +409,9 @@ type View interface {
type FileSource interface {
// ReadFile returns the FileHandle for a given URI, either by
// reading the content of the file or by obtaining it from a cache.
+ //
+ // Invariant: ReadFile must only return an error in the case of context
+ // cancellation. If ctx.Err() is nil, the resulting error must also be nil.
ReadFile(ctx context.Context, uri span.URI) (FileHandle, error)
}
@@ -768,9 +771,9 @@ type FileHandle interface {
// FileIdentity returns a FileIdentity for the file, even if there was an
// error reading it.
FileIdentity() FileIdentity
- // Saved reports whether the file has the same content on disk:
+ // SameContentsOnDisk reports whether the file has the same content on disk:
// it is false for files open on an editor with unsaved edits.
- Saved() bool
+ SameContentsOnDisk() bool
// Version returns the file version, as defined by the LSP client.
// For on-disk file handles, Version returns 0.
Version() int32
diff --git a/gopls/internal/lsp/testdata/arraytype/array_type.go.in b/gopls/internal/lsp/testdata/arraytype/array_type.go.in
deleted file mode 100644
index ac1a3e78297..00000000000
--- a/gopls/internal/lsp/testdata/arraytype/array_type.go.in
+++ /dev/null
@@ -1,50 +0,0 @@
-package arraytype
-
-import (
- "golang.org/lsptests/foo"
-)
-
-func _() {
- var (
- val string //@item(atVal, "val", "string", "var")
- )
-
- // disabled - see issue #54822
- [] // complete(" //", PackageFoo)
-
- []val //@complete(" //")
-
- []foo.StructFoo //@complete(" //", StructFoo)
-
- []foo.StructFoo(nil) //@complete("(", StructFoo)
-
- []*foo.StructFoo //@complete(" //", StructFoo)
-
- [...]foo.StructFoo //@complete(" //", StructFoo)
-
- [2][][4]foo.StructFoo //@complete(" //", StructFoo)
-
- []struct { f []foo.StructFoo } //@complete(" }", StructFoo)
-}
-
-func _() {
- type myInt int //@item(atMyInt, "myInt", "int", "type")
-
- var mark []myInt //@item(atMark, "mark", "[]myInt", "var")
-
- var s []myInt //@item(atS, "s", "[]myInt", "var")
- s = []m //@complete(" //", atMyInt)
- // disabled - see issue #54822
- s = [] // complete(" //", atMyInt, PackageFoo)
-
- var a [1]myInt
- a = [1]m //@complete(" //", atMyInt)
-
- var ds [][]myInt
- ds = [][]m //@complete(" //", atMyInt)
-}
-
-func _() {
- var b [0]byte //@item(atByte, "b", "[0]byte", "var")
- var _ []byte = b //@snippet(" //", atByte, "b[:]", "b[:]")
-}
diff --git a/gopls/internal/lsp/testdata/bad/bad0_go120.go b/gopls/internal/lsp/testdata/bad/bad0_go120.go
deleted file mode 100644
index 78ddb0b4081..00000000000
--- a/gopls/internal/lsp/testdata/bad/bad0_go120.go
+++ /dev/null
@@ -1,24 +0,0 @@
-//go:build go1.11 && !go1.21
-// +build go1.11,!go1.21
-
-package bad
-
-import _ "golang.org/lsptests/assign/internal/secret" //@diag("\"golang.org/lsptests/assign/internal/secret\"", "compiler", "could not import golang.org/lsptests/assign/internal/secret \\(invalid use of internal package \"golang.org/lsptests/assign/internal/secret\"\\)", "error")
-
-func stuff() { //@item(stuff, "stuff", "func()", "func")
- x := "heeeeyyyy"
- random2(x) //@diag("x", "compiler", "cannot use x \\(variable of type string\\) as int value in argument to random2", "error")
- random2(1) //@complete("dom", random, random2, random3)
- y := 3 //@diag("y", "compiler", "y declared (and|but) not used", "error")
-}
-
-type bob struct { //@item(bob, "bob", "struct{...}", "struct")
- x int
-}
-
-func _() {
- var q int
- _ = &bob{
- f: q, //@diag("f: q", "compiler", "unknown field f in struct literal", "error")
- }
-}
diff --git a/gopls/internal/lsp/testdata/bad/bad0_go121.go b/gopls/internal/lsp/testdata/bad/bad0_go121.go
deleted file mode 100644
index c4f4ecc6383..00000000000
--- a/gopls/internal/lsp/testdata/bad/bad0_go121.go
+++ /dev/null
@@ -1,26 +0,0 @@
-//go:build go1.21
-// +build go1.21
-
-package bad
-
-// TODO(matloob): uncomment this and remove the space between the // and the @diag
-// once the changes that produce the new go list error are submitted.
-import _ "golang.org/lsptests/assign/internal/secret" //@diag("\"golang.org/lsptests/assign/internal/secret\"", "compiler", "could not import golang.org/lsptests/assign/internal/secret \\(invalid use of internal package \"golang.org/lsptests/assign/internal/secret\"\\)", "error"),diag("_", "go list", "use of internal package golang.org/lsptests/assign/internal/secret not allowed", "error")
-
-func stuff() { //@item(stuff, "stuff", "func()", "func")
- x := "heeeeyyyy"
- random2(x) //@diag("x", "compiler", "cannot use x \\(variable of type string\\) as int value in argument to random2", "error")
- random2(1) //@complete("dom", random, random2, random3)
- y := 3 //@diag("y", "compiler", "y declared (and|but) not used", "error")
-}
-
-type bob struct { //@item(bob, "bob", "struct{...}", "struct")
- x int
-}
-
-func _() {
- var q int
- _ = &bob{
- f: q, //@diag("f: q", "compiler", "unknown field f in struct literal", "error")
- }
-}
diff --git a/gopls/internal/lsp/testdata/bad/bad1.go b/gopls/internal/lsp/testdata/bad/bad1.go
deleted file mode 100644
index 13b3d0af61c..00000000000
--- a/gopls/internal/lsp/testdata/bad/bad1.go
+++ /dev/null
@@ -1,34 +0,0 @@
-//go:build go1.11
-// +build go1.11
-
-package bad
-
-// See #36637
-type stateFunc func() stateFunc //@item(stateFunc, "stateFunc", "func() stateFunc", "type")
-
-var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", "compiler", "(undeclared name|undefined): unknown", "error")
-
-func random() int { //@item(random, "random", "func() int", "func")
- //@complete("", global_a, bob, random, random2, random3, stateFunc, stuff)
- return 0
-}
-
-func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func"),item(bad_y_param, "y", "int", "var")
- x := 6 //@item(x, "x", "int", "var"),diag("x", "compiler", "x declared (and|but) not used", "error")
- var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared (and|but) not used", "error"),diag("blah", "compiler", "(undeclared name|undefined): blah", "error")
- var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared (and|but) not used", "error"),diag("blob", "compiler", "(undeclared name|undefined): blob", "error")
- //@complete("", q, t, x, bad_y_param, global_a, bob, random, random2, random3, stateFunc, stuff)
-
- return y
-}
-
-func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),item(y_variadic_param, "y", "[]int", "var")
- //@complete("", y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff)
-
- var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared (and|but) not used", "error"),diag("favType1", "compiler", "(undeclared name|undefined): favType1", "error")
- var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared (and|but) not used", "error"),diag("keyType", "compiler", "(undeclared name|undefined): keyType", "error")
- var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared (and|but) not used", "error"),diag("favType2", "compiler", "(undeclared name|undefined): favType2", "error")
- var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared (and|but) not used", "error"),diag("badResult", "compiler", "(undeclared name|undefined): badResult", "error")
- var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared (and|but) not used", "error"),diag("badParam", "compiler", "(undeclared name|undefined): badParam", "error")
- //@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff)
-}
diff --git a/gopls/internal/lsp/testdata/badstmt/badstmt.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt.go.in
deleted file mode 100644
index 3b8f9e06b39..00000000000
--- a/gopls/internal/lsp/testdata/badstmt/badstmt.go.in
+++ /dev/null
@@ -1,28 +0,0 @@
-package badstmt
-
-import (
- "golang.org/lsptests/foo"
-)
-
-// (The syntax error causes suppression of diagnostics for type errors.
-// See issue #59888.)
-
-func _(x int) {
- defer foo.F //@complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement|expression in defer must be function call", "error")
- defer foo.F //@complete(" //", Foo)
-}
-
-func _() {
- switch true {
- case true:
- go foo.F //@complete(" //", Foo)
- }
-}
-
-func _() {
- defer func() {
- foo.F //@complete(" //", Foo),snippet(" //", Foo, "Foo()", "Foo()")
-
- foo. //@rank(" //", Foo)
- }
-}
diff --git a/gopls/internal/lsp/testdata/badstmt/badstmt_2.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_2.go.in
deleted file mode 100644
index 6af9c35e3cf..00000000000
--- a/gopls/internal/lsp/testdata/badstmt/badstmt_2.go.in
+++ /dev/null
@@ -1,9 +0,0 @@
-package badstmt
-
-import (
- "golang.org/lsptests/foo"
-)
-
-func _() {
- defer func() { foo. } //@rank(" }", Foo)
-}
diff --git a/gopls/internal/lsp/testdata/badstmt/badstmt_3.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_3.go.in
deleted file mode 100644
index d135e201505..00000000000
--- a/gopls/internal/lsp/testdata/badstmt/badstmt_3.go.in
+++ /dev/null
@@ -1,9 +0,0 @@
-package badstmt
-
-import (
- "golang.org/lsptests/foo"
-)
-
-func _() {
- go foo. //@rank(" //", Foo, IntFoo),snippet(" //", Foo, "Foo()", "Foo()")
-}
diff --git a/gopls/internal/lsp/testdata/badstmt/badstmt_4.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_4.go.in
deleted file mode 100644
index 6afd635ec2d..00000000000
--- a/gopls/internal/lsp/testdata/badstmt/badstmt_4.go.in
+++ /dev/null
@@ -1,11 +0,0 @@
-package badstmt
-
-import (
- "golang.org/lsptests/foo"
-)
-
-func _() {
- go func() {
- defer foo. //@rank(" //", Foo, IntFoo)
- }
-}
diff --git a/gopls/internal/lsp/testdata/bar/bar.go.in b/gopls/internal/lsp/testdata/bar/bar.go.in
deleted file mode 100644
index 502bdf74060..00000000000
--- a/gopls/internal/lsp/testdata/bar/bar.go.in
+++ /dev/null
@@ -1,47 +0,0 @@
-// +build go1.11
-
-package bar
-
-import (
- "golang.org/lsptests/foo" //@item(foo, "foo", "\"golang.org/lsptests/foo\"", "package")
-)
-
-func helper(i foo.IntFoo) {} //@item(helper, "helper", "func(i foo.IntFoo)", "func")
-
-func _() {
- help //@complete("l", helper)
- _ = foo.StructFoo{} //@complete("S", IntFoo, StructFoo)
-}
-
-// Bar is a function.
-func Bar() { //@item(Bar, "Bar", "func()", "func", "Bar is a function.")
- foo.Foo() //@complete("F", Foo, IntFoo, StructFoo)
- var _ foo.IntFoo //@complete("I", IntFoo, StructFoo)
- foo.() //@complete("(", Foo, IntFoo, StructFoo)
-}
-
-func _() {
- var Valentine int //@item(Valentine, "Valentine", "int", "var")
-
- _ = foo.StructFoo{
- Valu //@complete(" //", Value)
- }
- _ = foo.StructFoo{
- Va //@complete("a", Value, Valentine)
- }
- _ = foo.StructFoo{
- Value: 5, //@complete("a", Value)
- }
- _ = foo.StructFoo{
- //@complete("", Value, Valentine, foo, helper, Bar)
- }
- _ = foo.StructFoo{
- Value: Valen //@complete("le", Valentine)
- }
- _ = foo.StructFoo{
- Value: //@complete(" //", Valentine, foo, helper, Bar)
- }
- _ = foo.StructFoo{
- Value: //@complete(" ", Valentine, foo, helper, Bar)
- }
-}
diff --git a/gopls/internal/lsp/testdata/baz/baz.go.in b/gopls/internal/lsp/testdata/baz/baz.go.in
deleted file mode 100644
index 94952e1267b..00000000000
--- a/gopls/internal/lsp/testdata/baz/baz.go.in
+++ /dev/null
@@ -1,33 +0,0 @@
-// +build go1.11
-
-package baz
-
-import (
- "golang.org/lsptests/bar"
-
- f "golang.org/lsptests/foo"
-)
-
-var FooStruct f.StructFoo
-
-func Baz() {
- defer bar.Bar() //@complete("B", Bar)
- // TODO(rstambler): Test completion here.
- defer bar.B
- var x f.IntFoo //@complete("n", IntFoo),typdef("x", IntFoo)
- bar.Bar() //@complete("B", Bar)
-}
-
-func _() {
- bob := f.StructFoo{Value: 5}
- if x := bob. //@complete(" //", Value)
- switch true == false {
- case true:
- if x := bob. //@complete(" //", Value)
- case false:
- }
- if x := bob.Va //@complete("a", Value)
- switch true == true {
- default:
- }
-}
diff --git a/gopls/internal/lsp/testdata/builtins/builtin_go117.go b/gopls/internal/lsp/testdata/builtins/builtin_go117.go
deleted file mode 100644
index 57abcde1517..00000000000
--- a/gopls/internal/lsp/testdata/builtins/builtin_go117.go
+++ /dev/null
@@ -1,8 +0,0 @@
-//go:build !go1.18
-// +build !go1.18
-
-package builtins
-
-func _() {
- //@complete("", append, bool, byte, cap, close, complex, complex128, complex64, copy, delete, error, _false, float32, float64, imag, int, int16, int32, int64, int8, len, make, new, panic, print, println, real, recover, rune, string, _true, uint, uint16, uint32, uint64, uint8, uintptr, _nil)
-}
diff --git a/gopls/internal/lsp/testdata/builtins/builtin_go118.go b/gopls/internal/lsp/testdata/builtins/builtin_go118.go
deleted file mode 100644
index dabffcc679c..00000000000
--- a/gopls/internal/lsp/testdata/builtins/builtin_go118.go
+++ /dev/null
@@ -1,8 +0,0 @@
-//go:build go1.18 && !go1.21
-// +build go1.18,!go1.21
-
-package builtins
-
-func _() {
- //@complete("", any, append, bool, byte, cap, close, comparable, complex, complex128, complex64, copy, delete, error, _false, float32, float64, imag, int, int16, int32, int64, int8, len, make, new, panic, print, println, real, recover, rune, string, _true, uint, uint16, uint32, uint64, uint8, uintptr, _nil)
-}
diff --git a/gopls/internal/lsp/testdata/builtins/builtin_go121.go b/gopls/internal/lsp/testdata/builtins/builtin_go121.go
deleted file mode 100644
index 14f59def9ac..00000000000
--- a/gopls/internal/lsp/testdata/builtins/builtin_go121.go
+++ /dev/null
@@ -1,8 +0,0 @@
-//go:build go1.21 && !go1.22 && ignore
-// +build go1.21,!go1.22,ignore
-
-package builtins
-
-func _() {
- //@complete("", any, append, bool, byte, cap, clear, close, comparable, complex, complex128, complex64, copy, delete, error, _false, float32, float64, imag, int, int16, int32, int64, int8, len, make, max, min, new, panic, print, println, real, recover, rune, string, _true, uint, uint16, uint32, uint64, uint8, uintptr, _nil)
-}
diff --git a/gopls/internal/lsp/testdata/builtins/builtin_go122.go b/gopls/internal/lsp/testdata/builtins/builtin_go122.go
deleted file mode 100644
index f799c1225a1..00000000000
--- a/gopls/internal/lsp/testdata/builtins/builtin_go122.go
+++ /dev/null
@@ -1,8 +0,0 @@
-//go:build go1.22 && ignore
-// +build go1.22,ignore
-
-package builtins
-
-func _() {
- //@complete("", any, append, bool, byte, cap, clear, close, comparable, complex, complex128, complex64, copy, delete, error, _false, float32, float64, imag, int, int16, int32, int64, int8, len, make, max, min, new, panic, print, println, real, recover, rune, string, _true, uint, uint16, uint32, uint64, uint8, uintptr, zero, _nil)
-}
diff --git a/gopls/internal/lsp/testdata/builtins/builtins.go b/gopls/internal/lsp/testdata/builtins/builtins.go
index a6450362a78..bd47477d831 100644
--- a/gopls/internal/lsp/testdata/builtins/builtins.go
+++ b/gopls/internal/lsp/testdata/builtins/builtins.go
@@ -1,50 +1,13 @@
package builtins
-// Definitions of builtin completion items.
+// Definitions of builtin completion items that are still used in tests.
-/* any */ //@item(any, "any", "", "interface")
-/* Create markers for builtin types. Only for use by this test.
-/* append(slice []Type, elems ...Type) []Type */ //@item(append, "append", "func(slice []Type, elems ...Type) []Type", "func")
/* bool */ //@item(bool, "bool", "", "type")
-/* byte */ //@item(byte, "byte", "", "type")
-/* cap(v Type) int */ //@item(cap, "cap", "func(v Type) int", "func")
-/* clear[T interface{ ~[]Type | ~map[Type]Type1 }](t T) */ //@item(clear, "clear", "func(t T)", "func")
-/* close(c chan<- Type) */ //@item(close, "close", "func(c chan<- Type)", "func")
-/* comparable */ //@item(comparable, "comparable", "", "interface")
/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func")
-/* complex128 */ //@item(complex128, "complex128", "", "type")
-/* complex64 */ //@item(complex64, "complex64", "", "type")
-/* copy(dst []Type, src []Type) int */ //@item(copy, "copy", "func(dst []Type, src []Type) int", "func")
-/* delete(m map[Type]Type1, key Type) */ //@item(delete, "delete", "func(m map[Type]Type1, key Type)", "func")
-/* error */ //@item(error, "error", "", "interface")
-/* false */ //@item(_false, "false", "", "const")
/* float32 */ //@item(float32, "float32", "", "type")
/* float64 */ //@item(float64, "float64", "", "type")
/* imag(c complex128) float64 */ //@item(imag, "imag", "func(c complex128) float64", "func")
/* int */ //@item(int, "int", "", "type")
-/* int16 */ //@item(int16, "int16", "", "type")
-/* int32 */ //@item(int32, "int32", "", "type")
-/* int64 */ //@item(int64, "int64", "", "type")
-/* int8 */ //@item(int8, "int8", "", "type")
/* iota */ //@item(iota, "iota", "", "const")
-/* len(v Type) int */ //@item(len, "len", "func(v Type) int", "func")
-/* max(x T, y ...T) T */ //@item(max, "max", "func(x T, y ...T) T", "func")
-/* min(y T, y ...T) T */ //@item(min, "min", "func(x T, y ...T) T", "func")
-/* make(t Type, size ...int) Type */ //@item(make, "make", "func(t Type, size ...int) Type", "func")
-/* new(Type) *Type */ //@item(new, "new", "func(Type) *Type", "func")
-/* nil */ //@item(_nil, "nil", "", "var")
-/* panic(v interface{}) */ //@item(panic, "panic", "func(v interface{})", "func")
-/* print(args ...Type) */ //@item(print, "print", "func(args ...Type)", "func")
-/* println(args ...Type) */ //@item(println, "println", "func(args ...Type)", "func")
-/* real(c complex128) float64 */ //@item(real, "real", "func(c complex128) float64", "func")
-/* recover() interface{} */ //@item(recover, "recover", "func() interface{}", "func")
-/* rune */ //@item(rune, "rune", "", "type")
/* string */ //@item(string, "string", "", "type")
/* true */ //@item(_true, "true", "", "const")
-/* uint */ //@item(uint, "uint", "", "type")
-/* uint16 */ //@item(uint16, "uint16", "", "type")
-/* uint32 */ //@item(uint32, "uint32", "", "type")
-/* uint64 */ //@item(uint64, "uint64", "", "type")
-/* uint8 */ //@item(uint8, "uint8", "", "type")
-/* uintptr */ //@item(uintptr, "uintptr", "", "type")
-/* zero */ //@item(zero, "zero", "", "var")
diff --git a/gopls/internal/lsp/testdata/cgo/declarecgo.go b/gopls/internal/lsp/testdata/cgo/declarecgo.go
deleted file mode 100644
index c283cdfb2b7..00000000000
--- a/gopls/internal/lsp/testdata/cgo/declarecgo.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package cgo
-
-/*
-#include
-#include
-
-void myprint(char* s) {
- printf("%s\n", s);
-}
-*/
-import "C"
-
-import (
- "fmt"
- "unsafe"
-)
-
-func Example() { //@mark(funccgoexample, "Example"),item(funccgoexample, "Example", "func()", "func")
- fmt.Println()
- cs := C.CString("Hello from stdio\n")
- C.myprint(cs)
- C.free(unsafe.Pointer(cs))
-}
-
-func _() {
- Example() //@godef("ample", funccgoexample),complete("ample", funccgoexample)
-}
diff --git a/gopls/internal/lsp/testdata/cgo/declarecgo.go.golden b/gopls/internal/lsp/testdata/cgo/declarecgo.go.golden
deleted file mode 100644
index 0d6fbb0fff6..00000000000
--- a/gopls/internal/lsp/testdata/cgo/declarecgo.go.golden
+++ /dev/null
@@ -1,30 +0,0 @@
--- funccgoexample-definition --
-cgo/declarecgo.go:18:6-13: defined here as ```go
-func Example()
-```
-
-[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)
--- funccgoexample-definition-json --
-{
- "span": {
- "uri": "file://cgo/declarecgo.go",
- "start": {
- "line": 18,
- "column": 6,
- "offset": 151
- },
- "end": {
- "line": 18,
- "column": 13,
- "offset": 158
- }
- },
- "description": "```go\nfunc Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)"
-}
-
--- funccgoexample-hoverdef --
-```go
-func Example()
-```
-
-[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)
diff --git a/gopls/internal/lsp/testdata/cgo/declarecgo_nocgo.go b/gopls/internal/lsp/testdata/cgo/declarecgo_nocgo.go
deleted file mode 100644
index a05c01257d0..00000000000
--- a/gopls/internal/lsp/testdata/cgo/declarecgo_nocgo.go
+++ /dev/null
@@ -1,6 +0,0 @@
-//+build !cgo
-
-package cgo
-
-// Set a dummy marker to keep the test framework happy. The tests should be skipped.
-var _ = "Example" //@mark(funccgoexample, "Example"),godef("ample", funccgoexample),complete("ample", funccgoexample)
diff --git a/gopls/internal/lsp/testdata/cgoimport/usecgo.go.golden b/gopls/internal/lsp/testdata/cgoimport/usecgo.go.golden
deleted file mode 100644
index 03fc22468ca..00000000000
--- a/gopls/internal/lsp/testdata/cgoimport/usecgo.go.golden
+++ /dev/null
@@ -1,30 +0,0 @@
--- funccgoexample-definition --
-cgo/declarecgo.go:18:6-13: defined here as ```go
-func cgo.Example()
-```
-
-[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)
--- funccgoexample-definition-json --
-{
- "span": {
- "uri": "file://cgo/declarecgo.go",
- "start": {
- "line": 18,
- "column": 6,
- "offset": 151
- },
- "end": {
- "line": 18,
- "column": 13,
- "offset": 158
- }
- },
- "description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)"
-}
-
--- funccgoexample-hoverdef --
-```go
-func cgo.Example()
-```
-
-[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)
diff --git a/gopls/internal/lsp/testdata/cgoimport/usecgo.go.in b/gopls/internal/lsp/testdata/cgoimport/usecgo.go.in
deleted file mode 100644
index 414a739da99..00000000000
--- a/gopls/internal/lsp/testdata/cgoimport/usecgo.go.in
+++ /dev/null
@@ -1,9 +0,0 @@
-package cgoimport
-
-import (
- "golang.org/lsptests/cgo"
-)
-
-func _() {
- cgo.Example() //@godef("ample", funccgoexample),complete("ample", funccgoexample)
-}
diff --git a/gopls/internal/lsp/testdata/codelens/codelens_test.go b/gopls/internal/lsp/testdata/codelens/codelens_test.go
deleted file mode 100644
index f6c696416a8..00000000000
--- a/gopls/internal/lsp/testdata/codelens/codelens_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package codelens //@codelens("package codelens", "run file benchmarks", "test")
-
-import "testing"
-
-func TestMain(m *testing.M) {} // no code lens for TestMain
-
-func TestFuncWithCodeLens(t *testing.T) { //@codelens("func", "run test", "test")
-}
-
-func thisShouldNotHaveACodeLens(t *testing.T) {
-}
-
-func BenchmarkFuncWithCodeLens(b *testing.B) { //@codelens("func", "run benchmark", "test")
-}
-
-func helper() {} // expect no code lens
diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go
index 8d4b15bff6a..e25b00ce6c3 100644
--- a/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go
+++ b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go
@@ -1,8 +1,10 @@
package danglingstmt
-import "golang.org/lsptests/foo"
+// TODO: re-enable this test, which was broken when the foo package was removed.
+// (we can replicate the relevant definitions in the new marker test)
+// import "golang.org/lsptests/foo"
func _() {
- foo. //@rank(" //", Foo)
- var _ = []string{foo.} //@rank("}", Foo)
+ foo. // rank(" //", Foo)
+ var _ = []string{foo.} // rank("}", Foo)
}
diff --git a/gopls/internal/lsp/testdata/folding/a.go b/gopls/internal/lsp/testdata/folding/a.go
deleted file mode 100644
index e07d7e0bf19..00000000000
--- a/gopls/internal/lsp/testdata/folding/a.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:
- if true {
- fmt.Println("true")
- } else {
- fmt.Println("false")
- }
- case false:
- fmt.Println("false")
- default:
- fmt.Println("default")
- }
- /* This is a multiline
- block
- comment */
-
- /* This is a multiline
- block
- comment */
- // Followed by another comment.
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:
- if val {
- fmt.Println("true from x")
- } else {
- fmt.Println("false from x")
- }
- case <-y:
- fmt.Println("y")
- default:
- fmt.Println("default")
- }
- // This is a multiline comment
- // that is not a doc comment.
- return `
-this string
-is not indented`
-}
diff --git a/gopls/internal/lsp/testdata/folding/a.go.golden b/gopls/internal/lsp/testdata/folding/a.go.golden
deleted file mode 100644
index b04ca4dab3f..00000000000
--- a/gopls/internal/lsp/testdata/folding/a.go.golden
+++ /dev/null
@@ -1,722 +0,0 @@
--- foldingRange-0 --
-package folding //@fold("package")
-
-import (<>)
-
-import _ "os"
-
-// bar is a function.<>
-func bar(<>) string {<>}
-
--- foldingRange-1 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {<>}
- /* This is a multiline<>
-
- /* This is a multiline<>
- _ = []int{<>}
- _ = [2]string{<>}
- _ = map[string]int{<>}
- type T struct {<>}
- _ = T{<>}
- x, y := make(<>), make(<>)
- select {<>}
- // This is a multiline comment<>
- return <>
-}
-
--- foldingRange-2 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:<>
- case false:<>
- default:<>
- }
- /* This is a multiline
- block
- comment */
-
- /* This is a multiline
- block
- comment */
- // Followed by another comment.
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:<>
- case <-y:<>
- default:<>
- }
- // This is a multiline comment
- // that is not a doc comment.
- return `
-this string
-is not indented`
-}
-
--- foldingRange-3 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:
- if true {<>} else {<>}
- case false:
- fmt.Println(<>)
- default:
- fmt.Println(<>)
- }
- /* This is a multiline
- block
- comment */
-
- /* This is a multiline
- block
- comment */
- // Followed by another comment.
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:
- if val {<>} else {<>}
- case <-y:
- fmt.Println(<>)
- default:
- fmt.Println(<>)
- }
- // This is a multiline comment
- // that is not a doc comment.
- return `
-this string
-is not indented`
-}
-
--- foldingRange-4 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:
- if true {
- fmt.Println(<>)
- } else {
- fmt.Println(<>)
- }
- case false:
- fmt.Println("false")
- default:
- fmt.Println("default")
- }
- /* This is a multiline
- block
- comment */
-
- /* This is a multiline
- block
- comment */
- // Followed by another comment.
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:
- if val {
- fmt.Println(<>)
- } else {
- fmt.Println(<>)
- }
- case <-y:
- fmt.Println("y")
- default:
- fmt.Println("default")
- }
- // This is a multiline comment
- // that is not a doc comment.
- return `
-this string
-is not indented`
-}
-
--- foldingRange-comment-0 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.<>
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:
- if true {
- fmt.Println("true")
- } else {
- fmt.Println("false")
- }
- case false:
- fmt.Println("false")
- default:
- fmt.Println("default")
- }
- /* This is a multiline<>
-
- /* This is a multiline<>
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:
- if val {
- fmt.Println("true from x")
- } else {
- fmt.Println("false from x")
- }
- case <-y:
- fmt.Println("y")
- default:
- fmt.Println("default")
- }
- // This is a multiline comment<>
- return `
-this string
-is not indented`
-}
-
--- foldingRange-imports-0 --
-package folding //@fold("package")
-
-import (<>)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:
- if true {
- fmt.Println("true")
- } else {
- fmt.Println("false")
- }
- case false:
- fmt.Println("false")
- default:
- fmt.Println("default")
- }
- /* This is a multiline
- block
- comment */
-
- /* This is a multiline
- block
- comment */
- // Followed by another comment.
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:
- if val {
- fmt.Println("true from x")
- } else {
- fmt.Println("false from x")
- }
- case <-y:
- fmt.Println("y")
- default:
- fmt.Println("default")
- }
- // This is a multiline comment
- // that is not a doc comment.
- return `
-this string
-is not indented`
-}
-
--- foldingRange-lineFolding-0 --
-package folding //@fold("package")
-
-import (<>
-)
-
-import _ "os"
-
-// bar is a function.<>
-func bar() string {<>
-}
-
--- foldingRange-lineFolding-1 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {<>
- }
- /* This is a multiline<>
-
- /* This is a multiline<>
- _ = []int{<>,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{<>,
- }
- type T struct {<>
- }
- _ = T{<>,
- }
- x, y := make(chan bool), make(chan bool)
- select {<>
- }
- // This is a multiline comment<>
- return <>
-}
-
--- foldingRange-lineFolding-2 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:<>
- case false:<>
- default:<>
- }
- /* This is a multiline
- block
- comment */
-
- /* This is a multiline
- block
- comment */
- // Followed by another comment.
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:<>
- case <-y:<>
- default:<>
- }
- // This is a multiline comment
- // that is not a doc comment.
- return `
-this string
-is not indented`
-}
-
--- foldingRange-lineFolding-3 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:
- if true {<>
- } else {<>
- }
- case false:
- fmt.Println("false")
- default:
- fmt.Println("default")
- }
- /* This is a multiline
- block
- comment */
-
- /* This is a multiline
- block
- comment */
- // Followed by another comment.
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:
- if val {<>
- } else {<>
- }
- case <-y:
- fmt.Println("y")
- default:
- fmt.Println("default")
- }
- // This is a multiline comment
- // that is not a doc comment.
- return `
-this string
-is not indented`
-}
-
--- foldingRange-lineFolding-comment-0 --
-package folding //@fold("package")
-
-import (
- "fmt"
- _ "log"
-)
-
-import _ "os"
-
-// bar is a function.<>
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:
- if true {
- fmt.Println("true")
- } else {
- fmt.Println("false")
- }
- case false:
- fmt.Println("false")
- default:
- fmt.Println("default")
- }
- /* This is a multiline<>
-
- /* This is a multiline<>
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:
- if val {
- fmt.Println("true from x")
- } else {
- fmt.Println("false from x")
- }
- case <-y:
- fmt.Println("y")
- default:
- fmt.Println("default")
- }
- // This is a multiline comment<>
- return `
-this string
-is not indented`
-}
-
--- foldingRange-lineFolding-imports-0 --
-package folding //@fold("package")
-
-import (<>
-)
-
-import _ "os"
-
-// bar is a function.
-// With a multiline doc comment.
-func bar() string {
- /* This is a single line comment */
- switch {
- case true:
- if true {
- fmt.Println("true")
- } else {
- fmt.Println("false")
- }
- case false:
- fmt.Println("false")
- default:
- fmt.Println("default")
- }
- /* This is a multiline
- block
- comment */
-
- /* This is a multiline
- block
- comment */
- // Followed by another comment.
- _ = []int{
- 1,
- 2,
- 3,
- }
- _ = [2]string{"d",
- "e",
- }
- _ = map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- type T struct {
- f string
- g int
- h string
- }
- _ = T{
- f: "j",
- g: 4,
- h: "i",
- }
- x, y := make(chan bool), make(chan bool)
- select {
- case val := <-x:
- if val {
- fmt.Println("true from x")
- } else {
- fmt.Println("false from x")
- }
- case <-y:
- fmt.Println("y")
- default:
- fmt.Println("default")
- }
- // This is a multiline comment
- // that is not a doc comment.
- return `
-this string
-is not indented`
-}
-
diff --git a/gopls/internal/lsp/testdata/folding/bad.go.golden b/gopls/internal/lsp/testdata/folding/bad.go.golden
deleted file mode 100644
index ab274f75ac6..00000000000
--- a/gopls/internal/lsp/testdata/folding/bad.go.golden
+++ /dev/null
@@ -1,81 +0,0 @@
--- foldingRange-0 --
-package folding //@fold("package")
-
-import (<>)
-
-import (<>)
-
-// badBar is a function.
-func badBar(<>) string {<>}
-
--- foldingRange-1 --
-package folding //@fold("package")
-
-import ( "fmt"
- _ "log"
-)
-
-import (
- _ "os" )
-
-// badBar is a function.
-func badBar() string { x := true
- if x {<>} else {<>}
- return
-}
-
--- foldingRange-2 --
-package folding //@fold("package")
-
-import ( "fmt"
- _ "log"
-)
-
-import (
- _ "os" )
-
-// badBar is a function.
-func badBar() string { x := true
- if x {
- // This is the only foldable thing in this file when lineFoldingOnly
- fmt.Println(<>)
- } else {
- fmt.Println(<>) }
- return
-}
-
--- foldingRange-imports-0 --
-package folding //@fold("package")
-
-import (<>)
-
-import (<>)
-
-// badBar is a function.
-func badBar() string { x := true
- if x {
- // This is the only foldable thing in this file when lineFoldingOnly
- fmt.Println("true")
- } else {
- fmt.Println("false") }
- return
-}
-
--- foldingRange-lineFolding-0 --
-package folding //@fold("package")
-
-import ( "fmt"
- _ "log"
-)
-
-import (
- _ "os" )
-
-// badBar is a function.
-func badBar() string { x := true
- if x {<>
- } else {
- fmt.Println("false") }
- return
-}
-
diff --git a/gopls/internal/lsp/testdata/folding/bad.go.in b/gopls/internal/lsp/testdata/folding/bad.go.in
deleted file mode 100644
index 84fcb740f40..00000000000
--- a/gopls/internal/lsp/testdata/folding/bad.go.in
+++ /dev/null
@@ -1,18 +0,0 @@
-package folding //@fold("package")
-
-import ( "fmt"
- _ "log"
-)
-
-import (
- _ "os" )
-
-// badBar is a function.
-func badBar() string { x := true
- if x {
- // This is the only foldable thing in this file when lineFoldingOnly
- fmt.Println("true")
- } else {
- fmt.Println("false") }
- return
-}
diff --git a/gopls/internal/lsp/testdata/foo/foo.go b/gopls/internal/lsp/testdata/foo/foo.go
deleted file mode 100644
index 490ff2e657c..00000000000
--- a/gopls/internal/lsp/testdata/foo/foo.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package foo //@mark(PackageFoo, "foo"),item(PackageFoo, "foo", "\"golang.org/lsptests/foo\"", "package")
-
-type StructFoo struct { //@item(StructFoo, "StructFoo", "struct{...}", "struct")
- Value int //@item(Value, "Value", "int", "field")
-}
-
-// Pre-set this marker, as we don't have a "source" for it in this package.
-/* Error() */ //@item(Error, "Error", "func() string", "method")
-
-func Foo() { //@item(Foo, "Foo", "func()", "func")
- var err error
- err.Error() //@complete("E", Error)
-}
-
-func _() {
- var sFoo StructFoo //@complete("t", StructFoo)
- if x := sFoo; x.Value == 1 { //@complete("V", Value),typdef("sFoo", StructFoo)
- return
- }
-}
-
-func _() {
- shadowed := 123
- {
- shadowed := "hi" //@item(shadowed, "shadowed", "string", "var")
- sha //@complete("a", shadowed)
- }
-}
-
-type IntFoo int //@item(IntFoo, "IntFoo", "int", "type")
diff --git a/gopls/internal/lsp/testdata/generate/generate.go b/gopls/internal/lsp/testdata/generate/generate.go
deleted file mode 100644
index ae5e90d1a48..00000000000
--- a/gopls/internal/lsp/testdata/generate/generate.go
+++ /dev/null
@@ -1,4 +0,0 @@
-package generate
-
-//go:generate echo Hi //@ codelens("//go:generate", "run go generate", "generate"), codelens("//go:generate", "run go generate ./...", "generate")
-//go:generate echo I shall have no CodeLens
diff --git a/gopls/internal/lsp/testdata/godef/a/a_x_test.go b/gopls/internal/lsp/testdata/godef/a/a_x_test.go
deleted file mode 100644
index f166f055084..00000000000
--- a/gopls/internal/lsp/testdata/godef/a/a_x_test.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package a_test
-
-import (
- "testing"
-)
-
-func TestA2(t *testing.T) { //@TestA2,godef(TestA2, TestA2)
- Nonexistant() //@diag("Nonexistant", "compiler", "(undeclared name|undefined): Nonexistant", "error")
-}
diff --git a/gopls/internal/lsp/testdata/godef/a/a_x_test.go.golden b/gopls/internal/lsp/testdata/godef/a/a_x_test.go.golden
deleted file mode 100644
index 2e3064794f2..00000000000
--- a/gopls/internal/lsp/testdata/godef/a/a_x_test.go.golden
+++ /dev/null
@@ -1,26 +0,0 @@
--- TestA2-definition --
-godef/a/a_x_test.go:7:6-12: defined here as ```go
-func TestA2(t *testing.T)
-```
--- TestA2-definition-json --
-{
- "span": {
- "uri": "file://godef/a/a_x_test.go",
- "start": {
- "line": 7,
- "column": 6,
- "offset": 44
- },
- "end": {
- "line": 7,
- "column": 12,
- "offset": 50
- }
- },
- "description": "```go\nfunc TestA2(t *testing.T)\n```"
-}
-
--- TestA2-hoverdef --
-```go
-func TestA2(t *testing.T)
-```
diff --git a/gopls/internal/lsp/testdata/godef/a/d.go b/gopls/internal/lsp/testdata/godef/a/d.go
deleted file mode 100644
index a1d17ad0da3..00000000000
--- a/gopls/internal/lsp/testdata/godef/a/d.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package a //@mark(a, "a "),hoverdef("a ", a)
-
-import "fmt"
-
-type Thing struct { //@Thing
- Member string //@Member
-}
-
-var Other Thing //@Other
-
-func Things(val []string) []Thing { //@Things
- return nil
-}
-
-func (t Thing) Method(i int) string { //@Method
- return t.Member
-}
-
-func (t Thing) Method3() {
-}
-
-func (t *Thing) Method2(i int, j int) (error, string) {
- return nil, t.Member
-}
-
-func (t *Thing) private() {
-}
-
-func useThings() {
- t := Thing{ //@mark(aStructType, "ing")
- Member: "string", //@mark(fMember, "ember")
- }
- fmt.Print(t.Member) //@mark(aMember, "ember")
- fmt.Print(Other) //@mark(aVar, "ther")
- Things() //@mark(aFunc, "ings")
- t.Method() //@mark(aMethod, "eth")
-}
-
-type NextThing struct { //@NextThing
- Thing
- Value int
-}
-
-func (n NextThing) another() string {
- return n.Member
-}
-
-// Shadows Thing.Method3
-func (n *NextThing) Method3() int {
- return n.Value
-}
-
-var nextThing NextThing //@hoverdef("NextThing", NextThing)
-
-/*@
-godef(aStructType, Thing)
-godef(aMember, Member)
-godef(aVar, Other)
-godef(aFunc, Things)
-godef(aMethod, Method)
-godef(fMember, Member)
-godef(Member, Member)
-
-//param
-//package name
-//const
-//anon field
-
-*/
diff --git a/gopls/internal/lsp/testdata/godef/a/d.go.golden b/gopls/internal/lsp/testdata/godef/a/d.go.golden
deleted file mode 100644
index ee687750c3e..00000000000
--- a/gopls/internal/lsp/testdata/godef/a/d.go.golden
+++ /dev/null
@@ -1,191 +0,0 @@
--- Member-definition --
-godef/a/d.go:6:2-8: defined here as ```go
-field Member string
-```
-
-@Member
-
-
-[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)
--- Member-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 6,
- "column": 2,
- "offset": 90
- },
- "end": {
- "line": 6,
- "column": 8,
- "offset": 96
- }
- },
- "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)"
-}
-
--- Member-hoverdef --
-```go
-field Member string
-```
-
-@Member
-
-
-[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)
--- Method-definition --
-godef/a/d.go:15:16-22: defined here as ```go
-func (Thing).Method(i int) string
-```
-
-[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method)
--- Method-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 15,
- "column": 16,
- "offset": 219
- },
- "end": {
- "line": 15,
- "column": 22,
- "offset": 225
- }
- },
- "description": "```go\nfunc (Thing).Method(i int) string\n```\n\n[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method)"
-}
-
--- Method-hoverdef --
-```go
-func (Thing).Method(i int) string
-```
-
-[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method)
--- NextThing-hoverdef --
-```go
-type NextThing struct {
- Thing
- Value int
-}
-
-func (*NextThing).Method3() int
-func (NextThing).another() string
-```
-
-[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#NextThing)
--- Other-definition --
-godef/a/d.go:9:5-10: defined here as ```go
-var Other Thing
-```
-
-@Other
-
-
-[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)
--- Other-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 9,
- "column": 5,
- "offset": 121
- },
- "end": {
- "line": 9,
- "column": 10,
- "offset": 126
- }
- },
- "description": "```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)"
-}
-
--- Other-hoverdef --
-```go
-var Other Thing
-```
-
-@Other
-
-
-[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)
--- Thing-definition --
-godef/a/d.go:5:6-11: defined here as ```go
-type Thing struct {
- Member string //@Member
-}
-
-func (Thing).Method(i int) string
-func (*Thing).Method2(i int, j int) (error, string)
-func (Thing).Method3()
-func (*Thing).private()
-```
-
-[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)
--- Thing-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 5,
- "column": 6,
- "offset": 65
- },
- "end": {
- "line": 5,
- "column": 11,
- "offset": 70
- }
- },
- "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n\nfunc (Thing).Method(i int) string\nfunc (*Thing).Method2(i int, j int) (error, string)\nfunc (Thing).Method3()\nfunc (*Thing).private()\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)"
-}
-
--- Thing-hoverdef --
-```go
-type Thing struct {
- Member string //@Member
-}
-
-func (Thing).Method(i int) string
-func (*Thing).Method2(i int, j int) (error, string)
-func (Thing).Method3()
-func (*Thing).private()
-```
-
-[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)
--- Things-definition --
-godef/a/d.go:11:6-12: defined here as ```go
-func Things(val []string) []Thing
-```
-
-[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)
--- Things-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 11,
- "column": 6,
- "offset": 148
- },
- "end": {
- "line": 11,
- "column": 12,
- "offset": 154
- }
- },
- "description": "```go\nfunc Things(val []string) []Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)"
-}
-
--- Things-hoverdef --
-```go
-func Things(val []string) []Thing
-```
-
-[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)
--- a-hoverdef --
-Package a is a package for testing go to definition.
-
diff --git a/gopls/internal/lsp/testdata/godef/a/f.go b/gopls/internal/lsp/testdata/godef/a/f.go
deleted file mode 100644
index 10f88262a81..00000000000
--- a/gopls/internal/lsp/testdata/godef/a/f.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Package a is a package for testing go to definition.
-package a
-
-import "fmt"
-
-func TypeStuff() { //@Stuff
- var x string
-
- switch y := interface{}(x).(type) { //@mark(switchY, "y"),godef("y", switchY)
- case int: //@mark(intY, "int")
- fmt.Printf("%v", y) //@hoverdef("y", intY)
- case string: //@mark(stringY, "string")
- fmt.Printf("%v", y) //@hoverdef("y", stringY)
- }
-
-}
diff --git a/gopls/internal/lsp/testdata/godef/a/f.go.golden b/gopls/internal/lsp/testdata/godef/a/f.go.golden
deleted file mode 100644
index a084356c06b..00000000000
--- a/gopls/internal/lsp/testdata/godef/a/f.go.golden
+++ /dev/null
@@ -1,34 +0,0 @@
--- intY-hoverdef --
-```go
-var y int
-```
--- stringY-hoverdef --
-```go
-var y string
-```
--- switchY-definition --
-godef/a/f.go:8:9-10: defined here as ```go
-var y interface{}
-```
--- switchY-definition-json --
-{
- "span": {
- "uri": "file://godef/a/f.go",
- "start": {
- "line": 8,
- "column": 9,
- "offset": 76
- },
- "end": {
- "line": 8,
- "column": 10,
- "offset": 77
- }
- },
- "description": "```go\nvar y interface{}\n```"
-}
-
--- switchY-hoverdef --
-```go
-var y interface{}
-```
diff --git a/gopls/internal/lsp/testdata/godef/a/h.go b/gopls/internal/lsp/testdata/godef/a/h.go
deleted file mode 100644
index 5a5dcc6784d..00000000000
--- a/gopls/internal/lsp/testdata/godef/a/h.go
+++ /dev/null
@@ -1,147 +0,0 @@
-package a
-
-func _() {
- type s struct {
- nested struct {
- // nested number
- number int64 //@mark(nestedNumber, "number")
- }
- nested2 []struct {
- // nested string
- str string //@mark(nestedString, "str")
- }
- x struct {
- x struct {
- x struct {
- x struct {
- x struct {
- // nested map
- m map[string]float64 //@mark(nestedMap, "m")
- }
- }
- }
- }
- }
- }
-
- var t s
- _ = t.nested.number //@hoverdef("number", nestedNumber)
- _ = t.nested2[0].str //@hoverdef("str", nestedString)
- _ = t.x.x.x.x.x.m //@hoverdef("m", nestedMap)
-}
-
-func _() {
- var s struct {
- // a field
- a int //@mark(structA, "a")
- // b nested struct
- b struct { //@mark(structB, "b")
- // c field of nested struct
- c int //@mark(structC, "c")
- }
- }
- _ = s.a //@hoverdef("a", structA)
- _ = s.b //@hoverdef("b", structB)
- _ = s.b.c //@hoverdef("c", structC)
-
- var arr []struct {
- // d field
- d int //@mark(arrD, "d")
- // e nested struct
- e struct { //@mark(arrE, "e")
- // f field of nested struct
- f int //@mark(arrF, "f")
- }
- }
- _ = arr[0].d //@hoverdef("d", arrD)
- _ = arr[0].e //@hoverdef("e", arrE)
- _ = arr[0].e.f //@hoverdef("f", arrF)
-
- var complex []struct {
- c <-chan map[string][]struct {
- // h field
- h int //@mark(complexH, "h")
- // i nested struct
- i struct { //@mark(complexI, "i")
- // j field of nested struct
- j int //@mark(complexJ, "j")
- }
- }
- }
- _ = (<-complex[0].c)["0"][0].h //@hoverdef("h", complexH)
- _ = (<-complex[0].c)["0"][0].i //@hoverdef("i", complexI)
- _ = (<-complex[0].c)["0"][0].i.j //@hoverdef("j", complexJ)
-
- var mapWithStructKey map[struct {
- // X key field
- x []string //@mark(mapStructKeyX, "x")
- }]int
- for k := range mapWithStructKey {
- _ = k.x //@hoverdef("x", mapStructKeyX)
- }
-
- var mapWithStructKeyAndValue map[struct {
- // Y key field
- y string //@mark(mapStructKeyY, "y")
- }]struct {
- // X value field
- x string //@mark(mapStructValueX, "x")
- }
- for k, v := range mapWithStructKeyAndValue {
- // TODO: we don't show docs for y field because both map key and value
- // are structs. And in this case, we parse only map value
- _ = k.y //@hoverdef("y", mapStructKeyY)
- _ = v.x //@hoverdef("x", mapStructValueX)
- }
-
- var i []map[string]interface {
- // open method comment
- open() error //@mark(openMethod, "open")
- }
- i[0]["1"].open() //@hoverdef("open", openMethod)
-}
-
-func _() {
- test := struct {
- // test description
- desc string //@mark(testDescription, "desc")
- }{}
- _ = test.desc //@hoverdef("desc", testDescription)
-
- for _, tt := range []struct {
- // test input
- in map[string][]struct { //@mark(testInput, "in")
- // test key
- key string //@mark(testInputKey, "key")
- // test value
- value interface{} //@mark(testInputValue, "value")
- }
- result struct {
- v <-chan struct {
- // expected test value
- value int //@mark(testResultValue, "value")
- }
- }
- }{} {
- _ = tt.in //@hoverdef("in", testInput)
- _ = tt.in["0"][0].key //@hoverdef("key", testInputKey)
- _ = tt.in["0"][0].value //@hoverdef("value", testInputValue)
-
- _ = (<-tt.result.v).value //@hoverdef("value", testResultValue)
- }
-}
-
-func _() {
- getPoints := func() []struct {
- // X coord
- x int //@mark(returnX, "x")
- // Y coord
- y int //@mark(returnY, "y")
- } {
- return nil
- }
-
- r := getPoints()
- r[0].x //@hoverdef("x", returnX)
- r[0].y //@hoverdef("y", returnY)
-}
diff --git a/gopls/internal/lsp/testdata/godef/a/h.go.golden b/gopls/internal/lsp/testdata/godef/a/h.go.golden
deleted file mode 100644
index 7cef9ee967a..00000000000
--- a/gopls/internal/lsp/testdata/godef/a/h.go.golden
+++ /dev/null
@@ -1,161 +0,0 @@
--- arrD-hoverdef --
-```go
-field d int
-```
-
-d field
-
--- arrE-hoverdef --
-```go
-field e struct{f int}
-```
-
-e nested struct
-
--- arrF-hoverdef --
-```go
-field f int
-```
-
-f field of nested struct
-
--- complexH-hoverdef --
-```go
-field h int
-```
-
-h field
-
--- complexI-hoverdef --
-```go
-field i struct{j int}
-```
-
-i nested struct
-
--- complexJ-hoverdef --
-```go
-field j int
-```
-
-j field of nested struct
-
--- mapStructKeyX-hoverdef --
-```go
-field x []string
-```
-
-X key field
-
--- mapStructKeyY-hoverdef --
-```go
-field y string
-```
-
-Y key field
-
--- mapStructValueX-hoverdef --
-```go
-field x string
-```
-
-X value field
-
--- nestedMap-hoverdef --
-```go
-field m map[string]float64
-```
-
-nested map
-
--- nestedNumber-hoverdef --
-```go
-field number int64
-```
-
-nested number
-
--- nestedString-hoverdef --
-```go
-field str string
-```
-
-nested string
-
--- openMethod-hoverdef --
-```go
-func (interface).open() error
-```
-
-open method comment
-
--- returnX-hoverdef --
-```go
-field x int
-```
-
-X coord
-
--- returnY-hoverdef --
-```go
-field y int
-```
-
-Y coord
-
--- structA-hoverdef --
-```go
-field a int
-```
-
-a field
-
--- structB-hoverdef --
-```go
-field b struct{c int}
-```
-
-b nested struct
-
--- structC-hoverdef --
-```go
-field c int
-```
-
-c field of nested struct
-
--- testDescription-hoverdef --
-```go
-field desc string
-```
-
-test description
-
--- testInput-hoverdef --
-```go
-field in map[string][]struct{key string; value interface{}}
-```
-
-test input
-
--- testInputKey-hoverdef --
-```go
-field key string
-```
-
-test key
-
--- testInputValue-hoverdef --
-```go
-field value interface{}
-```
-
-test value
-
--- testResultValue-hoverdef --
-```go
-field value int
-```
-
-expected test value
-
diff --git a/gopls/internal/lsp/testdata/godef/b/e.go b/gopls/internal/lsp/testdata/godef/b/e.go
deleted file mode 100644
index 9c81cad3171..00000000000
--- a/gopls/internal/lsp/testdata/godef/b/e.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package b
-
-import (
- "fmt"
-
- "golang.org/lsptests/godef/a"
-)
-
-func useThings() {
- t := a.Thing{} //@mark(bStructType, "ing")
- fmt.Print(t.Member) //@mark(bMember, "ember")
- fmt.Print(a.Other) //@mark(bVar, "ther")
- a.Things() //@mark(bFunc, "ings")
-}
-
-/*@
-godef(bStructType, Thing)
-godef(bMember, Member)
-godef(bVar, Other)
-godef(bFunc, Things)
-*/
-
-func _() {
- var x interface{} //@mark(eInterface, "interface{}")
- switch x := x.(type) { //@hoverdef("x", eInterface)
- case string: //@mark(eString, "string")
- fmt.Println(x) //@hoverdef("x", eString)
- case int: //@mark(eInt, "int")
- fmt.Println(x) //@hoverdef("x", eInt)
- }
-}
diff --git a/gopls/internal/lsp/testdata/godef/b/e.go.golden b/gopls/internal/lsp/testdata/godef/b/e.go.golden
deleted file mode 100644
index 3d7d8979771..00000000000
--- a/gopls/internal/lsp/testdata/godef/b/e.go.golden
+++ /dev/null
@@ -1,156 +0,0 @@
--- Member-definition --
-godef/a/d.go:6:2-8: defined here as ```go
-field Member string
-```
-
-@Member
-
-
-[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)
--- Member-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 6,
- "column": 2,
- "offset": 90
- },
- "end": {
- "line": 6,
- "column": 8,
- "offset": 96
- }
- },
- "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)"
-}
-
--- Member-hoverdef --
-```go
-field Member string
-```
-
-@Member
-
-
-[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)
--- Other-definition --
-godef/a/d.go:9:5-10: defined here as ```go
-var a.Other a.Thing
-```
-
-@Other
-
-
-[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)
--- Other-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 9,
- "column": 5,
- "offset": 121
- },
- "end": {
- "line": 9,
- "column": 10,
- "offset": 126
- }
- },
- "description": "```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)"
-}
-
--- Other-hoverdef --
-```go
-var a.Other a.Thing
-```
-
-@Other
-
-
-[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)
--- Thing-definition --
-godef/a/d.go:5:6-11: defined here as ```go
-type Thing struct {
- Member string //@Member
-}
-
-func (a.Thing).Method(i int) string
-func (*a.Thing).Method2(i int, j int) (error, string)
-func (a.Thing).Method3()
-```
-
-[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)
--- Thing-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 5,
- "column": 6,
- "offset": 65
- },
- "end": {
- "line": 5,
- "column": 11,
- "offset": 70
- }
- },
- "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n\nfunc (a.Thing).Method(i int) string\nfunc (*a.Thing).Method2(i int, j int) (error, string)\nfunc (a.Thing).Method3()\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)"
-}
-
--- Thing-hoverdef --
-```go
-type Thing struct {
- Member string //@Member
-}
-
-func (a.Thing).Method(i int) string
-func (*a.Thing).Method2(i int, j int) (error, string)
-func (a.Thing).Method3()
-```
-
-[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)
--- Things-definition --
-godef/a/d.go:11:6-12: defined here as ```go
-func a.Things(val []string) []a.Thing
-```
-
-[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)
--- Things-definition-json --
-{
- "span": {
- "uri": "file://godef/a/d.go",
- "start": {
- "line": 11,
- "column": 6,
- "offset": 148
- },
- "end": {
- "line": 11,
- "column": 12,
- "offset": 154
- }
- },
- "description": "```go\nfunc a.Things(val []string) []a.Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)"
-}
-
--- Things-hoverdef --
-```go
-func a.Things(val []string) []a.Thing
-```
-
-[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)
--- eInt-hoverdef --
-```go
-var x int
-```
--- eInterface-hoverdef --
-```go
-var x interface{}
-```
--- eString-hoverdef --
-```go
-var x string
-```
diff --git a/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.golden b/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.golden
deleted file mode 100644
index 9ce869848cb..00000000000
--- a/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.golden
+++ /dev/null
@@ -1,31 +0,0 @@
--- myUnclosedIf-definition --
-godef/broken/unclosedIf.go:7:7-19: defined here as ```go
-var myUnclosedIf string
-```
-
-@myUnclosedIf
--- myUnclosedIf-definition-json --
-{
- "span": {
- "uri": "file://godef/broken/unclosedIf.go",
- "start": {
- "line": 7,
- "column": 7,
- "offset": 68
- },
- "end": {
- "line": 7,
- "column": 19,
- "offset": 80
- }
- },
- "description": "```go\nvar myUnclosedIf string\n```\n\n@myUnclosedIf"
-}
-
--- myUnclosedIf-hoverdef --
-```go
-var myUnclosedIf string
-```
-
-@myUnclosedIf
-
diff --git a/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.in b/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.in
deleted file mode 100644
index 0f2cf1b1e5d..00000000000
--- a/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.in
+++ /dev/null
@@ -1,9 +0,0 @@
-package broken
-
-import "fmt"
-
-func unclosedIf() {
- if false {
- var myUnclosedIf string //@myUnclosedIf
- fmt.Printf("s = %v\n", myUnclosedIf) //@godef("my", myUnclosedIf)
-}
diff --git a/gopls/internal/lsp/testdata/highlights/highlights.go b/gopls/internal/lsp/testdata/highlights/highlights.go
deleted file mode 100644
index 55ae68aa124..00000000000
--- a/gopls/internal/lsp/testdata/highlights/highlights.go
+++ /dev/null
@@ -1,151 +0,0 @@
-package highlights
-
-import (
- "fmt" //@mark(fmtImp, "\"fmt\""),highlight(fmtImp, fmtImp, fmt1, fmt2, fmt3, fmt4)
- h2 "net/http" //@mark(hImp, "h2"),highlight(hImp, hImp, hUse)
- "sort"
-)
-
-type F struct{ bar int } //@mark(barDeclaration, "bar"),highlight(barDeclaration, barDeclaration, bar1, bar2, bar3)
-
-func _() F {
- return F{
- bar: 123, //@mark(bar1, "bar"),highlight(bar1, barDeclaration, bar1, bar2, bar3)
- }
-}
-
-var foo = F{bar: 52} //@mark(fooDeclaration, "foo"),mark(bar2, "bar"),highlight(fooDeclaration, fooDeclaration, fooUse),highlight(bar2, barDeclaration, bar1, bar2, bar3)
-
-func Print() { //@mark(printFunc, "Print"),highlight(printFunc, printFunc, printTest)
- _ = h2.Client{} //@mark(hUse, "h2"),highlight(hUse, hImp, hUse)
-
- fmt.Println(foo) //@mark(fooUse, "foo"),highlight(fooUse, fooDeclaration, fooUse),mark(fmt1, "fmt"),highlight(fmt1, fmtImp, fmt1, fmt2, fmt3, fmt4)
- fmt.Print("yo") //@mark(printSep, "Print"),highlight(printSep, printSep, print1, print2),mark(fmt2, "fmt"),highlight(fmt2, fmtImp, fmt1, fmt2, fmt3, fmt4)
-}
-
-func (x *F) Inc() { //@mark(xRightDecl, "x"),mark(xLeftDecl, " *"),highlight(xRightDecl, xRightDecl, xUse),highlight(xLeftDecl, xRightDecl, xUse)
- x.bar++ //@mark(xUse, "x"),mark(bar3, "bar"),highlight(xUse, xRightDecl, xUse),highlight(bar3, barDeclaration, bar1, bar2, bar3)
-}
-
-func testFunctions() {
- fmt.Print("main start") //@mark(print1, "Print"),highlight(print1, printSep, print1, print2),mark(fmt3, "fmt"),highlight(fmt3, fmtImp, fmt1, fmt2, fmt3, fmt4)
- fmt.Print("ok") //@mark(print2, "Print"),highlight(print2, printSep, print1, print2),mark(fmt4, "fmt"),highlight(fmt4, fmtImp, fmt1, fmt2, fmt3, fmt4)
- Print() //@mark(printTest, "Print"),highlight(printTest, printFunc, printTest)
-}
-
-func toProtocolHighlight(rngs []int) []DocumentHighlight { //@mark(doc1, "DocumentHighlight"),mark(docRet1, "[]DocumentHighlight"),highlight(doc1, docRet1, doc1, doc2, doc3, result)
- result := make([]DocumentHighlight, 0, len(rngs)) //@mark(doc2, "DocumentHighlight"),highlight(doc2, doc1, doc2, doc3)
- for _, rng := range rngs {
- result = append(result, DocumentHighlight{ //@mark(doc3, "DocumentHighlight"),highlight(doc3, doc1, doc2, doc3)
- Range: rng,
- })
- }
- return result //@mark(result, "result")
-}
-
-func testForLoops() {
- for i := 0; i < 10; i++ { //@mark(forDecl1, "for"),highlight(forDecl1, forDecl1, brk1, cont1)
- if i > 8 {
- break //@mark(brk1, "break"),highlight(brk1, forDecl1, brk1, cont1)
- }
- if i < 2 {
- for j := 1; j < 10; j++ { //@mark(forDecl2, "for"),highlight(forDecl2, forDecl2, cont2)
- if j < 3 {
- for k := 1; k < 10; k++ { //@mark(forDecl3, "for"),highlight(forDecl3, forDecl3, cont3)
- if k < 3 {
- continue //@mark(cont3, "continue"),highlight(cont3, forDecl3, cont3)
- }
- }
- continue //@mark(cont2, "continue"),highlight(cont2, forDecl2, cont2)
- }
- }
- continue //@mark(cont1, "continue"),highlight(cont1, forDecl1, brk1, cont1)
- }
- }
-
- arr := []int{}
- for i := range arr { //@mark(forDecl4, "for"),highlight(forDecl4, forDecl4, brk4, cont4)
- if i > 8 {
- break //@mark(brk4, "break"),highlight(brk4, forDecl4, brk4, cont4)
- }
- if i < 4 {
- continue //@mark(cont4, "continue"),highlight(cont4, forDecl4, brk4, cont4)
- }
- }
-
-Outer:
- for i := 0; i < 10; i++ { //@mark(forDecl5, "for"),highlight(forDecl5, forDecl5, brk5, brk6, brk8)
- break //@mark(brk5, "break"),highlight(brk5, forDecl5, brk5, brk6, brk8)
- for { //@mark(forDecl6, "for"),highlight(forDecl6, forDecl6, cont5)
- if i == 1 {
- break Outer //@mark(brk6, "break Outer"),highlight(brk6, forDecl5, brk5, brk6, brk8)
- }
- switch i { //@mark(switch1, "switch"),highlight(switch1, switch1, brk7)
- case 5:
- break //@mark(brk7, "break"),highlight(brk7, switch1, brk7)
- case 6:
- continue //@mark(cont5, "continue"),highlight(cont5, forDecl6, cont5)
- case 7:
- break Outer //@mark(brk8, "break Outer"),highlight(brk8, forDecl5, brk5, brk6, brk8)
- }
- }
- }
-}
-
-func testSwitch() {
- var i, j int
-
-L1:
- for { //@mark(forDecl7, "for"),highlight(forDecl7, forDecl7, brk10, cont6)
- L2:
- switch i { //@mark(switch2, "switch"),highlight(switch2, switch2, brk11, brk12, brk13)
- case 1:
- switch j { //@mark(switch3, "switch"),highlight(switch3, switch3, brk9)
- case 1:
- break //@mark(brk9, "break"),highlight(brk9, switch3, brk9)
- case 2:
- break L1 //@mark(brk10, "break L1"),highlight(brk10, forDecl7, brk10, cont6)
- case 3:
- break L2 //@mark(brk11, "break L2"),highlight(brk11, switch2, brk11, brk12, brk13)
- default:
- continue //@mark(cont6, "continue"),highlight(cont6, forDecl7, brk10, cont6)
- }
- case 2:
- break //@mark(brk12, "break"),highlight(brk12, switch2, brk11, brk12, brk13)
- default:
- break L2 //@mark(brk13, "break L2"),highlight(brk13, switch2, brk11, brk12, brk13)
- }
- }
-}
-
-func testReturn() bool { //@mark(func1, "func"),mark(bool1, "bool"),highlight(func1, func1, fullRet11, fullRet12),highlight(bool1, bool1, false1, bool2, true1)
- if 1 < 2 {
- return false //@mark(ret11, "return"),mark(fullRet11, "return false"),mark(false1, "false"),highlight(ret11, func1, fullRet11, fullRet12)
- }
- candidates := []int{}
- sort.SliceStable(candidates, func(i, j int) bool { //@mark(func2, "func"),mark(bool2, "bool"),highlight(func2, func2, fullRet2)
- return candidates[i] > candidates[j] //@mark(ret2, "return"),mark(fullRet2, "return candidates[i] > candidates[j]"),highlight(ret2, func2, fullRet2)
- })
- return true //@mark(ret12, "return"),mark(fullRet12, "return true"),mark(true1, "true"),highlight(ret12, func1, fullRet11, fullRet12)
-}
-
-func testReturnFields() float64 { //@mark(retVal1, "float64"),highlight(retVal1, retVal1, retVal11, retVal21)
- if 1 < 2 {
- return 20.1 //@mark(retVal11, "20.1"),highlight(retVal11, retVal1, retVal11, retVal21)
- }
- z := 4.3 //@mark(zDecl, "z")
- return z //@mark(retVal21, "z"),highlight(retVal21, retVal1, retVal11, zDecl, retVal21)
-}
-
-func testReturnMultipleFields() (float32, string) { //@mark(retVal31, "float32"),mark(retVal32, "string"),highlight(retVal31, retVal31, retVal41, retVal51),highlight(retVal32, retVal32, retVal42, retVal52)
- y := "im a var" //@mark(yDecl, "y"),
- if 1 < 2 {
- return 20.1, y //@mark(retVal41, "20.1"),mark(retVal42, "y"),highlight(retVal41, retVal31, retVal41, retVal51),highlight(retVal42, retVal32, yDecl, retVal42, retVal52)
- }
- return 4.9, "test" //@mark(retVal51, "4.9"),mark(retVal52, "\"test\""),highlight(retVal51, retVal31, retVal41, retVal51),highlight(retVal52, retVal32, retVal42, retVal52)
-}
-
-func testReturnFunc() int32 { //@mark(retCall, "int32")
- mulch := 1 //@mark(mulchDec, "mulch"),highlight(mulchDec, mulchDec, mulchRet)
- return int32(mulch) //@mark(mulchRet, "mulch"),mark(retFunc, "int32"),mark(retTotal, "int32(mulch)"),highlight(mulchRet, mulchDec, mulchRet),highlight(retFunc, retCall, retFunc, retTotal)
-}
diff --git a/gopls/internal/lsp/testdata/highlights/issue60435.go b/gopls/internal/lsp/testdata/highlights/issue60435.go
deleted file mode 100644
index de0070e5832..00000000000
--- a/gopls/internal/lsp/testdata/highlights/issue60435.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package highlights
-
-import (
- "net/http" //@mark(httpImp, `"net/http"`)
- "net/http/httptest" //@mark(httptestImp, `"net/http/httptest"`)
-)
-
-// This is a regression test for issue 60435:
-// Highlighting "net/http" shouldn't have any effect
-// on an import path that contains it as a substring,
-// such as httptest.
-
-var _ = httptest.NewRequest
-var _ = http.NewRequest //@mark(here, "http"), highlight(here, here, httpImp)
diff --git a/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in b/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in
index 2f4cbada141..05ba54006a5 100644
--- a/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in
+++ b/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in
@@ -1,22 +1,32 @@
package importedcomplit
import (
- "golang.org/lsptests/foo"
+ // TODO(rfindley): re-enable after moving to new framework
+ // "golang.org/lsptests/foo"
+
+ // import completions (separate blocks to avoid comment alignment)
+ "crypto/elli" //@complete("\" //", cryptoImport)
- // import completions
"fm" //@complete("\" //", fmtImport)
+
"go/pars" //@complete("\" //", parserImport)
- "golang.org/lsptests/signa" //@complete("na\" //", signatureImport)
+
+ namedParser "go/pars" //@complete("\" //", parserImport)
+
"golang.org/lspte" //@complete("\" //", lsptestsImport)
- "crypto/elli" //@complete("\" //", cryptoImport)
+
"golang.org/lsptests/sign" //@complete("\" //", signatureImport)
+
"golang.org/lsptests/sign" //@complete("ests", lsptestsImport)
- namedParser "go/pars" //@complete("\" //", parserImport)
+
+ "golang.org/lsptests/signa" //@complete("na\" //", signatureImport)
)
func _() {
var V int //@item(icVVar, "V", "int", "var")
- _ = foo.StructFoo{V} //@complete("}", Value, icVVar)
+
+ // TODO(rfindley): re-enable after moving to new framework
+ // _ = foo.StructFoo{V} // complete("}", Value, icVVar)
}
func _() {
@@ -25,14 +35,16 @@ func _() {
ab int //@item(icABVar, "ab", "int", "var")
)
- _ = foo.StructFoo{a} //@complete("}", abVar, aaVar)
+ // TODO(rfindley): re-enable after moving to new framework
+ // _ = foo.StructFoo{a} // complete("}", abVar, aaVar)
var s struct {
AA string //@item(icFieldAA, "AA", "string", "field")
AB int //@item(icFieldAB, "AB", "int", "field")
}
- _ = foo.StructFoo{s.} //@complete("}", icFieldAB, icFieldAA)
+ // TODO(rfindley): re-enable after moving to new framework
+ //_ = foo.StructFoo{s.} // complete("}", icFieldAB, icFieldAA)
}
/* "fmt" */ //@item(fmtImport, "fmt", "\"fmt\"", "package")
diff --git a/gopls/internal/lsp/testdata/nodisk/empty b/gopls/internal/lsp/testdata/nodisk/empty
deleted file mode 100644
index 0c10a42f942..00000000000
--- a/gopls/internal/lsp/testdata/nodisk/empty
+++ /dev/null
@@ -1 +0,0 @@
-an empty file so that this directory exists
\ No newline at end of file
diff --git a/gopls/internal/lsp/testdata/nodisk/nodisk.overlay.go b/gopls/internal/lsp/testdata/nodisk/nodisk.overlay.go
deleted file mode 100644
index 08aebd12f7b..00000000000
--- a/gopls/internal/lsp/testdata/nodisk/nodisk.overlay.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package nodisk
-
-import (
- "golang.org/lsptests/foo"
-)
-
-func _() {
- foo.Foo() //@complete("F", Foo, IntFoo, StructFoo)
-}
diff --git a/gopls/internal/lsp/testdata/selector/selector.go.in b/gopls/internal/lsp/testdata/selector/selector.go.in
deleted file mode 100644
index b1498a08c77..00000000000
--- a/gopls/internal/lsp/testdata/selector/selector.go.in
+++ /dev/null
@@ -1,66 +0,0 @@
-// +build go1.11
-
-package selector
-
-import (
- "golang.org/lsptests/bar"
-)
-
-type S struct {
- B, A, C int //@item(Bf, "B", "int", "field"),item(Af, "A", "int", "field"),item(Cf, "C", "int", "field")
-}
-
-func _() {
- _ = S{}.; //@complete(";", Af, Bf, Cf)
-}
-
-type bob struct { a int } //@item(a, "a", "int", "field")
-type george struct { b int }
-type jack struct { c int } //@item(c, "c", "int", "field")
-type jill struct { d int }
-
-func (b *bob) george() *george {} //@item(george, "george", "func() *george", "method")
-func (g *george) jack() *jack {}
-func (j *jack) jill() *jill {} //@item(jill, "jill", "func() *jill", "method")
-
-func _() {
- b := &bob{}
- y := b.george().
- jack();
- y.; //@complete(";", c, jill)
-}
-
-func _() {
- bar. //@complete(" /", Bar)
- x := 5
-
- var b *bob
- b. //@complete(" /", a, george)
- y, z := 5, 6
-
- b. //@complete(" /", a, george)
- y, z, a, b, c := 5, 6
-}
-
-func _() {
- bar. //@complete(" /", Bar)
- bar.Bar()
-
- bar. //@complete(" /", Bar)
- go f()
-}
-
-func _() {
- var b *bob
- if y != b. //@complete(" /", a, george)
- z := 5
-
- if z + y + 1 + b. //@complete(" /", a, george)
- r, s, t := 4, 5
-
- if y != b. //@complete(" /", a, george)
- z = 5
-
- if z + y + 1 + b. //@complete(" /", a, george)
- r = 4
-}
diff --git a/gopls/internal/lsp/testdata/snippets/literal_snippets.go.in b/gopls/internal/lsp/testdata/snippets/literal_snippets.go.in
deleted file mode 100644
index c6e6c0fbd60..00000000000
--- a/gopls/internal/lsp/testdata/snippets/literal_snippets.go.in
+++ /dev/null
@@ -1,233 +0,0 @@
-package snippets
-
-import (
- "bytes"
- "context"
- "go/ast"
- "net/http"
- "sort"
-
- "golang.org/lsptests/foo"
-)
-
-func _() {
- []int{} //@item(litIntSlice, "[]int{}", "", "var")
- &[]int{} //@item(litIntSliceAddr, "&[]int{}", "", "var")
- make([]int, 0) //@item(makeIntSlice, "make([]int, 0)", "", "func")
-
- var _ *[]int = in //@snippet(" //", litIntSliceAddr, "&[]int{$0\\}", "&[]int{$0\\}")
- var _ **[]int = in //@complete(" //")
-
- var slice []int
- slice = i //@snippet(" //", litIntSlice, "[]int{$0\\}", "[]int{$0\\}")
- slice = m //@snippet(" //", makeIntSlice, "make([]int, ${1:})", "make([]int, ${1:0})")
-}
-
-func _() {
- type namedInt []int
-
- namedInt{} //@item(litNamedSlice, "namedInt{}", "", "var")
- make(namedInt, 0) //@item(makeNamedSlice, "make(namedInt, 0)", "", "func")
-
- var namedSlice namedInt
- namedSlice = n //@snippet(" //", litNamedSlice, "namedInt{$0\\}", "namedInt{$0\\}")
- namedSlice = m //@snippet(" //", makeNamedSlice, "make(namedInt, ${1:})", "make(namedInt, ${1:0})")
-}
-
-func _() {
- make(chan int) //@item(makeChan, "make(chan int)", "", "func")
-
- var ch chan int
- ch = m //@snippet(" //", makeChan, "make(chan int)", "make(chan int)")
-}
-
-func _() {
- map[string]struct{}{} //@item(litMap, "map[string]struct{}{}", "", "var")
- make(map[string]struct{}) //@item(makeMap, "make(map[string]struct{})", "", "func")
-
- var m map[string]struct{}
- m = m //@snippet(" //", litMap, "map[string]struct{\\}{$0\\}", "map[string]struct{\\}{$0\\}")
- m = m //@snippet(" //", makeMap, "make(map[string]struct{\\})", "make(map[string]struct{\\})")
-
- struct{}{} //@item(litEmptyStruct, "struct{}{}", "", "var")
-
- m["hi"] = s //@snippet(" //", litEmptyStruct, "struct{\\}{\\}", "struct{\\}{\\}")
-}
-
-func _() {
- type myStruct struct{ i int } //@item(myStructType, "myStruct", "struct{...}", "struct")
-
- myStruct{} //@item(litStruct, "myStruct{}", "", "var")
- &myStruct{} //@item(litStructPtr, "&myStruct{}", "", "var")
-
- var ms myStruct
- ms = m //@snippet(" //", litStruct, "myStruct{$0\\}", "myStruct{$0\\}")
-
- var msPtr *myStruct
- msPtr = m //@snippet(" //", litStructPtr, "&myStruct{$0\\}", "&myStruct{$0\\}")
-
- msPtr = &m //@snippet(" //", litStruct, "myStruct{$0\\}", "myStruct{$0\\}")
-
- type myStructCopy struct { i int } //@item(myStructCopyType, "myStructCopy", "struct{...}", "struct")
-
- // Don't offer literal completion for convertible structs.
- ms = myStruct //@complete(" //", litStruct, myStructType, myStructCopyType)
-}
-
-type myImpl struct{}
-
-func (myImpl) foo() {}
-
-func (*myImpl) bar() {}
-
-type myBasicImpl string
-
-func (myBasicImpl) foo() {}
-
-func _() {
- type myIntf interface {
- foo()
- }
-
- myImpl{} //@item(litImpl, "myImpl{}", "", "var")
-
- var mi myIntf
- mi = m //@snippet(" //", litImpl, "myImpl{\\}", "myImpl{\\}")
-
- myBasicImpl() //@item(litBasicImpl, "myBasicImpl()", "string", "var")
-
- mi = m //@snippet(" //", litBasicImpl, "myBasicImpl($0)", "myBasicImpl($0)")
-
- // only satisfied by pointer to myImpl
- type myPtrIntf interface {
- bar()
- }
-
- &myImpl{} //@item(litImplPtr, "&myImpl{}", "", "var")
-
- var mpi myPtrIntf
- mpi = m //@snippet(" //", litImplPtr, "&myImpl{\\}", "&myImpl{\\}")
-}
-
-func _() {
- var s struct{ i []int } //@item(litSliceField, "i", "[]int", "field")
- var foo []int
- // no literal completions after selector
- foo = s.i //@complete(" //", litSliceField)
-}
-
-func _() {
- type myStruct struct{ i int } //@item(litMyStructType, "myStruct", "struct{...}", "struct")
- myStruct{} //@item(litMyStruct, "myStruct{}", "", "var")
-
- foo := func(s string, args ...myStruct) {}
- // Don't give literal slice candidate for variadic arg.
- // Do give literal candidates for variadic element.
- foo("", myStruct) //@complete(")", litMyStruct, litMyStructType)
-}
-
-func _() {
- Buffer{} //@item(litBuffer, "Buffer{}", "", "var")
-
- var b *bytes.Buffer
- b = bytes.Bu //@snippet(" //", litBuffer, "Buffer{\\}", "Buffer{\\}")
-}
-
-func _() {
- _ = "func(...) {}" //@item(litFunc, "func(...) {}", "", "var")
-
- sort.Slice(nil, fun) //@complete(")", litFunc),snippet(")", litFunc, "func(i, j int) bool {$0\\}", "func(i, j int) bool {$0\\}")
-
- http.HandleFunc("", f) //@snippet(")", litFunc, "func(w http.ResponseWriter, r *http.Request) {$0\\}", "func(${1:w} http.ResponseWriter, ${2:r} *http.Request) {$0\\}")
-
- // no literal "func" completions
- http.Handle("", fun) //@complete(")")
-
- http.HandlerFunc() //@item(handlerFunc, "http.HandlerFunc()", "", "var")
- http.Handle("", h) //@snippet(")", handlerFunc, "http.HandlerFunc($0)", "http.HandlerFunc($0)")
- http.Handle("", http.HandlerFunc()) //@snippet("))", litFunc, "func(w http.ResponseWriter, r *http.Request) {$0\\}", "func(${1:w} http.ResponseWriter, ${2:r} *http.Request) {$0\\}")
-
- var namedReturn func(s string) (b bool)
- namedReturn = f //@snippet(" //", litFunc, "func(s string) (b bool) {$0\\}", "func(s string) (b bool) {$0\\}")
-
- var multiReturn func() (bool, int)
- multiReturn = f //@snippet(" //", litFunc, "func() (bool, int) {$0\\}", "func() (bool, int) {$0\\}")
-
- var multiNamedReturn func() (b bool, i int)
- multiNamedReturn = f //@snippet(" //", litFunc, "func() (b bool, i int) {$0\\}", "func() (b bool, i int) {$0\\}")
-
- var duplicateParams func(myImpl, int, myImpl)
- duplicateParams = f //@snippet(" //", litFunc, "func(mi1 myImpl, i int, mi2 myImpl) {$0\\}", "func(${1:mi1} myImpl, ${2:i} int, ${3:mi2} myImpl) {$0\\}")
-
- type aliasImpl = myImpl
- var aliasParams func(aliasImpl) aliasImpl
- aliasParams = f //@snippet(" //", litFunc, "func(ai aliasImpl) aliasImpl {$0\\}", "func(${1:ai} aliasImpl) aliasImpl {$0\\}")
-
- const two = 2
- var builtinTypes func([]int, [two]bool, map[string]string, struct{ i int }, interface{ foo() }, <-chan int)
- builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [two]bool, m map[string]string, s struct{ i int \\}, i2 interface{ foo() \\}, c <-chan int) {$0\\}", "func(${1:i1} []int, ${2:b} [two]bool, ${3:m} map[string]string, ${4:s} struct{ i int \\}, ${5:i2} interface{ foo() \\}, ${6:c} <-chan int) {$0\\}")
-
- var _ func(ast.Node) = f //@snippet(" //", litFunc, "func(n ast.Node) {$0\\}", "func(${1:n} ast.Node) {$0\\}")
- var _ func(error) = f //@snippet(" //", litFunc, "func(err error) {$0\\}", "func(${1:err} error) {$0\\}")
- var _ func(context.Context) = f //@snippet(" //", litFunc, "func(ctx context.Context) {$0\\}", "func(${1:ctx} context.Context) {$0\\}")
-
- type context struct {}
- var _ func(context) = f //@snippet(" //", litFunc, "func(ctx context) {$0\\}", "func(${1:ctx} context) {$0\\}")
-}
-
-func _() {
- StructFoo{} //@item(litStructFoo, "StructFoo{}", "struct{...}", "struct")
-
- var sfp *foo.StructFoo
- // Don't insert the "&" before "StructFoo{}".
- sfp = foo.Str //@snippet(" //", litStructFoo, "StructFoo{$0\\}", "StructFoo{$0\\}")
-
- var sf foo.StructFoo
- sf = foo.Str //@snippet(" //", litStructFoo, "StructFoo{$0\\}", "StructFoo{$0\\}")
- sf = foo. //@snippet(" //", litStructFoo, "StructFoo{$0\\}", "StructFoo{$0\\}")
-}
-
-func _() {
- float64() //@item(litFloat64, "float64()", "float64", "var")
-
- // don't complete to "&float64()"
- var _ *float64 = float64 //@complete(" //")
-
- var f float64
- f = fl //@complete(" //", litFloat64),snippet(" //", litFloat64, "float64($0)", "float64($0)")
-
- type myInt int
- myInt() //@item(litMyInt, "myInt()", "", "var")
-
- var mi myInt
- mi = my //@snippet(" //", litMyInt, "myInt($0)", "myInt($0)")
-}
-
-func _() {
- type ptrStruct struct {
- p *ptrStruct
- }
-
- ptrStruct{} //@item(litPtrStruct, "ptrStruct{}", "", "var")
-
- ptrStruct{
- p: &ptrSt, //@rank(",", litPtrStruct)
- }
-
- &ptrStruct{} //@item(litPtrStructPtr, "&ptrStruct{}", "", "var")
-
- &ptrStruct{
- p: ptrSt, //@rank(",", litPtrStructPtr)
- }
-}
-
-func _() {
- f := func(...[]int) {}
- f() //@snippet(")", litIntSlice, "[]int{$0\\}", "[]int{$0\\}")
-}
-
-
-func _() {
- // don't complete to "untyped int()"
- []int{}[untyped] //@complete("] //")
-}
diff --git a/gopls/internal/lsp/testdata/snippets/literal_snippets118.go.in b/gopls/internal/lsp/testdata/snippets/literal_snippets118.go.in
deleted file mode 100644
index 8251a6384a3..00000000000
--- a/gopls/internal/lsp/testdata/snippets/literal_snippets118.go.in
+++ /dev/null
@@ -1,14 +0,0 @@
-// +build go1.18
-//go:build go1.18
-
-package snippets
-
-type Tree[T any] struct{}
-
-func (tree Tree[T]) Do(f func(s T)) {}
-
-func _() {
- _ = "func(...) {}" //@item(litFunc, "func(...) {}", "", "var")
- var t Tree[string]
- t.Do(fun) //@complete(")", litFunc),snippet(")", litFunc, "func(s string) {$0\\}", "func(s string) {$0\\}")
-}
diff --git a/gopls/internal/lsp/testdata/snippets/snippets.go.in b/gopls/internal/lsp/testdata/snippets/snippets.go.in
index 58150c644ca..79bff334233 100644
--- a/gopls/internal/lsp/testdata/snippets/snippets.go.in
+++ b/gopls/internal/lsp/testdata/snippets/snippets.go.in
@@ -1,5 +1,8 @@
package snippets
+// Pre-set this marker, as we don't have a "source" for it in this package.
+/* Error() */ //@item(Error, "Error", "func() string", "method")
+
type AliasType = int //@item(sigAliasType, "AliasType", "AliasType", "type")
func foo(i int, b bool) {} //@item(snipFoo, "foo", "func(i int, b bool)", "func")
diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden
index 4e6c3a08cdc..7059a381d19 100644
--- a/gopls/internal/lsp/testdata/summary.txt.golden
+++ b/gopls/internal/lsp/testdata/summary.txt.golden
@@ -1,25 +1,18 @@
-- summary --
CallHierarchyCount = 2
-CodeLensCount = 5
-CompletionsCount = 263
-CompletionSnippetCount = 106
-UnimportedCompletionsCount = 5
+CompletionsCount = 194
+CompletionSnippetCount = 74
DeepCompletionsCount = 5
FuzzyCompletionsCount = 8
-RankedCompletionsCount = 164
+RankedCompletionsCount = 166
CaseSensitiveCompletionsCount = 4
-DiagnosticsCount = 23
-FoldingRangesCount = 2
SemanticTokenCount = 3
-SuggestedFixCount = 74
+SuggestedFixCount = 80
MethodExtractionCount = 8
-DefinitionsCount = 46
-TypeDefinitionsCount = 18
-HighlightsCount = 70
-InlayHintsCount = 4
-RenamesCount = 41
+InlayHintsCount = 5
+RenamesCount = 48
PrepareRenamesCount = 7
-SignaturesCount = 33
+SignaturesCount = 32
LinksCount = 7
SelectionRangesCount = 3
diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden
deleted file mode 100644
index 7375b821e69..00000000000
--- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden
+++ /dev/null
@@ -1,25 +0,0 @@
--- summary --
-CallHierarchyCount = 2
-CodeLensCount = 5
-CompletionsCount = 264
-CompletionSnippetCount = 115
-UnimportedCompletionsCount = 5
-DeepCompletionsCount = 5
-FuzzyCompletionsCount = 8
-RankedCompletionsCount = 174
-CaseSensitiveCompletionsCount = 4
-DiagnosticsCount = 23
-FoldingRangesCount = 2
-SemanticTokenCount = 3
-SuggestedFixCount = 80
-MethodExtractionCount = 8
-DefinitionsCount = 46
-TypeDefinitionsCount = 18
-HighlightsCount = 70
-InlayHintsCount = 5
-RenamesCount = 48
-PrepareRenamesCount = 7
-SignaturesCount = 33
-LinksCount = 7
-SelectionRangesCount = 3
-
diff --git a/gopls/internal/lsp/testdata/summary_go1.21.txt.golden b/gopls/internal/lsp/testdata/summary_go1.21.txt.golden
deleted file mode 100644
index 619c25ba757..00000000000
--- a/gopls/internal/lsp/testdata/summary_go1.21.txt.golden
+++ /dev/null
@@ -1,25 +0,0 @@
--- summary --
-CallHierarchyCount = 2
-CodeLensCount = 5
-CompletionsCount = 263
-CompletionSnippetCount = 115
-UnimportedCompletionsCount = 5
-DeepCompletionsCount = 5
-FuzzyCompletionsCount = 8
-RankedCompletionsCount = 174
-CaseSensitiveCompletionsCount = 4
-DiagnosticsCount = 24
-FoldingRangesCount = 2
-SemanticTokenCount = 3
-SuggestedFixCount = 80
-MethodExtractionCount = 8
-DefinitionsCount = 46
-TypeDefinitionsCount = 18
-HighlightsCount = 70
-InlayHintsCount = 5
-RenamesCount = 48
-PrepareRenamesCount = 7
-SignaturesCount = 33
-LinksCount = 7
-SelectionRangesCount = 3
-
diff --git a/gopls/internal/lsp/testdata/testy/testy.go b/gopls/internal/lsp/testdata/testy/testy.go
deleted file mode 100644
index 9f74091af87..00000000000
--- a/gopls/internal/lsp/testdata/testy/testy.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package testy
-
-func a() { //@item(funcA, "a", "func()", "func")
- //@complete("", funcA)
-}
diff --git a/gopls/internal/lsp/testdata/testy/testy_test.go b/gopls/internal/lsp/testdata/testy/testy_test.go
deleted file mode 100644
index 793aacfd825..00000000000
--- a/gopls/internal/lsp/testdata/testy/testy_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package testy
-
-import (
- "testing"
-
- sig "golang.org/lsptests/signature"
- "golang.org/lsptests/snippets"
-)
-
-func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *testing.T)", "", "func")
- var x int //@mark(testyX, "x"),diag("x", "compiler", "x declared (and|but) not used", "error")
- a() //@mark(testyA, "a")
-}
-
-func _() {
- _ = snippets.X(nil) //@signature("nil", "X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias", 0)
- var _ sig.Alias
-}
diff --git a/gopls/internal/lsp/testdata/testy/testy_test.go.golden b/gopls/internal/lsp/testdata/testy/testy_test.go.golden
deleted file mode 100644
index cafc380d065..00000000000
--- a/gopls/internal/lsp/testdata/testy/testy_test.go.golden
+++ /dev/null
@@ -1,3 +0,0 @@
--- X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias-signature --
-X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias
-
diff --git a/gopls/internal/lsp/testdata/typdef/typdef.go b/gopls/internal/lsp/testdata/typdef/typdef.go
deleted file mode 100644
index bd2ea4b00c7..00000000000
--- a/gopls/internal/lsp/testdata/typdef/typdef.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package typdef
-
-type Struct struct { //@item(Struct, "Struct", "struct{...}", "struct")
- Field string
-}
-
-type Int int //@item(Int, "Int", "int", "type")
-
-func _() {
- var (
- value Struct
- point *Struct
- )
- _ = value //@typdef("value", Struct)
- _ = point //@typdef("point", Struct)
-
- var (
- array [3]Struct
- slice []Struct
- ch chan Struct
- complex [3]chan *[5][]Int
- )
- _ = array //@typdef("array", Struct)
- _ = slice //@typdef("slice", Struct)
- _ = ch //@typdef("ch", Struct)
- _ = complex //@typdef("complex", Int)
-
- var s struct {
- x struct {
- xx struct {
- field1 []Struct
- field2 []Int
- }
- }
- }
- s.x.xx.field1 //@typdef("field1", Struct)
- s.x.xx.field2 //@typdef("field2", Int)
-}
-
-func F1() Int { return 0 }
-func F2() (Int, float64) { return 0, 0 }
-func F3() (Struct, int, bool, error) { return Struct{}, 0, false, nil }
-func F4() (**int, Int, bool, *error) { return nil, Struct{}, false, nil }
-func F5() (int, float64, error, Struct) { return 0, 0, nil, Struct{} }
-func F6() (int, float64, ***Struct, error) { return 0, 0, nil, nil }
-
-func _() {
- F1() //@typdef("F1", Int)
- F2() //@typdef("F2", Int)
- F3() //@typdef("F3", Struct)
- F4() //@typdef("F4", Int)
- F5() //@typdef("F5", Struct)
- F6() //@typdef("F6", Struct)
-
- f := func() Int { return 0 }
- f() //@typdef("f", Int)
-}
-
-// https://github.com/golang/go/issues/38589#issuecomment-620350922
-func _() {
- type myFunc func(int) Int //@item(myFunc, "myFunc", "func", "type")
-
- var foo myFunc
- bar := foo() //@typdef("foo", myFunc)
-}
diff --git a/gopls/internal/lsp/testdata/unimported/export_test.go b/gopls/internal/lsp/testdata/unimported/export_test.go
deleted file mode 100644
index 707768e1da2..00000000000
--- a/gopls/internal/lsp/testdata/unimported/export_test.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package unimported
-
-var TestExport int //@item(testexport, "TestExport", "var (from \"golang.org/lsptests/unimported\")", "var")
diff --git a/gopls/internal/lsp/testdata/unimported/unimported.go.in b/gopls/internal/lsp/testdata/unimported/unimported.go.in
deleted file mode 100644
index 74d51ffe82a..00000000000
--- a/gopls/internal/lsp/testdata/unimported/unimported.go.in
+++ /dev/null
@@ -1,23 +0,0 @@
-package unimported
-
-func _() {
- http //@unimported("p", nethttp)
- // container/ring is extremely unlikely to be imported by anything, so shouldn't have type information.
- ring.Ring //@unimported("Ring", ringring)
- signature.Foo //@unimported("Foo", signaturefoo)
-
- context.Bac //@unimported(" //", contextBackground)
-}
-
-// Create markers for unimported std lib packages. Only for use by this test.
-/* http */ //@item(nethttp, "http", "\"net/http\"", "package")
-
-/* ring.Ring */ //@item(ringring, "Ring", "(from \"container/ring\")", "var")
-
-/* signature.Foo */ //@item(signaturefoo, "Foo", "func (from \"golang.org/lsptests/signature\")", "func")
-
-/* context.Background */ //@item(contextBackground, "Background", "func (from \"context\")", "func")
-
-// Now that we no longer type-check imported completions,
-// we don't expect the context.Background().Err method (see golang/go#58663).
-/* context.Background().Err */ //@item(contextBackgroundErr, "Background().Err", "func (from \"context\")", "method")
diff --git a/gopls/internal/lsp/testdata/unimported/unimported_cand_type.go b/gopls/internal/lsp/testdata/unimported/unimported_cand_type.go
deleted file mode 100644
index 554c426a998..00000000000
--- a/gopls/internal/lsp/testdata/unimported/unimported_cand_type.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package unimported
-
-import (
- _ "context"
-
- "golang.org/lsptests/baz"
- _ "golang.org/lsptests/signature" // provide type information for unimported completions in the other file
-)
-
-func _() {
- foo.StructFoo{} //@item(litFooStructFoo, "foo.StructFoo{}", "struct{...}", "struct")
-
- // We get the literal completion for "foo.StructFoo{}" even though we haven't
- // imported "foo" yet.
- baz.FooStruct = f //@snippet(" //", litFooStructFoo, "foo.StructFoo{$0\\}", "foo.StructFoo{$0\\}")
-}
diff --git a/gopls/internal/lsp/testdata/unimported/x_test.go b/gopls/internal/lsp/testdata/unimported/x_test.go
deleted file mode 100644
index 681dcb2536d..00000000000
--- a/gopls/internal/lsp/testdata/unimported/x_test.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package unimported_test
-
-import (
- "testing"
-)
-
-func TestSomething(t *testing.T) {
- _ = unimported.TestExport //@unimported("TestExport", testexport)
-}
diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go
index 4f5fc3c5080..5b7074fe6fa 100644
--- a/gopls/internal/lsp/tests/tests.go
+++ b/gopls/internal/lsp/tests/tests.go
@@ -13,7 +13,6 @@ import (
"go/ast"
"go/token"
"io"
- "io/ioutil"
"os"
"path/filepath"
"regexp"
@@ -27,7 +26,6 @@ import (
"golang.org/x/tools/go/expect"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/packages/packagestest"
- "golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/lsp/source"
@@ -43,6 +41,7 @@ const (
overlayFileSuffix = ".overlay"
goldenFileSuffix = ".golden"
inFileSuffix = ".in"
+ summaryFile = "summary.txt"
// The module path containing the testdata packages.
//
@@ -51,37 +50,21 @@ const (
testModule = "golang.org/lsptests"
)
-var summaryFile = "summary.txt"
-
-func init() {
- if testenv.Go1Point() >= 21 {
- summaryFile = "summary_go1.21.txt"
- } else if testenv.Go1Point() >= 18 {
- summaryFile = "summary_go1.18.txt"
- }
-}
-
var UpdateGolden = flag.Bool("golden", false, "Update golden files")
// These type names apparently avoid the need to repeat the
// type in the field name and the make() expression.
type CallHierarchy = map[span.Span]*CallHierarchyResult
-type CodeLens = map[span.URI][]protocol.CodeLens
-type Diagnostics = map[span.URI][]*source.Diagnostic
type CompletionItems = map[token.Pos]*completion.CompletionItem
type Completions = map[span.Span][]Completion
type CompletionSnippets = map[span.Span][]CompletionSnippet
-type UnimportedCompletions = map[span.Span][]Completion
type DeepCompletions = map[span.Span][]Completion
type FuzzyCompletions = map[span.Span][]Completion
type CaseSensitiveCompletions = map[span.Span][]Completion
type RankCompletions = map[span.Span][]Completion
-type FoldingRanges = []span.Span
type SemanticTokens = []span.Span
type SuggestedFixes = map[span.Span][]SuggestedFix
type MethodExtractions = map[span.Span]span.Span
-type Definitions = map[span.Span]Definition
-type Highlights = map[span.Span][]span.Span
type Renames = map[span.Span]string
type PrepareRenames = map[span.Span]*source.PrepareItem
type InlayHints = []span.Span
@@ -94,22 +77,16 @@ type Data struct {
Config packages.Config
Exported *packagestest.Exported
CallHierarchy CallHierarchy
- CodeLens CodeLens
- Diagnostics Diagnostics
CompletionItems CompletionItems
Completions Completions
CompletionSnippets CompletionSnippets
- UnimportedCompletions UnimportedCompletions
DeepCompletions DeepCompletions
FuzzyCompletions FuzzyCompletions
CaseSensitiveCompletions CaseSensitiveCompletions
RankCompletions RankCompletions
- FoldingRanges FoldingRanges
SemanticTokens SemanticTokens
SuggestedFixes SuggestedFixes
MethodExtractions MethodExtractions
- Definitions Definitions
- Highlights Highlights
Renames Renames
InlayHints InlayHints
PrepareRenames PrepareRenames
@@ -130,28 +107,22 @@ type Data struct {
}
// The Tests interface abstracts the LSP-based implementation of the marker
-// test operators (such as @codelens) appearing in files beneath ../testdata/.
+// test operators appearing in files beneath ../testdata/.
//
// TODO(adonovan): reduce duplication; see https://github.com/golang/go/issues/54845.
// There is only one implementation (*runner in ../lsp_test.go), so
// we can abolish the interface now.
type Tests interface {
CallHierarchy(*testing.T, span.Span, *CallHierarchyResult)
- CodeLens(*testing.T, span.URI, []protocol.CodeLens)
- Diagnostics(*testing.T, span.URI, []*source.Diagnostic)
Completion(*testing.T, span.Span, Completion, CompletionItems)
CompletionSnippet(*testing.T, span.Span, CompletionSnippet, bool, CompletionItems)
- UnimportedCompletion(*testing.T, span.Span, Completion, CompletionItems)
DeepCompletion(*testing.T, span.Span, Completion, CompletionItems)
FuzzyCompletion(*testing.T, span.Span, Completion, CompletionItems)
CaseSensitiveCompletion(*testing.T, span.Span, Completion, CompletionItems)
RankCompletion(*testing.T, span.Span, Completion, CompletionItems)
- FoldingRanges(*testing.T, span.Span)
SemanticTokens(*testing.T, span.Span)
SuggestedFix(*testing.T, span.Span, []SuggestedFix, int)
MethodExtraction(*testing.T, span.Span, span.Span)
- Definition(*testing.T, span.Span, Definition)
- Highlight(*testing.T, span.Span, []span.Span)
InlayHints(*testing.T, span.Span)
Rename(*testing.T, span.Span, string)
PrepareRename(*testing.T, span.Span, *source.PrepareItem)
@@ -161,22 +132,12 @@ type Tests interface {
SelectionRanges(*testing.T, span.Span)
}
-type Definition struct {
- Name string
- IsType bool
- OnlyHover bool
- Src, Def span.Span
-}
-
type CompletionTestType int
const (
// Default runs the standard completion tests.
CompletionDefault = CompletionTestType(iota)
- // Unimported tests the autocompletion of unimported packages.
- CompletionUnimported
-
// Deep tests deep completion.
CompletionDeep
@@ -241,13 +202,19 @@ func DefaultOptions(o *source.Options) {
source.Work: {},
source.Tmpl: {},
}
- o.UserOptions.Codelenses[string(command.Test)] = true
- o.HoverKind = source.SynopsisDocumentation
o.InsertTextFormat = protocol.SnippetTextFormat
o.CompletionBudget = time.Minute
o.HierarchicalDocumentSymbolSupport = true
o.SemanticTokens = true
o.InternalOptions.NewDiff = "new"
+
+ // Enable all inlay hints.
+ if o.Hints == nil {
+ o.Hints = make(map[string]bool)
+ }
+ for name := range source.AllInlayHints {
+ o.Hints[name] = true
+ }
}
func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*testing.T, *Data)) {
@@ -268,18 +235,13 @@ func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*tes
func load(t testing.TB, mode string, dir string) *Data {
datum := &Data{
CallHierarchy: make(CallHierarchy),
- CodeLens: make(CodeLens),
- Diagnostics: make(Diagnostics),
CompletionItems: make(CompletionItems),
Completions: make(Completions),
CompletionSnippets: make(CompletionSnippets),
- UnimportedCompletions: make(UnimportedCompletions),
DeepCompletions: make(DeepCompletions),
FuzzyCompletions: make(FuzzyCompletions),
RankCompletions: make(RankCompletions),
CaseSensitiveCompletions: make(CaseSensitiveCompletions),
- Definitions: make(Definitions),
- Highlights: make(Highlights),
Renames: make(Renames),
PrepareRenames: make(PrepareRenames),
SuggestedFixes: make(SuggestedFixes),
@@ -338,7 +300,7 @@ func load(t testing.TB, mode string, dir string) *Data {
} else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 {
delete(files, fragment)
partial := fragment[:index] + fragment[index+len(overlayFileSuffix):]
- contents, err := ioutil.ReadFile(filepath.Join(dir, fragment))
+ contents, err := os.ReadFile(filepath.Join(dir, fragment))
if err != nil {
t.Fatal(err)
}
@@ -419,22 +381,14 @@ func load(t testing.TB, mode string, dir string) *Data {
// Collect any data that needs to be used by subsequent tests.
if err := datum.Exported.Expect(map[string]interface{}{
- "codelens": datum.collectCodeLens,
- "diag": datum.collectDiagnostics,
"item": datum.collectCompletionItems,
"complete": datum.collectCompletions(CompletionDefault),
- "unimported": datum.collectCompletions(CompletionUnimported),
"deep": datum.collectCompletions(CompletionDeep),
"fuzzy": datum.collectCompletions(CompletionFuzzy),
"casesensitive": datum.collectCompletions(CompletionCaseSensitive),
"rank": datum.collectCompletions(CompletionRank),
"snippet": datum.collectCompletionSnippets,
- "fold": datum.collectFoldingRanges,
"semantic": datum.collectSemanticTokens,
- "godef": datum.collectDefinitions,
- "typdef": datum.collectTypeDefinitions,
- "hoverdef": datum.collectHoverDefinitions,
- "highlight": datum.collectHighlights,
"inlayHint": datum.collectInlayHints,
"rename": datum.collectRenames,
"prepare": datum.collectPrepareRenames,
@@ -450,13 +404,6 @@ func load(t testing.TB, mode string, dir string) *Data {
t.Fatal(err)
}
- // Collect names for the entries that require golden files.
- if err := datum.Exported.Expect(map[string]interface{}{
- "godef": datum.collectDefinitionNames,
- "hoverdef": datum.collectDefinitionNames,
- }); err != nil {
- t.Fatal(err)
- }
if mode == "MultiModule" {
if err := moveFile(filepath.Join(datum.Config.Dir, "go.mod"), filepath.Join(datum.Config.Dir, "testmodule/go.mod")); err != nil {
t.Fatal(err)
@@ -558,11 +505,6 @@ func Run(t *testing.T, tests Tests, data *Data) {
}
})
- t.Run("UnimportedCompletion", func(t *testing.T) {
- t.Helper()
- eachCompletion(t, data.UnimportedCompletions, tests.UnimportedCompletion)
- })
-
t.Run("DeepCompletion", func(t *testing.T) {
t.Helper()
eachCompletion(t, data.DeepCompletions, tests.DeepCompletion)
@@ -583,44 +525,6 @@ func Run(t *testing.T, tests Tests, data *Data) {
eachCompletion(t, data.RankCompletions, tests.RankCompletion)
})
- t.Run("CodeLens", func(t *testing.T) {
- t.Helper()
- for uri, want := range data.CodeLens {
- // Check if we should skip this URI if the -modfile flag is not available.
- if shouldSkip(data, uri) {
- continue
- }
- t.Run(uriName(uri), func(t *testing.T) {
- t.Helper()
- tests.CodeLens(t, uri, want)
- })
- }
- })
-
- t.Run("Diagnostics", func(t *testing.T) {
- t.Helper()
- for uri, want := range data.Diagnostics {
- // Check if we should skip this URI if the -modfile flag is not available.
- if shouldSkip(data, uri) {
- continue
- }
- t.Run(uriName(uri), func(t *testing.T) {
- t.Helper()
- tests.Diagnostics(t, uri, want)
- })
- }
- })
-
- t.Run("FoldingRange", func(t *testing.T) {
- t.Helper()
- for _, spn := range data.FoldingRanges {
- t.Run(uriName(spn.URI()), func(t *testing.T) {
- t.Helper()
- tests.FoldingRanges(t, spn)
- })
- }
- })
-
t.Run("SemanticTokens", func(t *testing.T) {
t.Helper()
for _, spn := range data.SemanticTokens {
@@ -659,29 +563,6 @@ func Run(t *testing.T, tests Tests, data *Data) {
}
})
- t.Run("Definition", func(t *testing.T) {
- t.Helper()
- for spn, d := range data.Definitions {
- t.Run(SpanName(spn), func(t *testing.T) {
- t.Helper()
- if strings.Contains(t.Name(), "cgo") {
- testenv.NeedsTool(t, "cgo")
- }
- tests.Definition(t, spn, d)
- })
- }
- })
-
- t.Run("Highlight", func(t *testing.T) {
- t.Helper()
- for pos, locations := range data.Highlights {
- t.Run(SpanName(pos), func(t *testing.T) {
- t.Helper()
- tests.Highlight(t, pos, locations)
- })
- }
- })
-
t.Run("InlayHints", func(t *testing.T) {
t.Helper()
for _, src := range data.InlayHints {
@@ -770,7 +651,7 @@ func Run(t *testing.T, tests Tests, data *Data) {
sort.Slice(golden.Archive.Files, func(i, j int) bool {
return golden.Archive.Files[i].Name < golden.Archive.Files[j].Name
})
- if err := ioutil.WriteFile(golden.Filename, txtar.Format(golden.Archive), 0666); err != nil {
+ if err := os.WriteFile(golden.Filename, txtar.Format(golden.Archive), 0666); err != nil {
t.Fatal(err)
}
}
@@ -779,23 +660,10 @@ func Run(t *testing.T, tests Tests, data *Data) {
func checkData(t *testing.T, data *Data) {
buf := &bytes.Buffer{}
- diagnosticsCount := 0
- for _, want := range data.Diagnostics {
- diagnosticsCount += len(want)
- }
linksCount := 0
for _, want := range data.Links {
linksCount += len(want)
}
- definitionCount := 0
- typeDefinitionCount := 0
- for _, d := range data.Definitions {
- if d.IsType {
- typeDefinitionCount++
- } else {
- definitionCount++
- }
- }
snippetCount := 0
for _, want := range data.CompletionSnippets {
@@ -809,30 +677,16 @@ func checkData(t *testing.T, data *Data) {
return count
}
- countCodeLens := func(c map[span.URI][]protocol.CodeLens) (count int) {
- for _, want := range c {
- count += len(want)
- }
- return count
- }
-
fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy))
- fmt.Fprintf(buf, "CodeLensCount = %v\n", countCodeLens(data.CodeLens))
fmt.Fprintf(buf, "CompletionsCount = %v\n", countCompletions(data.Completions))
fmt.Fprintf(buf, "CompletionSnippetCount = %v\n", snippetCount)
- fmt.Fprintf(buf, "UnimportedCompletionsCount = %v\n", countCompletions(data.UnimportedCompletions))
fmt.Fprintf(buf, "DeepCompletionsCount = %v\n", countCompletions(data.DeepCompletions))
fmt.Fprintf(buf, "FuzzyCompletionsCount = %v\n", countCompletions(data.FuzzyCompletions))
fmt.Fprintf(buf, "RankedCompletionsCount = %v\n", countCompletions(data.RankCompletions))
fmt.Fprintf(buf, "CaseSensitiveCompletionsCount = %v\n", countCompletions(data.CaseSensitiveCompletions))
- fmt.Fprintf(buf, "DiagnosticsCount = %v\n", diagnosticsCount)
- fmt.Fprintf(buf, "FoldingRangesCount = %v\n", len(data.FoldingRanges))
fmt.Fprintf(buf, "SemanticTokenCount = %v\n", len(data.SemanticTokens))
fmt.Fprintf(buf, "SuggestedFixCount = %v\n", len(data.SuggestedFixes))
fmt.Fprintf(buf, "MethodExtractionCount = %v\n", len(data.MethodExtractions))
- fmt.Fprintf(buf, "DefinitionsCount = %v\n", definitionCount)
- fmt.Fprintf(buf, "TypeDefinitionsCount = %v\n", typeDefinitionCount)
- fmt.Fprintf(buf, "HighlightsCount = %v\n", len(data.Highlights))
fmt.Fprintf(buf, "InlayHintsCount = %v\n", len(data.InlayHints))
fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames))
fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames))
@@ -919,37 +773,6 @@ func (data *Data) Golden(t *testing.T, tag, target string, update func() ([]byte
return file.Data[:len(file.Data)-1] // drop the trailing \n
}
-func (data *Data) collectCodeLens(spn span.Span, title, cmd string) {
- data.CodeLens[spn.URI()] = append(data.CodeLens[spn.URI()], protocol.CodeLens{
- Range: data.mustRange(spn),
- Command: &protocol.Command{
- Title: title,
- Command: cmd,
- },
- })
-}
-
-func (data *Data) collectDiagnostics(spn span.Span, msgSource, msgPattern, msgSeverity string) {
- severity := protocol.SeverityError
- switch msgSeverity {
- case "error":
- severity = protocol.SeverityError
- case "warning":
- severity = protocol.SeverityWarning
- case "hint":
- severity = protocol.SeverityHint
- case "information":
- severity = protocol.SeverityInformation
- }
-
- data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], &source.Diagnostic{
- Range: data.mustRange(spn),
- Severity: severity,
- Source: source.DiagnosticSource(msgSource),
- Message: msgPattern,
- })
-}
-
func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) {
result := func(m map[span.Span][]Completion, src span.Span, expected []token.Pos) {
m[src] = append(m[src], Completion{
@@ -961,10 +784,6 @@ func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []t
return func(src span.Span, expected []token.Pos) {
result(data.DeepCompletions, src, expected)
}
- case CompletionUnimported:
- return func(src span.Span, expected []token.Pos) {
- result(data.UnimportedCompletions, src, expected)
- }
case CompletionFuzzy:
return func(src span.Span, expected []token.Pos) {
result(data.FuzzyCompletions, src, expected)
@@ -997,10 +816,6 @@ func (data *Data) collectCompletionItems(pos token.Pos, label, detail, kind stri
}
}
-func (data *Data) collectFoldingRanges(spn span.Span) {
- data.FoldingRanges = append(data.FoldingRanges, spn)
-}
-
func (data *Data) collectAddImports(spn span.Span, imp string) {
data.AddImport[spn.URI()] = imp
}
@@ -1019,13 +834,6 @@ func (data *Data) collectMethodExtractions(start span.Span, end span.Span) {
}
}
-func (data *Data) collectDefinitions(src, target span.Span) {
- data.Definitions[src] = Definition{
- Src: src,
- Def: target,
- }
-}
-
func (data *Data) collectSelectionRanges(spn span.Span) {
data.SelectionRanges = append(data.SelectionRanges, spn)
}
@@ -1064,33 +872,6 @@ func (data *Data) collectOutgoingCalls(src span.Span, calls []span.Span) {
}
}
-func (data *Data) collectHoverDefinitions(src, target span.Span) {
- data.Definitions[src] = Definition{
- Src: src,
- Def: target,
- OnlyHover: true,
- }
-}
-
-func (data *Data) collectTypeDefinitions(src, target span.Span) {
- data.Definitions[src] = Definition{
- Src: src,
- Def: target,
- IsType: true,
- }
-}
-
-func (data *Data) collectDefinitionNames(src span.Span, name string) {
- d := data.Definitions[src]
- d.Name = name
- data.Definitions[src] = d
-}
-
-func (data *Data) collectHighlights(src span.Span, expected []span.Span) {
- // Declaring a highlight in a test file: @highlight(src, expected1, expected2)
- data.Highlights[src] = append(data.Highlights[src], expected...)
-}
-
func (data *Data) collectInlayHints(src span.Span) {
data.InlayHints = append(data.InlayHints, src)
}
diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go
index a4bfaa0152a..b9a21fe9627 100644
--- a/gopls/internal/lsp/tests/util.go
+++ b/gopls/internal/lsp/tests/util.go
@@ -8,17 +8,13 @@ import (
"bytes"
"fmt"
"go/token"
- "path"
- "regexp"
"sort"
"strconv"
"strings"
- "testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/source/completion"
"golang.org/x/tools/gopls/internal/lsp/tests/compare"
"golang.org/x/tools/gopls/internal/span"
@@ -92,73 +88,6 @@ func DiffLinks(mapper *protocol.Mapper, wantLinks []Link, gotLinks []protocol.Do
return msg.String()
}
-// CompareDiagnostics reports testing errors to t when the diagnostic set got
-// does not match want.
-func CompareDiagnostics(t *testing.T, uri span.URI, want, got []*source.Diagnostic) {
- t.Helper()
- fileName := path.Base(string(uri))
-
- // Build a helper function to match an actual diagnostic to an overlapping
- // expected diagnostic (if any).
- unmatched := make([]*source.Diagnostic, len(want))
- copy(unmatched, want)
- source.SortDiagnostics(unmatched)
- match := func(g *source.Diagnostic) *source.Diagnostic {
- // Find the last expected diagnostic d for which start(d) < end(g), and
- // check to see if it overlaps.
- i := sort.Search(len(unmatched), func(i int) bool {
- d := unmatched[i]
- // See rangeOverlaps: if a range is a single point, we consider End to be
- // included in the range...
- if g.Range.Start == g.Range.End {
- return protocol.ComparePosition(d.Range.Start, g.Range.End) > 0
- }
- // ...otherwise the end position of a range is not included.
- return protocol.ComparePosition(d.Range.Start, g.Range.End) >= 0
- })
- if i == 0 {
- return nil
- }
- w := unmatched[i-1]
- if rangeOverlaps(w.Range, g.Range) {
- unmatched = append(unmatched[:i-1], unmatched[i:]...)
- return w
- }
- return nil
- }
-
- for _, g := range got {
- w := match(g)
- if w == nil {
- t.Errorf("%s:%s: unexpected diagnostic %q", fileName, g.Range, g.Message)
- continue
- }
- if match, err := regexp.MatchString(w.Message, g.Message); err != nil {
- t.Errorf("%s:%s: invalid regular expression %q: %v", fileName, w.Range.Start, w.Message, err)
- } else if !match {
- t.Errorf("%s:%s: got Message %q, want match for pattern %q", fileName, g.Range.Start, g.Message, w.Message)
- }
- if w.Severity != g.Severity {
- t.Errorf("%s:%s: got Severity %v, want %v", fileName, g.Range.Start, g.Severity, w.Severity)
- }
- if w.Source != g.Source {
- t.Errorf("%s:%s: got Source %v, want %v", fileName, g.Range.Start, g.Source, w.Source)
- }
- }
-
- for _, w := range unmatched {
- t.Errorf("%s:%s: unmatched diagnostic pattern %q", fileName, w.Range, w.Message)
- }
-}
-
-// rangeOverlaps reports whether r1 and r2 overlap.
-func rangeOverlaps(r1, r2 protocol.Range) bool {
- if inRange(r2.Start, r1) || inRange(r1.Start, r2) {
- return true
- }
- return false
-}
-
// inRange reports whether p is contained within [r.Start, r.End), or if p ==
// r.Start == r.End (special handling for the case where the range is a single
// point).
@@ -172,67 +101,6 @@ func inRange(p protocol.Position, r protocol.Range) bool {
return false
}
-func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string {
- sortCodeLens(want)
- sortCodeLens(got)
-
- if len(got) != len(want) {
- return summarizeCodeLens(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
- }
- for i, w := range want {
- g := got[i]
- if w.Command.Command != g.Command.Command {
- return summarizeCodeLens(i, uri, want, got, "incorrect Command Name got %v want %v", g.Command.Command, w.Command.Command)
- }
- if w.Command.Title != g.Command.Title {
- return summarizeCodeLens(i, uri, want, got, "incorrect Command Title got %v want %v", g.Command.Title, w.Command.Title)
- }
- if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
- return summarizeCodeLens(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
- }
- if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the codelens returns a zero-length range.
- if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
- return summarizeCodeLens(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
- }
- }
- }
- return ""
-}
-
-func sortCodeLens(c []protocol.CodeLens) {
- sort.Slice(c, func(i int, j int) bool {
- if r := protocol.CompareRange(c[i].Range, c[j].Range); r != 0 {
- return r < 0
- }
- if c[i].Command.Command < c[j].Command.Command {
- return true
- } else if c[i].Command.Command == c[j].Command.Command {
- return c[i].Command.Title < c[j].Command.Title
- } else {
- return false
- }
- })
-}
-
-func summarizeCodeLens(i int, uri span.URI, want, got []protocol.CodeLens, reason string, args ...interface{}) string {
- msg := &bytes.Buffer{}
- fmt.Fprint(msg, "codelens failed")
- if i >= 0 {
- fmt.Fprintf(msg, " at %d", i)
- }
- fmt.Fprint(msg, " because of ")
- fmt.Fprintf(msg, reason, args...)
- fmt.Fprint(msg, ":\nexpected:\n")
- for _, d := range want {
- fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
- }
- fmt.Fprintf(msg, "got:\n")
- for _, d := range got {
- fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
- }
- return msg.String()
-}
-
func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) string {
decorate := func(f string, args ...interface{}) string {
return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
@@ -441,38 +309,3 @@ func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason
}
return msg.String()
}
-
-func EnableAllAnalyzers(opts *source.Options) {
- if opts.Analyses == nil {
- opts.Analyses = make(map[string]bool)
- }
- for _, a := range opts.DefaultAnalyzers {
- if !a.IsEnabled(opts) {
- opts.Analyses[a.Analyzer.Name] = true
- }
- }
- for _, a := range opts.TypeErrorAnalyzers {
- if !a.IsEnabled(opts) {
- opts.Analyses[a.Analyzer.Name] = true
- }
- }
- for _, a := range opts.ConvenienceAnalyzers {
- if !a.IsEnabled(opts) {
- opts.Analyses[a.Analyzer.Name] = true
- }
- }
- for _, a := range opts.StaticcheckAnalyzers {
- if !a.IsEnabled(opts) {
- opts.Analyses[a.Analyzer.Name] = true
- }
- }
-}
-
-func EnableAllInlayHints(opts *source.Options) {
- if opts.Hints == nil {
- opts.Hints = make(map[string]bool)
- }
- for name := range source.AllInlayHints {
- opts.Hints[name] = true
- }
-}
diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go
index 5584287a78c..0dddab2b14c 100644
--- a/gopls/internal/lsp/text_synchronization.go
+++ b/gopls/internal/lsp/text_synchronization.go
@@ -20,28 +20,27 @@ import (
"golang.org/x/tools/internal/jsonrpc2"
)
-// ModificationSource identifies the originating cause of a file modification.
+// ModificationSource identifies the origin of a change.
type ModificationSource int
const (
- // FromDidOpen is a file modification caused by opening a file.
+ // FromDidOpen is from a didOpen notification.
FromDidOpen = ModificationSource(iota)
- // FromDidChange is a file modification caused by changing a file.
+ // FromDidChange is from a didChange notification.
FromDidChange
- // FromDidChangeWatchedFiles is a file modification caused by a change to a
- // watched file.
+ // FromDidChangeWatchedFiles is from didChangeWatchedFiles notification.
FromDidChangeWatchedFiles
- // FromDidSave is a file modification caused by a file save.
+ // FromDidSave is from a didSave notification.
FromDidSave
- // FromDidClose is a file modification caused by closing a file.
+ // FromDidClose is from a didClose notification.
FromDidClose
- // TODO: add FromDidChangeConfiguration, once configuration changes cause a
- // new snapshot to be created.
+ // FromDidChangeConfiguration is from a didChangeConfiguration notification.
+ FromDidChangeConfiguration
// FromRegenerateCgo refers to file modifications caused by regenerating
// the cgo sources for the workspace.
@@ -88,6 +87,8 @@ func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocume
// views, but it won't because ViewOf only returns an error when there
// are no views in the session. I don't know if that logic should go
// here, or if we can continue to rely on that implementation detail.
+ //
+ // TODO(golang/go#57979): this will be generalized to a different view calculation.
if _, err := s.session.ViewOf(uri); err != nil {
dir := filepath.Dir(uri.Filename())
if err := s.addFolders(ctx, []protocol.WorkspaceFolder{{
@@ -239,7 +240,7 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File
wg.Add(1)
defer wg.Done()
- if s.session.Options().VerboseWorkDoneProgress {
+ if s.Options().VerboseWorkDoneProgress {
work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil)
go func() {
wg.Wait()
diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go
index e5f813e730c..d48e4f473cf 100644
--- a/gopls/internal/lsp/workspace.go
+++ b/gopls/internal/lsp/workspace.go
@@ -7,6 +7,7 @@ package lsp
import (
"context"
"fmt"
+ "sync"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
@@ -36,8 +37,8 @@ func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source
if state < serverInitialized {
return nil, nil, fmt.Errorf("addView called before server initialized")
}
- options := s.session.Options().Clone()
- if err := s.fetchConfig(ctx, name, uri, options); err != nil {
+ options, err := s.fetchFolderOptions(ctx, uri)
+ if err != nil {
return nil, nil, err
}
_, snapshot, release, err := s.session.NewView(ctx, name, uri, options)
@@ -49,43 +50,32 @@ func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChan
defer done()
// Apply any changes to the session-level settings.
- options := s.session.Options().Clone()
- if err := s.fetchConfig(ctx, "", "", options); err != nil {
+ options, err := s.fetchFolderOptions(ctx, "")
+ if err != nil {
return err
}
- s.session.SetOptions(options)
+ s.SetOptions(options)
- // Go through each view, getting and updating its configuration.
+ // Collect options for all workspace folders.
+ seen := make(map[span.URI]bool)
for _, view := range s.session.Views() {
- options := s.session.Options().Clone()
- if err := s.fetchConfig(ctx, view.Name(), view.Folder(), options); err != nil {
- return err
+ if seen[view.Folder()] {
+ continue
}
- _, err := s.session.SetViewOptions(ctx, view, options)
+ seen[view.Folder()] = true
+ options, err := s.fetchFolderOptions(ctx, view.Folder())
if err != nil {
return err
}
+ s.session.SetFolderOptions(ctx, view.Folder(), options)
}
- // Now that all views have been updated: reset vulncheck diagnostics, rerun
- // diagnostics, and hope for the best...
- //
- // TODO(golang/go#60465): this not a reliable way to ensure the correctness
- // of the resulting diagnostics below. A snapshot could still be in the
- // process of diagnosing the workspace, and not observe the configuration
- // changes above.
- //
- // The real fix is golang/go#42814: we should create a new snapshot on any
- // change that could affect the derived results in that snapshot. However, we
- // are currently (2023-05-26) on the verge of a release, and the proper fix
- // is too risky a change. Since in the common case a configuration change is
- // only likely to occur during a period of quiescence on the server, it is
- // likely that the clearing below will have the desired effect.
- s.clearDiagnosticSource(modVulncheckSource)
-
+ var wg sync.WaitGroup
for _, view := range s.session.Views() {
view := view
+ wg.Add(1)
go func() {
+ defer wg.Done()
snapshot, release, err := view.Snapshot()
if err != nil {
return // view is shut down; no need to diagnose
@@ -95,6 +85,14 @@ func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChan
}()
}
+ if s.Options().VerboseWorkDoneProgress {
+ work := s.progress.Start(ctx, DiagnosticWorkTitle(FromDidChangeConfiguration), "Calculating diagnostics...", nil, nil)
+ go func() {
+ wg.Wait()
+ work.End(ctx, "Done.")
+ }()
+ }
+
// An options change may have affected the detected Go version.
s.checkViewGoVersions()
diff --git a/gopls/internal/lsp/workspace_symbol.go b/gopls/internal/lsp/workspace_symbol.go
index 88b3e8865ae..eb690b047e5 100644
--- a/gopls/internal/lsp/workspace_symbol.go
+++ b/gopls/internal/lsp/workspace_symbol.go
@@ -9,16 +9,22 @@ import (
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
)
-func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
+func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) (_ []protocol.SymbolInformation, rerr error) {
+ recordLatency := telemetry.StartLatencyTimer("symbol")
+ defer func() {
+ recordLatency(ctx, rerr)
+ }()
+
ctx, done := event.Start(ctx, "lsp.Server.symbol")
defer done()
views := s.session.Views()
- matcher := s.session.Options().SymbolMatcher
- style := s.session.Options().SymbolStyle
+ matcher := s.Options().SymbolMatcher
+ style := s.Options().SymbolStyle
// TODO(rfindley): it looks wrong that we need to pass views here.
//
// Evidence:
diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go
index 0120a1a65f0..610f43fa876 100644
--- a/gopls/internal/regtest/bench/bench_test.go
+++ b/gopls/internal/regtest/bench/bench_test.go
@@ -11,7 +11,6 @@ import (
"flag"
"fmt"
"io"
- "io/ioutil"
"log"
"os"
"os/exec"
@@ -77,7 +76,7 @@ func TestMain(m *testing.M) {
func getTempDir() string {
makeTempDirOnce.Do(func() {
var err error
- tempDir, err = ioutil.TempDir("", "gopls-bench")
+ tempDir, err = os.MkdirTemp("", "gopls-bench")
if err != nil {
log.Fatal(err)
}
diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go
index 0400e70b0bd..02e640423b9 100644
--- a/gopls/internal/regtest/bench/completion_test.go
+++ b/gopls/internal/regtest/bench/completion_test.go
@@ -5,6 +5,7 @@
package bench
import (
+ "flag"
"fmt"
"sync/atomic"
"testing"
@@ -145,105 +146,145 @@ func (c *completer) _() {
}, b)
}
-// Benchmark completion following an arbitrary edit.
-//
-// Edits force type-checked packages to be invalidated, so we want to measure
-// how long it takes before completion results are available.
-func BenchmarkCompletionFollowingEdit(b *testing.B) {
- tests := []struct {
- repo string
- file string // repo-relative file to create
- content string // file content
- locationRegexp string // regexp for completion
- }{
- {
- "tools",
- "internal/lsp/source/completion/completion2.go",
- `
+type completionFollowingEditTest struct {
+ repo string
+ name string
+ file string // repo-relative file to create
+ content string // file content
+ locationRegexp string // regexp for completion
+}
+
+var completionFollowingEditTests = []completionFollowingEditTest{
+ {
+ "tools",
+ "selector",
+ "internal/lsp/source/completion/completion2.go",
+ `
package completion
func (c *completer) _() {
c.inference.kindMatches(c.)
}
`,
- `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
- },
- {
- "kubernetes",
- "pkg/kubelet/kubelet2.go",
- `
+ `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
+ },
+ {
+ "kubernetes",
+ "selector",
+ "pkg/kubelet/kubelet2.go",
+ `
package kubelet
func (kl *Kubelet) _() {
kl.
}
`,
- `kl\.()`,
- },
- {
- "oracle",
- "dataintegration/pivot2.go",
- `
+ `kl\.()`,
+ },
+ {
+ "kubernetes",
+ "identifier",
+ "pkg/kubelet/kubelet2.go",
+ `
+package kubelet
+
+func (kl *Kubelet) _() {
+ k // here
+}
+`,
+ `k() // here`,
+ },
+ {
+ "oracle",
+ "selector",
+ "dataintegration/pivot2.go",
+ `
package dataintegration
func (p *Pivot) _() {
p.
}
`,
- `p\.()`,
- },
- }
+ `p\.()`,
+ },
+}
- for _, test := range tests {
- b.Run(test.repo, func(b *testing.B) {
- repo := getRepo(b, test.repo)
- sharedEnv := repo.sharedEnv(b) // ensure cache is warm
- env := repo.newEnv(b, fake.EditorConfig{
- Env: map[string]string{
- "GOPATH": sharedEnv.Sandbox.GOPATH(), // use the warm cache
- },
- Settings: map[string]interface{}{
- "completeUnimported": false,
- },
- }, "completionFollowingEdit", false)
- defer env.Close()
-
- env.CreateBuffer(test.file, "// __REGTEST_PLACEHOLDER_0__\n"+test.content)
- editPlaceholder := func() {
- edits := atomic.AddInt64(&editID, 1)
- env.EditBuffer(test.file, protocol.TextEdit{
- Range: protocol.Range{
- Start: protocol.Position{Line: 0, Character: 0},
- End: protocol.Position{Line: 1, Character: 0},
- },
- // Increment the placeholder text, to ensure cache misses.
- NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits),
+// Benchmark completion following an arbitrary edit.
+//
+// Edits force type-checked packages to be invalidated, so we want to measure
+// how long it takes before completion results are available.
+func BenchmarkCompletionFollowingEdit(b *testing.B) {
+ for _, test := range completionFollowingEditTests {
+ b.Run(fmt.Sprintf("%s_%s", test.repo, test.name), func(b *testing.B) {
+ for _, completeUnimported := range []bool{true, false} {
+ b.Run(fmt.Sprintf("completeUnimported=%v", completeUnimported), func(b *testing.B) {
+ for _, budget := range []string{"0s", "100ms"} {
+ b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) {
+ runCompletionFollowingEdit(b, test, completeUnimported, budget)
+ })
+ }
})
}
- env.AfterChange()
+ })
+ }
+}
- // Run a completion to make sure the system is warm.
- loc := env.RegexpSearch(test.file, test.locationRegexp)
- completions := env.Completion(loc)
+var gomodcache = flag.String("gomodcache", "", "optional GOMODCACHE for unimported completion benchmarks")
- if testing.Verbose() {
- fmt.Println("Results:")
- for i := 0; i < len(completions.Items); i++ {
- fmt.Printf("\t%d. %v\n", i, completions.Items[i])
- }
- }
+func runCompletionFollowingEdit(b *testing.B, test completionFollowingEditTest, completeUnimported bool, budget string) {
+ repo := getRepo(b, test.repo)
+ sharedEnv := repo.sharedEnv(b) // ensure cache is warm
+ envvars := map[string]string{
+ "GOPATH": sharedEnv.Sandbox.GOPATH(), // use the warm cache
+ }
- b.ResetTimer()
+ if *gomodcache != "" {
+ envvars["GOMODCACHE"] = *gomodcache
+ }
- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "completionFollowingEdit")); stopAndRecord != nil {
- defer stopAndRecord()
- }
+ env := repo.newEnv(b, fake.EditorConfig{
+ Env: envvars,
+ Settings: map[string]interface{}{
+ "completeUnimported": completeUnimported,
+ "completionBudget": budget,
+ },
+ }, "completionFollowingEdit", false)
+ defer env.Close()
- for i := 0; i < b.N; i++ {
- editPlaceholder()
- loc := env.RegexpSearch(test.file, test.locationRegexp)
- env.Completion(loc)
- }
+ env.CreateBuffer(test.file, "// __REGTEST_PLACEHOLDER_0__\n"+test.content)
+ editPlaceholder := func() {
+ edits := atomic.AddInt64(&editID, 1)
+ env.EditBuffer(test.file, protocol.TextEdit{
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 0, Character: 0},
+ End: protocol.Position{Line: 1, Character: 0},
+ },
+ // Increment the placeholder text, to ensure cache misses.
+ NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits),
})
}
+ env.AfterChange()
+
+ // Run a completion to make sure the system is warm.
+ loc := env.RegexpSearch(test.file, test.locationRegexp)
+ completions := env.Completion(loc)
+
+ if testing.Verbose() {
+ fmt.Println("Results:")
+ for i, item := range completions.Items {
+ fmt.Printf("\t%d. %v\n", i, item)
+ }
+ }
+
+ b.ResetTimer()
+
+ if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "completionFollowingEdit")); stopAndRecord != nil {
+ defer stopAndRecord()
+ }
+
+ for i := 0; i < b.N; i++ {
+ editPlaceholder()
+ loc := env.RegexpSearch(test.file, test.locationRegexp)
+ env.Completion(loc)
+ }
}
diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go
index 117e940e012..81300eb07e0 100644
--- a/gopls/internal/regtest/completion/completion_test.go
+++ b/gopls/internal/regtest/completion/completion_test.go
@@ -6,6 +6,7 @@ package completion
import (
"fmt"
+ "sort"
"strings"
"testing"
"time"
@@ -868,3 +869,137 @@ use ./dir/foobar/
}
})
}
+
+func TestBuiltinCompletion(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- a.go --
+package a
+
+func _() {
+ // here
+}
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a.go")
+ result := env.Completion(env.RegexpSearch("a.go", `// here`))
+ builtins := []string{
+ "any", "append", "bool", "byte", "cap", "close",
+ "comparable", "complex", "complex128", "complex64", "copy", "delete",
+ "error", "false", "float32", "float64", "imag", "int", "int16", "int32",
+ "int64", "int8", "len", "make", "new", "panic", "print", "println", "real",
+ "recover", "rune", "string", "true", "uint", "uint16", "uint32", "uint64",
+ "uint8", "uintptr", "nil",
+ }
+ if testenv.Go1Point() >= 21 {
+ builtins = append(builtins, "clear", "max", "min")
+ }
+ sort.Strings(builtins)
+ var got []string
+
+ for _, item := range result.Items {
+ // TODO(rfindley): for flexibility, ignore zero while it is being
+ // implemented. Remove this if/when zero lands.
+ if item.Label != "zero" {
+ got = append(got, item.Label)
+ }
+ }
+ sort.Strings(got)
+
+ if diff := cmp.Diff(builtins, got); diff != "" {
+ t.Errorf("Completion: unexpected mismatch (-want +got):\n%s", diff)
+ }
+ })
+}
+
+func TestOverlayCompletion(t *testing.T) {
+ const files = `
+-- go.mod --
+module foo.test
+
+go 1.18
+
+-- foo/foo.go --
+package foo
+
+type Foo struct{}
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.CreateBuffer("nodisk/nodisk.go", `
+package nodisk
+
+import (
+ "foo.test/foo"
+)
+
+func _() {
+ foo.Foo()
+}
+`)
+ list := env.Completion(env.RegexpSearch("nodisk/nodisk.go", "foo.(Foo)"))
+ want := []string{"Foo"}
+ var got []string
+ for _, item := range list.Items {
+ got = append(got, item.Label)
+ }
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("Completion: unexpected mismatch (-want +got):\n%s", diff)
+ }
+ })
+}
+
+// Fix for golang/go#60062: unimported completion included "golang.org/toolchain" results.
+func TestToolchainCompletions(t *testing.T) {
+ const files = `
+-- go.mod --
+module foo.test/foo
+
+go 1.21
+
+-- foo.go --
+package foo
+
+func _() {
+ os.Open
+}
+
+func _() {
+ strings
+}
+`
+
+ const proxy = `
+-- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/go.mod --
+module golang.org/toolchain
+-- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/src/os/os.go --
+package os
+
+func Open() {}
+-- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/src/strings/strings.go --
+package strings
+
+func Join() {}
+`
+
+ WithOptions(
+ ProxyFiles(proxy),
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.RunGoCommand("mod", "download", "golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64")
+ env.OpenFile("foo.go")
+
+ for _, pattern := range []string{"os.Open()", "string()"} {
+ loc := env.RegexpSearch("foo.go", pattern)
+ res := env.Completion(loc)
+ for _, item := range res.Items {
+ if strings.Contains(item.Detail, "golang.org/toolchain") {
+ t.Errorf("Completion(...) returned toolchain item %#v", item)
+ }
+ }
+ }
+ })
+}
diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
index 8066b7502c2..f5aa240e49d 100644
--- a/gopls/internal/regtest/diagnostics/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go
@@ -559,7 +559,7 @@ func f() {
// Deleting the import dismisses the warning.
env.RegexpReplace("a.go", `import "mod.com/hello"`, "")
env.AfterChange(
- NoOutstandingWork(),
+ NoOutstandingWork(IgnoreTelemetryPromptWork),
)
})
}
@@ -576,7 +576,7 @@ hi mom
).Run(t, files, func(t *testing.T, env *Env) {
env.OnceMet(
InitialWorkspaceLoad,
- NoOutstandingWork(),
+ NoOutstandingWork(IgnoreTelemetryPromptWork),
)
})
})
@@ -1469,7 +1469,7 @@ package foo_
env.RegexpReplace("foo/foo_test.go", "_t", "_test")
env.AfterChange(
NoDiagnostics(ForFile("foo/foo_test.go")),
- NoOutstandingWork(),
+ NoOutstandingWork(IgnoreTelemetryPromptWork),
)
})
}
@@ -1503,7 +1503,7 @@ go 1.hello
env.RegexpReplace("go.mod", "go 1.hello", "go 1.12")
env.SaveBufferWithoutActions("go.mod")
env.AfterChange(
- NoOutstandingWork(),
+ NoOutstandingWork(IgnoreTelemetryPromptWork),
)
})
}
diff --git a/gopls/internal/regtest/marker/testdata/codeaction/inline.txt b/gopls/internal/regtest/marker/testdata/codeaction/inline.txt
index 8f1ea924864..15d3cabfcc8 100644
--- a/gopls/internal/regtest/marker/testdata/codeaction/inline.txt
+++ b/gopls/internal/regtest/marker/testdata/codeaction/inline.txt
@@ -17,7 +17,7 @@ func add(x, y int) int { return x + y }
package a
func _() {
- println(func(x, y int) int { return x + y }(1, 2)) //@codeaction("refactor.inline", "add", ")", inline)
+ println(1 + 2) //@codeaction("refactor.inline", "add", ")", inline)
}
func add(x, y int) int { return x + y }
diff --git a/gopls/internal/regtest/marker/testdata/codelens/generate.txt b/gopls/internal/regtest/marker/testdata/codelens/generate.txt
new file mode 100644
index 00000000000..086c961f07d
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/codelens/generate.txt
@@ -0,0 +1,9 @@
+This test exercises the "generate" codelens.
+
+-- generate.go --
+//@codelenses()
+
+package generate
+
+//go:generate echo Hi //@ codelens("//go:generate", "run go generate"), codelens("//go:generate", "run go generate ./...")
+//go:generate echo I shall have no CodeLens
diff --git a/gopls/internal/regtest/marker/testdata/codelens/test.txt b/gopls/internal/regtest/marker/testdata/codelens/test.txt
new file mode 100644
index 00000000000..90782bddef9
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/codelens/test.txt
@@ -0,0 +1,31 @@
+This file tests codelenses for test functions.
+
+TODO: for some reason these code lens have zero width. Does that affect their
+utility/visibility in various LSP clients?
+
+-- settings.json --
+{
+ "codelenses": {
+ "test": true
+ }
+}
+
+-- p_test.go --
+//@codelenses()
+
+package codelens //@codelens(re"()package codelens", "run file benchmarks")
+
+import "testing"
+
+func TestMain(m *testing.M) {} // no code lens for TestMain
+
+func TestFuncWithCodeLens(t *testing.T) { //@codelens(re"()func", "run test")
+}
+
+func thisShouldNotHaveACodeLens(t *testing.T) {
+}
+
+func BenchmarkFuncWithCodeLens(b *testing.B) { //@codelens(re"()func", "run benchmark")
+}
+
+func helper() {} // expect no code lens
diff --git a/gopls/internal/regtest/marker/testdata/completion/bad.txt b/gopls/internal/regtest/marker/testdata/completion/bad.txt
new file mode 100644
index 00000000000..4da021ae322
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/completion/bad.txt
@@ -0,0 +1,68 @@
+This test exercises completion in the presence of type errors.
+
+Note: this test was ported from the old marker tests, which did not enable
+unimported completion. Enabling it causes matches in e.g. crypto/rand.
+
+-- settings.json --
+{
+ "completeUnimported": false
+}
+
+-- go.mod --
+module bad.test
+
+go 1.18
+
+-- bad/bad0.go --
+package bad
+
+func stuff() { //@item(stuff, "stuff", "func()", "func")
+ x := "heeeeyyyy"
+ random2(x) //@diag("x", re"cannot use x \\(variable of type string\\) as int value in argument to random2")
+ random2(1) //@complete("dom", random, random2, random3)
+ y := 3 //@diag("y", re"y declared (and|but) not used")
+}
+
+type bob struct { //@item(bob, "bob", "struct{...}", "struct")
+ x int
+}
+
+func _() {
+ var q int
+ _ = &bob{
+ f: q, //@diag("f: q", re"unknown field f in struct literal")
+ }
+}
+
+-- bad/bad1.go --
+package bad
+
+// See #36637
+type stateFunc func() stateFunc //@item(stateFunc, "stateFunc", "func() stateFunc", "type")
+
+var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", re"(undeclared name|undefined): unknown")
+
+func random() int { //@item(random, "random", "func() int", "func")
+ //@complete("", global_a, bob, random, random2, random3, stateFunc, stuff)
+ return 0
+}
+
+func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func"),item(bad_y_param, "y", "int", "var")
+ x := 6 //@item(x, "x", "int", "var"),diag("x", re"x declared (and|but) not used")
+ var q blah //@item(q, "q", "blah", "var"),diag("q", re"q declared (and|but) not used"),diag("blah", re"(undeclared name|undefined): blah")
+ var t **blob //@item(t, "t", "**blob", "var"),diag("t", re"t declared (and|but) not used"),diag("blob", re"(undeclared name|undefined): blob")
+ //@complete("", q, t, x, bad_y_param, global_a, bob, random, random2, random3, stateFunc, stuff)
+
+ return y
+}
+
+func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),item(y_variadic_param, "y", "[]int", "var")
+ //@complete("", y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff)
+
+ var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", re"ch declared (and|but) not used"),diag("favType1", re"(undeclared name|undefined): favType1")
+ var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", re"m declared (and|but) not used"),diag("keyType", re"(undeclared name|undefined): keyType")
+ var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", re"arr declared (and|but) not used"),diag("favType2", re"(undeclared name|undefined): favType2")
+ var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", re"fn1 declared (and|but) not used"),diag("badResult", re"(undeclared name|undefined): badResult")
+ var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", re"fn2 declared (and|but) not used"),diag("badParam", re"(undeclared name|undefined): badParam")
+ //@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff)
+}
diff --git a/gopls/internal/regtest/marker/testdata/completion/foobarbaz.txt b/gopls/internal/regtest/marker/testdata/completion/foobarbaz.txt
new file mode 100644
index 00000000000..24ac7171055
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/completion/foobarbaz.txt
@@ -0,0 +1,541 @@
+This test ports some arbitrary tests from the old marker framework, that were
+*mostly* about completion.
+
+-- flags --
+-ignore_extra_diags
+-min_go=go1.20
+
+-- settings.json --
+{
+ "completeUnimported": false,
+ "deepCompletion": false,
+ "experimentalPostfixCompletions": false
+}
+
+-- go.mod --
+module foobar.test
+
+go 1.18
+
+-- foo/foo.go --
+package foo //@loc(PackageFoo, "foo"),item(PackageFooItem, "foo", "\"foobar.test/foo\"", "package")
+
+type StructFoo struct { //@loc(StructFooLoc, "StructFoo"), item(StructFoo, "StructFoo", "struct{...}", "struct")
+ Value int //@item(Value, "Value", "int", "field")
+}
+
+// Pre-set this marker, as we don't have a "source" for it in this package.
+/* Error() */ //@item(Error, "Error", "func() string", "method")
+
+func Foo() { //@item(Foo, "Foo", "func()", "func")
+ var err error
+ err.Error() //@complete("E", Error)
+}
+
+func _() {
+ var sFoo StructFoo //@complete("t", StructFoo)
+ if x := sFoo; x.Value == 1 { //@complete("V", Value), typedef("sFoo", StructFooLoc)
+ return
+ }
+}
+
+func _() {
+ shadowed := 123
+ {
+ shadowed := "hi" //@item(shadowed, "shadowed", "string", "var")
+ sha //@complete("a", shadowed), diag("sha", re"(undefined|undeclared)")
+ _ = shadowed
+ }
+}
+
+type IntFoo int //@loc(IntFooLoc, "IntFoo"), item(IntFoo, "IntFoo", "int", "type")
+
+-- bar/bar.go --
+package bar
+
+import (
+ "foobar.test/foo" //@item(foo, "foo", "\"foobar.test/foo\"", "package")
+)
+
+func helper(i foo.IntFoo) {} //@item(helper, "helper", "func(i foo.IntFoo)", "func")
+
+func _() {
+ help //@complete("l", helper)
+ _ = foo.StructFoo{} //@complete("S", IntFoo, StructFoo)
+}
+
+// Bar is a function.
+func Bar() { //@item(Bar, "Bar", "func()", "func", "Bar is a function.")
+ foo.Foo() //@complete("F", Foo, IntFoo, StructFoo)
+ var _ foo.IntFoo //@complete("I", IntFoo, StructFoo)
+ foo.() //@complete("(", Foo, IntFoo, StructFoo), diag(")", re"expected type")
+}
+
+// These items weren't present in the old marker tests (due to settings), but
+// we may as well include them.
+//@item(intConversion, "int()"), item(fooFoo, "foo.Foo")
+//@item(fooIntFoo, "foo.IntFoo"), item(fooStructFoo, "foo.StructFoo")
+
+func _() {
+ var Valentine int //@item(Valentine, "Valentine", "int", "var")
+
+ _ = foo.StructFoo{ //@diag("foo", re"unkeyed fields")
+ Valu //@complete(" //", Value)
+ }
+ _ = foo.StructFoo{ //@diag("foo", re"unkeyed fields")
+ Va //@complete("a", Value, Valentine)
+
+ }
+ _ = foo.StructFoo{
+ Value: 5, //@complete("a", Value)
+ }
+ _ = foo.StructFoo{
+ //@complete("//", Value, Valentine, intConversion, foo, helper, Bar)
+ }
+ _ = foo.StructFoo{
+ Value: Valen //@complete("le", Valentine)
+ }
+ _ = foo.StructFoo{
+ Value: //@complete(" //", Valentine, intConversion, foo, helper, Bar)
+ }
+ _ = foo.StructFoo{
+ Value: //@complete(" ", Valentine, intConversion, foo, helper, Bar)
+ }
+}
+
+-- baz/baz.go --
+package baz
+
+import (
+ "foobar.test/bar"
+
+ f "foobar.test/foo"
+)
+
+var FooStruct f.StructFoo
+
+func Baz() {
+ defer bar.Bar() //@complete("B", Bar)
+ // TODO: Test completion here.
+ defer bar.B //@diag(re"bar.B()", re"must be function call")
+ var x f.IntFoo //@complete("n", IntFoo), typedef("x", IntFooLoc)
+ bar.Bar() //@complete("B", Bar)
+}
+
+func _() {
+ bob := f.StructFoo{Value: 5}
+ if x := bob. //@complete(" //", Value)
+ switch true == false {
+ case true:
+ if x := bob. //@complete(" //", Value)
+ case false:
+ }
+ if x := bob.Va //@complete("a", Value)
+ switch true == true {
+ default:
+ }
+}
+
+-- arraytype/arraytype.go --
+package arraytype
+
+import (
+ "foobar.test/foo"
+)
+
+func _() {
+ var (
+ val string //@item(atVal, "val", "string", "var")
+ )
+
+ [] //@complete(" //", atVal, PackageFooItem)
+
+ []val //@complete(" //")
+
+ []foo.StructFoo //@complete(" //", StructFoo)
+
+ []foo.StructFoo(nil) //@complete("(", StructFoo)
+
+ []*foo.StructFoo //@complete(" //", StructFoo)
+
+ [...]foo.StructFoo //@complete(" //", StructFoo)
+
+ [2][][4]foo.StructFoo //@complete(" //", StructFoo)
+
+ []struct { f []foo.StructFoo } //@complete(" }", StructFoo)
+}
+
+func _() {
+ type myInt int //@item(atMyInt, "myInt", "int", "type")
+
+ var mark []myInt //@item(atMark, "mark", "[]myInt", "var")
+
+ var s []myInt //@item(atS, "s", "[]myInt", "var")
+ s = []m //@complete(" //", atMyInt)
+
+ var a [1]myInt
+ a = [1]m //@complete(" //", atMyInt)
+
+ var ds [][]myInt
+ ds = [][]m //@complete(" //", atMyInt)
+}
+
+func _() {
+ var b [0]byte //@item(atByte, "b", "[0]byte", "var")
+ var _ []byte = b //@snippet(" //", atByte, "b[:]")
+}
+
+-- badstmt/badstmt.go --
+package badstmt
+
+import (
+ "foobar.test/foo"
+)
+
+// (The syntax error causes suppression of diagnostics for type errors.
+// See issue #59888.)
+
+func _(x int) {
+ defer foo.F //@complete(" //", Foo, IntFoo, StructFoo)
+ defer foo.F //@complete(" //", Foo, IntFoo, StructFoo)
+}
+
+func _() {
+ switch true {
+ case true:
+ go foo.F //@complete(" //", Foo, IntFoo, StructFoo)
+ }
+}
+
+func _() {
+ defer func() {
+ foo.F //@complete(" //", Foo, IntFoo, StructFoo), snippet(" //", Foo, "Foo()")
+
+ foo. //@rank(" //", Foo)
+ }
+}
+
+-- badstmt/badstmt_2.go --
+package badstmt
+
+import (
+ "foobar.test/foo"
+)
+
+func _() {
+ defer func() { foo. } //@rank(" }", Foo)
+}
+
+-- badstmt/badstmt_3.go --
+package badstmt
+
+import (
+ "foobar.test/foo"
+)
+
+func _() {
+ go foo. //@rank(" //", Foo, IntFoo), snippet(" //", Foo, "Foo()")
+}
+
+-- badstmt/badstmt_4.go --
+package badstmt
+
+import (
+ "foobar.test/foo"
+)
+
+func _() {
+ go func() {
+ defer foo. //@rank(" //", Foo, IntFoo)
+ }
+}
+
+-- selector/selector.go --
+package selector
+
+import (
+ "foobar.test/bar"
+)
+
+type S struct {
+ B, A, C int //@item(Bf, "B", "int", "field"),item(Af, "A", "int", "field"),item(Cf, "C", "int", "field")
+}
+
+func _() {
+ _ = S{}.; //@complete(";", Af, Bf, Cf)
+}
+
+type bob struct { a int } //@item(a, "a", "int", "field")
+type george struct { b int }
+type jack struct { c int } //@item(c, "c", "int", "field")
+type jill struct { d int }
+
+func (b *bob) george() *george {} //@item(george, "george", "func() *george", "method")
+func (g *george) jack() *jack {}
+func (j *jack) jill() *jill {} //@item(jill, "jill", "func() *jill", "method")
+
+func _() {
+ b := &bob{}
+ y := b.george().
+ jack();
+ y.; //@complete(";", c, jill)
+}
+
+func _() {
+ bar. //@complete(" /", Bar)
+ x := 5
+
+ var b *bob
+ b. //@complete(" /", a, george)
+ y, z := 5, 6
+
+ b. //@complete(" /", a, george)
+ y, z, a, b, c := 5, 6
+}
+
+func _() {
+ bar. //@complete(" /", Bar)
+ bar.Bar()
+
+ bar. //@complete(" /", Bar)
+ go f()
+}
+
+func _() {
+ var b *bob
+ if y != b. //@complete(" /", a, george)
+ z := 5
+
+ if z + y + 1 + b. //@complete(" /", a, george)
+ r, s, t := 4, 5
+
+ if y != b. //@complete(" /", a, george)
+ z = 5
+
+ if z + y + 1 + b. //@complete(" /", a, george)
+ r = 4
+}
+
+-- literal_snippets/literal_snippets.go --
+package literal_snippets
+
+import (
+ "bytes"
+ "context"
+ "go/ast"
+ "net/http"
+ "sort"
+
+ "golang.org/lsptests/foo"
+)
+
+func _() {
+ []int{} //@item(litIntSlice, "[]int{}", "", "var")
+ &[]int{} //@item(litIntSliceAddr, "&[]int{}", "", "var")
+ make([]int, 0) //@item(makeIntSlice, "make([]int, 0)", "", "func")
+
+ var _ *[]int = in //@snippet(" //", litIntSliceAddr, "&[]int{$0\\}")
+ var _ **[]int = in //@complete(" //")
+
+ var slice []int
+ slice = i //@snippet(" //", litIntSlice, "[]int{$0\\}")
+ slice = m //@snippet(" //", makeIntSlice, "make([]int, ${1:})")
+}
+
+func _() {
+ type namedInt []int
+
+ namedInt{} //@item(litNamedSlice, "namedInt{}", "", "var")
+ make(namedInt, 0) //@item(makeNamedSlice, "make(namedInt, 0)", "", "func")
+
+ var namedSlice namedInt
+ namedSlice = n //@snippet(" //", litNamedSlice, "namedInt{$0\\}")
+ namedSlice = m //@snippet(" //", makeNamedSlice, "make(namedInt, ${1:})")
+}
+
+func _() {
+ make(chan int) //@item(makeChan, "make(chan int)", "", "func")
+
+ var ch chan int
+ ch = m //@snippet(" //", makeChan, "make(chan int)")
+}
+
+func _() {
+ map[string]struct{}{} //@item(litMap, "map[string]struct{}{}", "", "var")
+ make(map[string]struct{}) //@item(makeMap, "make(map[string]struct{})", "", "func")
+
+ var m map[string]struct{}
+ m = m //@snippet(" //", litMap, "map[string]struct{\\}{$0\\}")
+ m = m //@snippet(" //", makeMap, "make(map[string]struct{\\})")
+
+ struct{}{} //@item(litEmptyStruct, "struct{}{}", "", "var")
+
+ m["hi"] = s //@snippet(" //", litEmptyStruct, "struct{\\}{\\}")
+}
+
+func _() {
+ type myStruct struct{ i int } //@item(myStructType, "myStruct", "struct{...}", "struct")
+
+ myStruct{} //@item(litStruct, "myStruct{}", "", "var")
+ &myStruct{} //@item(litStructPtr, "&myStruct{}", "", "var")
+
+ var ms myStruct
+ ms = m //@snippet(" //", litStruct, "myStruct{$0\\}")
+
+ var msPtr *myStruct
+ msPtr = m //@snippet(" //", litStructPtr, "&myStruct{$0\\}")
+
+ msPtr = &m //@snippet(" //", litStruct, "myStruct{$0\\}")
+
+ type myStructCopy struct { i int } //@item(myStructCopyType, "myStructCopy", "struct{...}", "struct")
+
+ // Don't offer literal completion for convertible structs.
+ ms = myStruct //@complete(" //", litStruct, myStructType, myStructCopyType)
+}
+
+type myImpl struct{}
+
+func (myImpl) foo() {}
+
+func (*myImpl) bar() {}
+
+type myBasicImpl string
+
+func (myBasicImpl) foo() {}
+
+func _() {
+ type myIntf interface {
+ foo()
+ }
+
+ myImpl{} //@item(litImpl, "myImpl{}", "", "var")
+
+ var mi myIntf
+ mi = m //@snippet(" //", litImpl, "myImpl{\\}")
+
+ myBasicImpl() //@item(litBasicImpl, "myBasicImpl()", "string", "var")
+
+ mi = m //@snippet(" //", litBasicImpl, "myBasicImpl($0)")
+
+ // only satisfied by pointer to myImpl
+ type myPtrIntf interface {
+ bar()
+ }
+
+ &myImpl{} //@item(litImplPtr, "&myImpl{}", "", "var")
+
+ var mpi myPtrIntf
+ mpi = m //@snippet(" //", litImplPtr, "&myImpl{\\}")
+}
+
+func _() {
+ var s struct{ i []int } //@item(litSliceField, "i", "[]int", "field")
+ var foo []int
+ // no literal completions after selector
+ foo = s.i //@complete(" //", litSliceField)
+}
+
+func _() {
+ type myStruct struct{ i int } //@item(litMyStructType, "myStruct", "struct{...}", "struct")
+ myStruct{} //@item(litMyStruct, "myStruct{}", "", "var")
+
+ foo := func(s string, args ...myStruct) {}
+ // Don't give literal slice candidate for variadic arg.
+ // Do give literal candidates for variadic element.
+ foo("", myStruct) //@complete(")", litMyStruct, litMyStructType)
+}
+
+func _() {
+ Buffer{} //@item(litBuffer, "Buffer{}", "", "var")
+
+ var b *bytes.Buffer
+ b = bytes.Bu //@snippet(" //", litBuffer, "Buffer{\\}")
+}
+
+func _() {
+ _ = "func(...) {}" //@item(litFunc, "func(...) {}", "", "var")
+
+ // no literal "func" completions
+ http.Handle("", fun) //@complete(")")
+
+ var namedReturn func(s string) (b bool)
+ namedReturn = f //@snippet(" //", litFunc, "func(s string) (b bool) {$0\\}")
+
+ var multiReturn func() (bool, int)
+ multiReturn = f //@snippet(" //", litFunc, "func() (bool, int) {$0\\}")
+
+ var multiNamedReturn func() (b bool, i int)
+ multiNamedReturn = f //@snippet(" //", litFunc, "func() (b bool, i int) {$0\\}")
+
+ var duplicateParams func(myImpl, int, myImpl)
+ duplicateParams = f //@snippet(" //", litFunc, "func(mi1 myImpl, i int, mi2 myImpl) {$0\\}")
+
+ type aliasImpl = myImpl
+ var aliasParams func(aliasImpl) aliasImpl
+ aliasParams = f //@snippet(" //", litFunc, "func(ai aliasImpl) aliasImpl {$0\\}")
+
+ const two = 2
+ var builtinTypes func([]int, [two]bool, map[string]string, struct{ i int }, interface{ foo() }, <-chan int)
+ builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [two]bool, m map[string]string, s struct{ i int \\}, i2 interface{ foo() \\}, c <-chan int) {$0\\}")
+
+ var _ func(ast.Node) = f //@snippet(" //", litFunc, "func(n ast.Node) {$0\\}")
+ var _ func(error) = f //@snippet(" //", litFunc, "func(err error) {$0\\}")
+ var _ func(context.Context) = f //@snippet(" //", litFunc, "func(ctx context.Context) {$0\\}")
+
+ type context struct {}
+ var _ func(context) = f //@snippet(" //", litFunc, "func(ctx context) {$0\\}")
+}
+
+func _() {
+ float64() //@item(litFloat64, "float64()", "float64", "var")
+
+ // don't complete to "&float64()"
+ var _ *float64 = float64 //@complete(" //")
+
+ var f float64
+ f = fl //@complete(" //", litFloat64),snippet(" //", litFloat64, "float64($0)")
+
+ type myInt int
+ myInt() //@item(litMyInt, "myInt()", "", "var")
+
+ var mi myInt
+ mi = my //@snippet(" //", litMyInt, "myInt($0)")
+}
+
+func _() {
+ type ptrStruct struct {
+ p *ptrStruct
+ }
+
+ ptrStruct{} //@item(litPtrStruct, "ptrStruct{}", "", "var")
+
+ ptrStruct{
+ p: &ptrSt, //@rank(",", litPtrStruct)
+ }
+
+ &ptrStruct{} //@item(litPtrStructPtr, "&ptrStruct{}", "", "var")
+
+ &ptrStruct{
+ p: ptrSt, //@rank(",", litPtrStructPtr)
+ }
+}
+
+func _() {
+ f := func(...[]int) {}
+ f() //@snippet(")", litIntSlice, "[]int{$0\\}")
+}
+
+
+func _() {
+ // don't complete to "untyped int()"
+ []int{}[untyped] //@complete("] //")
+}
+
+type Tree[T any] struct{}
+
+func (tree Tree[T]) Do(f func(s T)) {}
+
+func _() {
+ var t Tree[string]
+ t.Do(fun) //@complete(")", litFunc), snippet(")", litFunc, "func(s string) {$0\\}")
+}
diff --git a/gopls/internal/regtest/marker/testdata/completion/issue59096.txt b/gopls/internal/regtest/marker/testdata/completion/issue59096.txt
index 44bb10ae91d..23d82c4dc9c 100644
--- a/gopls/internal/regtest/marker/testdata/completion/issue59096.txt
+++ b/gopls/internal/regtest/marker/testdata/completion/issue59096.txt
@@ -9,9 +9,11 @@ module example.com
package a
func _() {
- b.(foo) //@complete(re"b.()", "B"), diag("b", re"(undefined|undeclared name): b")
+ b.(foo) //@complete(re"b.()", B), diag("b", re"(undefined|undeclared name): b")
}
+//@item(B, "B", "const (from \"example.com/b\")", "const")
+
-- b/b.go --
package b
diff --git a/gopls/internal/regtest/marker/testdata/completion/issue60545.txt b/gopls/internal/regtest/marker/testdata/completion/issue60545.txt
index 67221a67563..4d204979b6a 100644
--- a/gopls/internal/regtest/marker/testdata/completion/issue60545.txt
+++ b/gopls/internal/regtest/marker/testdata/completion/issue60545.txt
@@ -8,8 +8,12 @@ go 1.18
-- main.go --
package main
+//@item(Print, "Print", "func (from \"fmt\")", "func")
+//@item(Printf, "Printf", "func (from \"fmt\")", "func")
+//@item(Println, "Println", "func (from \"fmt\")", "func")
+
func main() {
- fmt.p //@complete(re"p()","Print", "Printf", "Println"), diag("fmt", re"(undefined|undeclared)")
+ fmt.p //@complete(re"fmt.p()", Print, Printf, Println), diag("fmt", re"(undefined|undeclared)")
}
-- other.go --
diff --git a/gopls/internal/regtest/marker/testdata/completion/issue62560.txt b/gopls/internal/regtest/marker/testdata/completion/issue62560.txt
new file mode 100644
index 00000000000..89763fe0221
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/completion/issue62560.txt
@@ -0,0 +1,19 @@
+This test verifies that completion of package members in unimported packages
+reflects their fuzzy score, even when those members are present in the
+transitive import graph of the main module. (For technical reasons, this was
+the nature of the regression in golang/go#62560.)
+
+-- go.mod --
+module mod.test
+
+-- foo/foo.go --
+package foo
+
+func _() {
+ json.U //@rankl(re"U()", "Unmarshal", "InvalidUTF8Error"), diag("json", re"(undefined|undeclared)")
+}
+
+-- bar/bar.go --
+package bar
+
+import _ "encoding/json"
diff --git a/gopls/internal/regtest/marker/testdata/completion/issue62676.txt b/gopls/internal/regtest/marker/testdata/completion/issue62676.txt
new file mode 100644
index 00000000000..af4c3b695ec
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/completion/issue62676.txt
@@ -0,0 +1,63 @@
+This test verifies that unimported completion respects the usePlaceholders setting.
+
+-- flags --
+-ignore_extra_diags
+
+-- settings.json --
+{
+ "usePlaceholders": false
+}
+
+-- go.mod --
+module mod.test
+
+go 1.21
+
+-- foo/foo.go --
+package foo
+
+func _() {
+ // This uses goimports-based completion; TODO: this should insert snippets.
+ os.Open //@acceptcompletion(re"Open()", "Open", open)
+}
+
+func _() {
+ // This uses metadata-based completion.
+ errors.New //@acceptcompletion(re"New()", "New", new)
+}
+
+-- bar/bar.go --
+package bar
+
+import _ "errors" // important: doesn't transitively import os.
+
+-- @new/foo/foo.go --
+package foo
+
+import "errors"
+
+func _() {
+ // This uses goimports-based completion; TODO: this should insert snippets.
+ os.Open //@acceptcompletion(re"Open()", "Open", open)
+}
+
+func _() {
+ // This uses metadata-based completion.
+ errors.New(${1:}) //@acceptcompletion(re"New()", "New", new)
+}
+
+-- @open/foo/foo.go --
+package foo
+
+import "os"
+
+func _() {
+ // This uses goimports-based completion; TODO: this should insert snippets.
+ os.Open //@acceptcompletion(re"Open()", "Open", open)
+}
+
+func _() {
+ // This uses metadata-based completion.
+ errors.New //@acceptcompletion(re"New()", "New", new)
+}
+
diff --git a/gopls/internal/regtest/marker/testdata/completion/lit.txt b/gopls/internal/regtest/marker/testdata/completion/lit.txt
new file mode 100644
index 00000000000..7224f42ab77
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/completion/lit.txt
@@ -0,0 +1,49 @@
+
+-- flags --
+-ignore_extra_diags
+
+-- go.mod --
+module mod.test
+
+go 1.18
+
+-- foo/foo.go --
+package foo
+
+type StructFoo struct{ F int }
+
+-- a.go --
+package a
+
+import "mod.test/foo"
+
+func _() {
+ StructFoo{} //@item(litStructFoo, "StructFoo{}", "struct{...}", "struct")
+
+ var sfp *foo.StructFoo
+ // Don't insert the "&" before "StructFoo{}".
+ sfp = foo.Str //@snippet(" //", litStructFoo, "StructFoo{$0\\}")
+
+ var sf foo.StructFoo
+ sf = foo.Str //@snippet(" //", litStructFoo, "StructFoo{$0\\}")
+ sf = foo. //@snippet(" //", litStructFoo, "StructFoo{$0\\}")
+}
+
+-- http.go --
+package a
+
+import (
+ "net/http"
+ "sort"
+)
+
+func _() {
+ sort.Slice(nil, fun) //@snippet(")", litFunc, "func(i, j int) bool {$0\\}")
+
+ http.HandleFunc("", f) //@snippet(")", litFunc, "func(w http.ResponseWriter, r *http.Request) {$0\\}")
+
+ //@item(litFunc, "func(...) {}", "", "var")
+ http.HandlerFunc() //@item(handlerFunc, "http.HandlerFunc()", "", "var")
+ http.Handle("", http.HandlerFunc()) //@snippet("))", litFunc, "func(w http.ResponseWriter, r *http.Request) {$0\\}")
+ http.Handle("", h) //@snippet(")", handlerFunc, "http.HandlerFunc($0)")
+}
diff --git a/gopls/internal/regtest/marker/testdata/completion/testy.txt b/gopls/internal/regtest/marker/testdata/completion/testy.txt
new file mode 100644
index 00000000000..f26b3ae1b1f
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/completion/testy.txt
@@ -0,0 +1,57 @@
+
+-- flags --
+-ignore_extra_diags
+
+-- go.mod --
+module testy.test
+
+go 1.18
+
+-- types/types.go --
+package types
+
+
+-- signature/signature.go --
+package signature
+
+type Alias = int
+
+-- snippets/snippets.go --
+package snippets
+
+import (
+ "testy.test/signature"
+ t "testy.test/types"
+)
+
+func X(_ map[signature.Alias]t.CoolAlias) (map[signature.Alias]t.CoolAlias) {
+ return nil
+}
+
+-- testy/testy.go --
+package testy
+
+func a() { //@item(funcA, "a", "func()", "func")
+ //@complete("", funcA)
+}
+
+
+-- testy/testy_test.go --
+package testy
+
+import (
+ "testing"
+
+ sig "testy.test/signature"
+ "testy.test/snippets"
+)
+
+func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *testing.T)", "", "func")
+ var x int //@loc(testyX, "x"), diag("x", re"x declared (and|but) not used")
+ a() //@loc(testyA, "a")
+}
+
+func _() {
+ _ = snippets.X(nil) //@signature("nil", "X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias")
+ var _ sig.Alias
+}
diff --git a/gopls/internal/regtest/marker/testdata/completion/unimported.txt b/gopls/internal/regtest/marker/testdata/completion/unimported.txt
new file mode 100644
index 00000000000..7d12269c8ba
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/completion/unimported.txt
@@ -0,0 +1,88 @@
+
+-- flags --
+-ignore_extra_diags
+
+-- go.mod --
+module unimported.test
+
+go 1.18
+
+-- unimported/export_test.go --
+package unimported
+
+var TestExport int //@item(testexport, "TestExport", "var (from \"unimported.test/unimported\")", "var")
+
+-- signature/signature.go --
+package signature
+
+func Foo() {}
+
+-- foo/foo.go --
+package foo
+
+type StructFoo struct{ F int }
+
+-- baz/baz.go --
+package baz
+
+import (
+ f "unimported.test/foo"
+)
+
+var FooStruct f.StructFoo
+
+-- unimported/unimported.go --
+package unimported
+
+func _() {
+ http //@complete("p", http, httptest, httptrace, httputil)
+ // container/ring is extremely unlikely to be imported by anything, so shouldn't have type information.
+ ring.Ring //@complete(re"R()ing", ringring)
+ signature.Foo //@complete("Foo", signaturefoo)
+
+ context.Bac //@complete(" //", contextBackground)
+}
+
+// Create markers for unimported std lib packages. Only for use by this test.
+/* http */ //@item(http, "http", "\"net/http\"", "package")
+/* httptest */ //@item(httptest, "httptest", "\"net/http/httptest\"", "package")
+/* httptrace */ //@item(httptrace, "httptrace", "\"net/http/httptrace\"", "package")
+/* httputil */ //@item(httputil, "httputil", "\"net/http/httputil\"", "package")
+
+/* ring.Ring */ //@item(ringring, "Ring", "(from \"container/ring\")", "var")
+
+/* signature.Foo */ //@item(signaturefoo, "Foo", "func (from \"unimported.test/signature\")", "func")
+
+/* context.Background */ //@item(contextBackground, "Background", "func (from \"context\")", "func")
+
+// Now that we no longer type-check imported completions,
+// we don't expect the context.Background().Err method (see golang/go#58663).
+/* context.Background().Err */ //@item(contextBackgroundErr, "Background().Err", "func (from \"context\")", "method")
+
+-- unimported/unimported_cand_type.go --
+package unimported
+
+import (
+ _ "context"
+
+ "unimported.test/baz"
+)
+
+func _() {
+ foo.StructFoo{} //@item(litFooStructFoo, "foo.StructFoo{}", "struct{...}", "struct")
+
+ // We get the literal completion for "foo.StructFoo{}" even though we haven't
+ // imported "foo" yet.
+ baz.FooStruct = f //@snippet(" //", litFooStructFoo, "foo.StructFoo{$0\\}")
+}
+
+-- unimported/x_test.go --
+package unimported_test
+
+import (
+ "testing"
+)
+
+func TestSomething(t *testing.T) {
+ _ = unimported.TestExport //@complete("TestExport", testexport)
+}
diff --git a/gopls/internal/regtest/marker/testdata/definition/cgo.txt b/gopls/internal/regtest/marker/testdata/definition/cgo.txt
new file mode 100644
index 00000000000..6d108a46656
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/definition/cgo.txt
@@ -0,0 +1,62 @@
+This test is ported from the old marker tests.
+It tests hover and definition for cgo declarations.
+
+-- flags --
+-cgo
+
+-- go.mod --
+module cgo.test
+
+go 1.18
+
+-- cgo/cgo.go --
+package cgo
+
+/*
+#include
+#include
+
+void myprint(char* s) {
+ printf("%s\n", s);
+}
+*/
+import "C"
+
+import (
+ "fmt"
+ "unsafe"
+)
+
+func Example() { //@loc(cgoexample, "Example"), item(cgoexampleItem, "Example", "func()", "func")
+ fmt.Println()
+ cs := C.CString("Hello from stdio\n")
+ C.myprint(cs)
+ C.free(unsafe.Pointer(cs))
+}
+
+func _() {
+ Example() //@hover("ample", "Example", hoverExample), def("ample", cgoexample), complete("ample", cgoexampleItem)
+}
+
+-- @hoverExample/hover.md --
+```go
+func Example()
+```
+
+[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/cgo.test/cgo#Example)
+-- usecgo/usecgo.go --
+package cgoimport
+
+import (
+ "cgo.test/cgo"
+)
+
+func _() {
+ cgo.Example() //@hover("ample", "Example", hoverImportedExample), def("ample", cgoexample), complete("ample", cgoexampleItem)
+}
+-- @hoverImportedExample/hover.md --
+```go
+func cgo.Example()
+```
+
+[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/cgo.test/cgo#Example)
diff --git a/gopls/internal/regtest/marker/testdata/diagnostics/useinternal.txt b/gopls/internal/regtest/marker/testdata/diagnostics/useinternal.txt
new file mode 100644
index 00000000000..11a3cc9a0c0
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/diagnostics/useinternal.txt
@@ -0,0 +1,21 @@
+This test checks a diagnostic for invalid use of internal packages.
+
+This list error changed in Go 1.21.
+
+-- flags --
+-min_go=go1.21
+
+-- go.mod --
+module bad.test
+
+go 1.18
+
+-- assign/internal/secret/secret.go --
+package secret
+
+func Hello() {}
+
+-- bad/bad.go --
+package bad
+
+import _ "bad.test/assign/internal/secret" //@diag("\"bad.test/assign/internal/secret\"", re"could not import bad.test/assign/internal/secret \\(invalid use of internal package \"bad.test/assign/internal/secret\"\\)"),diag("_", re"use of internal package bad.test/assign/internal/secret not allowed")
diff --git a/gopls/internal/regtest/marker/testdata/foldingrange/a.txt b/gopls/internal/regtest/marker/testdata/foldingrange/a.txt
new file mode 100644
index 00000000000..6210fc25251
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/foldingrange/a.txt
@@ -0,0 +1,154 @@
+This test checks basic behavior of textDocument/foldingRange.
+
+-- a.go --
+package folding //@foldingrange(raw)
+
+import (
+ "fmt"
+ _ "log"
+)
+
+import _ "os"
+
+// bar is a function.
+// With a multiline doc comment.
+func bar() string {
+ /* This is a single line comment */
+ switch {
+ case true:
+ if true {
+ fmt.Println("true")
+ } else {
+ fmt.Println("false")
+ }
+ case false:
+ fmt.Println("false")
+ default:
+ fmt.Println("default")
+ }
+ /* This is a multiline
+ block
+ comment */
+
+ /* This is a multiline
+ block
+ comment */
+ // Followed by another comment.
+ _ = []int{
+ 1,
+ 2,
+ 3,
+ }
+ _ = [2]string{"d",
+ "e",
+ }
+ _ = map[string]int{
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ }
+ type T struct {
+ f string
+ g int
+ h string
+ }
+ _ = T{
+ f: "j",
+ g: 4,
+ h: "i",
+ }
+ x, y := make(chan bool), make(chan bool)
+ select {
+ case val := <-x:
+ if val {
+ fmt.Println("true from x")
+ } else {
+ fmt.Println("false from x")
+ }
+ case <-y:
+ fmt.Println("y")
+ default:
+ fmt.Println("default")
+ }
+ // This is a multiline comment
+ // that is not a doc comment.
+ return `
+this string
+is not indented`
+}
+-- @raw --
+package folding //@foldingrange(raw)
+
+import (<0 kind="imports">
+ "fmt"
+ _ "log"
+0>)
+
+import _ "os"
+
+// bar is a function.<1 kind="comment">
+// With a multiline doc comment.1>
+func bar(<2 kind="">2>) string {<3 kind="">
+ /* This is a single line comment */
+ switch {<4 kind="">
+ case true:<5 kind="">
+ if true {<6 kind="">
+ fmt.Println(<7 kind="">"true"7>)
+ 6>} else {<8 kind="">
+ fmt.Println(<9 kind="">"false"9>)
+ 8>}5>
+ case false:<10 kind="">
+ fmt.Println(<11 kind="">"false"11>)10>
+ default:<12 kind="">
+ fmt.Println(<13 kind="">"default"13>)12>
+ 4>}
+ /* This is a multiline<14 kind="comment">
+ block
+ comment */14>
+
+ /* This is a multiline<15 kind="comment">
+ block
+ comment */
+ // Followed by another comment.15>
+ _ = []int{<16 kind="">
+ 1,
+ 2,
+ 3,
+ 16>}
+ _ = [2]string{<17 kind="">"d",
+ "e",
+ 17>}
+ _ = map[string]int{<18 kind="">
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ 18>}
+ type T struct {<19 kind="">
+ f string
+ g int
+ h string
+ 19>}
+ _ = T{<20 kind="">
+ f: "j",
+ g: 4,
+ h: "i",
+ 20>}
+ x, y := make(<21 kind="">chan bool21>), make(<22 kind="">chan bool22>)
+ select {<23 kind="">
+ case val := <-x:<24 kind="">
+ if val {<25 kind="">
+ fmt.Println(<26 kind="">"true from x"26>)
+ 25>} else {<27 kind="">
+ fmt.Println(<28 kind="">"false from x"28>)
+ 27>}24>
+ case <-y:<29 kind="">
+ fmt.Println(<30 kind="">"y"30>)29>
+ default:<31 kind="">
+ fmt.Println(<32 kind="">"default"32>)31>
+ 23>}
+ // This is a multiline comment<33 kind="comment">
+ // that is not a doc comment.33>
+ return <34 kind="">`
+this string
+is not indented`34>
+3>}
diff --git a/gopls/internal/regtest/marker/testdata/foldingrange/a_lineonly.txt b/gopls/internal/regtest/marker/testdata/foldingrange/a_lineonly.txt
new file mode 100644
index 00000000000..0c532e760f1
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/foldingrange/a_lineonly.txt
@@ -0,0 +1,163 @@
+This test checks basic behavior of the textDocument/foldingRange, when the
+editor only supports line folding.
+
+-- capabilities.json --
+{
+ "textDocument": {
+ "foldingRange": {
+ "lineFoldingOnly": true
+ }
+ }
+}
+-- a.go --
+package folding //@foldingrange(raw)
+
+import (
+ "fmt"
+ _ "log"
+)
+
+import _ "os"
+
+// bar is a function.
+// With a multiline doc comment.
+func bar() string {
+ /* This is a single line comment */
+ switch {
+ case true:
+ if true {
+ fmt.Println("true")
+ } else {
+ fmt.Println("false")
+ }
+ case false:
+ fmt.Println("false")
+ default:
+ fmt.Println("default")
+ }
+ /* This is a multiline
+ block
+ comment */
+
+ /* This is a multiline
+ block
+ comment */
+ // Followed by another comment.
+ _ = []int{
+ 1,
+ 2,
+ 3,
+ }
+ _ = [2]string{"d",
+ "e",
+ }
+ _ = map[string]int{
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ }
+ type T struct {
+ f string
+ g int
+ h string
+ }
+ _ = T{
+ f: "j",
+ g: 4,
+ h: "i",
+ }
+ x, y := make(chan bool), make(chan bool)
+ select {
+ case val := <-x:
+ if val {
+ fmt.Println("true from x")
+ } else {
+ fmt.Println("false from x")
+ }
+ case <-y:
+ fmt.Println("y")
+ default:
+ fmt.Println("default")
+ }
+ // This is a multiline comment
+ // that is not a doc comment.
+ return `
+this string
+is not indented`
+}
+-- @raw --
+package folding //@foldingrange(raw)
+
+import (<0 kind="imports">
+ "fmt"
+ _ "log"0>
+)
+
+import _ "os"
+
+// bar is a function.<1 kind="comment">
+// With a multiline doc comment.1>
+func bar() string {<2 kind="">
+ /* This is a single line comment */
+ switch {<3 kind="">
+ case true:<4 kind="">
+ if true {<5 kind="">
+ fmt.Println("true")5>
+ } else {<6 kind="">
+ fmt.Println("false")6>
+ }4>
+ case false:<7 kind="">
+ fmt.Println("false")7>
+ default:<8 kind="">
+ fmt.Println("default")3>8>
+ }
+ /* This is a multiline<9 kind="comment">
+ block
+ comment */9>
+
+ /* This is a multiline<10 kind="comment">
+ block
+ comment */
+ // Followed by another comment.10>
+ _ = []int{<11 kind="">
+ 1,
+ 2,
+ 311>,
+ }
+ _ = [2]string{"d",
+ "e",
+ }
+ _ = map[string]int{<12 kind="">
+ "a": 1,
+ "b": 2,
+ "c": 312>,
+ }
+ type T struct {<13 kind="">
+ f string
+ g int
+ h string13>
+ }
+ _ = T{<14 kind="">
+ f: "j",
+ g: 4,
+ h: "i"14>,
+ }
+ x, y := make(chan bool), make(chan bool)
+ select {<15 kind="">
+ case val := <-x:<16 kind="">
+ if val {<17 kind="">
+ fmt.Println("true from x")17>
+ } else {<18 kind="">
+ fmt.Println("false from x")18>
+ }16>
+ case <-y:<19 kind="">
+ fmt.Println("y")19>
+ default:<20 kind="">
+ fmt.Println("default")15>20>
+ }
+ // This is a multiline comment<21 kind="comment">
+ // that is not a doc comment.21>
+ return <22 kind="">`
+this string
+is not indented`2>22>
+}
diff --git a/gopls/internal/regtest/marker/testdata/foldingrange/bad.txt b/gopls/internal/regtest/marker/testdata/foldingrange/bad.txt
new file mode 100644
index 00000000000..f9f14a4fa7d
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/foldingrange/bad.txt
@@ -0,0 +1,41 @@
+This test verifies behavior of textDocument/foldingRange in the presence of
+unformatted syntax.
+
+-- a.go --
+package folding //@foldingrange(raw)
+
+import ( "fmt"
+ _ "log"
+)
+
+import (
+ _ "os" )
+
+// badBar is a function.
+func badBar() string { x := true
+ if x {
+ // This is the only foldable thing in this file when lineFoldingOnly
+ fmt.Println("true")
+ } else {
+ fmt.Println("false") }
+ return ""
+}
+-- @raw --
+package folding //@foldingrange(raw)
+
+import (<0 kind="imports"> "fmt"
+ _ "log"
+0>)
+
+import (<1 kind="imports">
+ _ "os" 1>)
+
+// badBar is a function.
+func badBar(<2 kind="">2>) string {<3 kind=""> x := true
+ if x {<4 kind="">
+ // This is the only foldable thing in this file when lineFoldingOnly
+ fmt.Println(<5 kind="">"true"5>)
+ 4>} else {<6 kind="">
+ fmt.Println(<7 kind="">"false"7>) 6>}
+ return ""
+3>}
diff --git a/gopls/internal/regtest/marker/testdata/highlight/highlight.txt b/gopls/internal/regtest/marker/testdata/highlight/highlight.txt
new file mode 100644
index 00000000000..10b30259b10
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/highlight/highlight.txt
@@ -0,0 +1,158 @@
+This test checks basic functionality of the textDocument/highlight request.
+
+-- highlights.go --
+package highlights
+
+import (
+ "fmt" //@loc(fmtImp, "\"fmt\""),highlight(fmtImp, fmtImp, fmt1, fmt2, fmt3, fmt4)
+ h2 "net/http" //@loc(hImp, "h2"),highlight(hImp, hImp, hUse)
+ "sort"
+)
+
+type F struct{ bar int } //@loc(barDeclaration, "bar"),highlight(barDeclaration, barDeclaration, bar1, bar2, bar3)
+
+func _() F {
+ return F{
+ bar: 123, //@loc(bar1, "bar"),highlight(bar1, barDeclaration, bar1, bar2, bar3)
+ }
+}
+
+var foo = F{bar: 52} //@loc(fooDeclaration, "foo"),loc(bar2, "bar"),highlight(fooDeclaration, fooDeclaration, fooUse),highlight(bar2, barDeclaration, bar1, bar2, bar3)
+
+func Print() { //@loc(printFunc, "Print"),highlight(printFunc, printFunc, printTest)
+ _ = h2.Client{} //@loc(hUse, "h2"),highlight(hUse, hImp, hUse)
+
+ fmt.Println(foo) //@loc(fooUse, "foo"),highlight(fooUse, fooDeclaration, fooUse),loc(fmt1, "fmt"),highlight(fmt1, fmtImp, fmt1, fmt2, fmt3, fmt4)
+ fmt.Print("yo") //@loc(printSep, "Print"),highlight(printSep, printSep, print1, print2),loc(fmt2, "fmt"),highlight(fmt2, fmtImp, fmt1, fmt2, fmt3, fmt4)
+}
+
+func (x *F) Inc() { //@loc(xRightDecl, "x"),loc(xLeftDecl, " *"),highlight(xRightDecl, xRightDecl, xUse),highlight(xLeftDecl, xRightDecl, xUse)
+ x.bar++ //@loc(xUse, "x"),loc(bar3, "bar"),highlight(xUse, xRightDecl, xUse),highlight(bar3, barDeclaration, bar1, bar2, bar3)
+}
+
+func testFunctions() {
+ fmt.Print("main start") //@loc(print1, "Print"),highlight(print1, printSep, print1, print2),loc(fmt3, "fmt"),highlight(fmt3, fmtImp, fmt1, fmt2, fmt3, fmt4)
+ fmt.Print("ok") //@loc(print2, "Print"),highlight(print2, printSep, print1, print2),loc(fmt4, "fmt"),highlight(fmt4, fmtImp, fmt1, fmt2, fmt3, fmt4)
+ Print() //@loc(printTest, "Print"),highlight(printTest, printFunc, printTest)
+}
+
+// DocumentHighlight is undefined, so its uses below are type errors.
+// Nevertheless, document highlighting should still work.
+//@diag(doc1, re"undefined|undeclared"), diag(doc2, re"undefined|undeclared"), diag(doc3, re"undefined|undeclared")
+
+func toProtocolHighlight(rngs []int) []DocumentHighlight { //@loc(doc1, "DocumentHighlight"),loc(docRet1, "[]DocumentHighlight"),highlight(doc1, docRet1, doc1, doc2, doc3, result)
+ result := make([]DocumentHighlight, 0, len(rngs)) //@loc(doc2, "DocumentHighlight"),highlight(doc2, doc1, doc2, doc3)
+ for _, rng := range rngs {
+ result = append(result, DocumentHighlight{ //@loc(doc3, "DocumentHighlight"),highlight(doc3, doc1, doc2, doc3)
+ Range: rng,
+ })
+ }
+ return result //@loc(result, "result")
+}
+
+func testForLoops() {
+ for i := 0; i < 10; i++ { //@loc(forDecl1, "for"),highlight(forDecl1, forDecl1, brk1, cont1)
+ if i > 8 {
+ break //@loc(brk1, "break"),highlight(brk1, forDecl1, brk1, cont1)
+ }
+ if i < 2 {
+ for j := 1; j < 10; j++ { //@loc(forDecl2, "for"),highlight(forDecl2, forDecl2, cont2)
+ if j < 3 {
+ for k := 1; k < 10; k++ { //@loc(forDecl3, "for"),highlight(forDecl3, forDecl3, cont3)
+ if k < 3 {
+ continue //@loc(cont3, "continue"),highlight(cont3, forDecl3, cont3)
+ }
+ }
+ continue //@loc(cont2, "continue"),highlight(cont2, forDecl2, cont2)
+ }
+ }
+ continue //@loc(cont1, "continue"),highlight(cont1, forDecl1, brk1, cont1)
+ }
+ }
+
+ arr := []int{}
+ for i := range arr { //@loc(forDecl4, "for"),highlight(forDecl4, forDecl4, brk4, cont4)
+ if i > 8 {
+ break //@loc(brk4, "break"),highlight(brk4, forDecl4, brk4, cont4)
+ }
+ if i < 4 {
+ continue //@loc(cont4, "continue"),highlight(cont4, forDecl4, brk4, cont4)
+ }
+ }
+
+Outer:
+ for i := 0; i < 10; i++ { //@loc(forDecl5, "for"),highlight(forDecl5, forDecl5, brk5, brk6, brk8)
+ break //@loc(brk5, "break"),highlight(brk5, forDecl5, brk5, brk6, brk8)
+ for { //@loc(forDecl6, "for"),highlight(forDecl6, forDecl6, cont5), diag("for", re"unreachable")
+ if i == 1 {
+ break Outer //@loc(brk6, "break Outer"),highlight(brk6, forDecl5, brk5, brk6, brk8)
+ }
+ switch i { //@loc(switch1, "switch"),highlight(switch1, switch1, brk7)
+ case 5:
+ break //@loc(brk7, "break"),highlight(brk7, switch1, brk7)
+ case 6:
+ continue //@loc(cont5, "continue"),highlight(cont5, forDecl6, cont5)
+ case 7:
+ break Outer //@loc(brk8, "break Outer"),highlight(brk8, forDecl5, brk5, brk6, brk8)
+ }
+ }
+ }
+}
+
+func testSwitch() {
+ var i, j int
+
+L1:
+ for { //@loc(forDecl7, "for"),highlight(forDecl7, forDecl7, brk10, cont6)
+ L2:
+ switch i { //@loc(switch2, "switch"),highlight(switch2, switch2, brk11, brk12, brk13)
+ case 1:
+ switch j { //@loc(switch3, "switch"),highlight(switch3, switch3, brk9)
+ case 1:
+ break //@loc(brk9, "break"),highlight(brk9, switch3, brk9)
+ case 2:
+ break L1 //@loc(brk10, "break L1"),highlight(brk10, forDecl7, brk10, cont6)
+ case 3:
+ break L2 //@loc(brk11, "break L2"),highlight(brk11, switch2, brk11, brk12, brk13)
+ default:
+ continue //@loc(cont6, "continue"),highlight(cont6, forDecl7, brk10, cont6)
+ }
+ case 2:
+ break //@loc(brk12, "break"),highlight(brk12, switch2, brk11, brk12, brk13)
+ default:
+ break L2 //@loc(brk13, "break L2"),highlight(brk13, switch2, brk11, brk12, brk13)
+ }
+ }
+}
+
+func testReturn() bool { //@loc(func1, "func"),loc(bool1, "bool"),highlight(func1, func1, fullRet11, fullRet12),highlight(bool1, bool1, false1, bool2, true1)
+ if 1 < 2 {
+ return false //@loc(ret11, "return"),loc(fullRet11, "return false"),loc(false1, "false"),highlight(ret11, func1, fullRet11, fullRet12)
+ }
+ candidates := []int{}
+ sort.SliceStable(candidates, func(i, j int) bool { //@loc(func2, "func"),loc(bool2, "bool"),highlight(func2, func2, fullRet2)
+ return candidates[i] > candidates[j] //@loc(ret2, "return"),loc(fullRet2, "return candidates[i] > candidates[j]"),highlight(ret2, func2, fullRet2)
+ })
+ return true //@loc(ret12, "return"),loc(fullRet12, "return true"),loc(true1, "true"),highlight(ret12, func1, fullRet11, fullRet12)
+}
+
+func testReturnFields() float64 { //@loc(retVal1, "float64"),highlight(retVal1, retVal1, retVal11, retVal21)
+ if 1 < 2 {
+ return 20.1 //@loc(retVal11, "20.1"),highlight(retVal11, retVal1, retVal11, retVal21)
+ }
+ z := 4.3 //@loc(zDecl, "z")
+ return z //@loc(retVal21, "z"),highlight(retVal21, retVal1, retVal11, zDecl, retVal21)
+}
+
+func testReturnMultipleFields() (float32, string) { //@loc(retVal31, "float32"),loc(retVal32, "string"),highlight(retVal31, retVal31, retVal41, retVal51),highlight(retVal32, retVal32, retVal42, retVal52)
+ y := "im a var" //@loc(yDecl, "y"),
+ if 1 < 2 {
+ return 20.1, y //@loc(retVal41, "20.1"),loc(retVal42, "y"),highlight(retVal41, retVal31, retVal41, retVal51),highlight(retVal42, retVal32, yDecl, retVal42, retVal52)
+ }
+ return 4.9, "test" //@loc(retVal51, "4.9"),loc(retVal52, "\"test\""),highlight(retVal51, retVal31, retVal41, retVal51),highlight(retVal52, retVal32, retVal42, retVal52)
+}
+
+func testReturnFunc() int32 { //@loc(retCall, "int32")
+ mulch := 1 //@loc(mulchDec, "mulch"),highlight(mulchDec, mulchDec, mulchRet)
+ return int32(mulch) //@loc(mulchRet, "mulch"),loc(retFunc, "int32"),loc(retTotal, "int32(mulch)"),highlight(mulchRet, mulchDec, mulchRet),highlight(retFunc, retCall, retFunc, retTotal)
+}
diff --git a/gopls/internal/regtest/marker/testdata/highlight/issue60435.txt b/gopls/internal/regtest/marker/testdata/highlight/issue60435.txt
new file mode 100644
index 00000000000..324e4b85e77
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/highlight/issue60435.txt
@@ -0,0 +1,15 @@
+This is a regression test for issue 60435:
+Highlighting "net/http" shouldn't have any effect
+on an import path that contains it as a substring,
+such as httptest.
+
+-- highlights.go --
+package highlights
+
+import (
+ "net/http" //@loc(httpImp, `"net/http"`)
+ "net/http/httptest" //@loc(httptestImp, `"net/http/httptest"`)
+)
+
+var _ = httptest.NewRequest
+var _ = http.NewRequest //@loc(here, "http"), highlight(here, here, httpImp)
diff --git a/gopls/internal/regtest/marker/testdata/hover/godef.txt b/gopls/internal/regtest/marker/testdata/hover/godef.txt
new file mode 100644
index 00000000000..e6a67616302
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/hover/godef.txt
@@ -0,0 +1,406 @@
+This test was ported from 'godef' in the old marker tests.
+It tests various hover and definition requests.
+
+-- flags --
+-min_go=go1.20
+
+-- go.mod --
+module godef.test
+
+go 1.18
+
+-- a/a_x_test.go --
+package a_test
+
+import (
+ "testing"
+)
+
+func TestA2(t *testing.T) { //@hover("TestA2", "TestA2", TestA2)
+ Nonexistant() //@diag("Nonexistant", re"(undeclared name|undefined): Nonexistant")
+}
+
+-- @TestA2/hover.md --
+```go
+func TestA2(t *testing.T)
+```
+-- @ember/hover.md --
+```go
+field Member string
+```
+
+@loc(Member, "Member")
+
+
+[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing.Member)
+-- a/d.go --
+package a //@hover("a", _, a)
+
+import "fmt"
+
+type Thing struct { //@loc(Thing, "Thing")
+ Member string //@loc(Member, "Member")
+}
+
+var Other Thing //@loc(Other, "Other")
+
+func Things(val []string) []Thing { //@loc(Things, "Things")
+ return nil
+}
+
+func (t Thing) Method(i int) string { //@loc(Method, "Method")
+ return t.Member
+}
+
+func (t Thing) Method3() {
+}
+
+func (t *Thing) Method2(i int, j int) (error, string) {
+ return nil, t.Member
+}
+
+func (t *Thing) private() {
+}
+
+func useThings() {
+ t := Thing{ //@hover("ing", "Thing", ing)
+ Member: "string", //@hover("ember", "Member", ember), def("ember", Member)
+ }
+ fmt.Print(t.Member) //@hover("ember", "Member", ember), def("ember", Member)
+ fmt.Print(Other) //@hover("ther", "Other", ther), def("ther", Other)
+ Things(nil) //@hover("ings", "Things", ings), def("ings", Things)
+ t.Method(0) //@hover("eth", "Method", eth), def("eth", Method)
+}
+
+type NextThing struct { //@loc(NextThing, "NextThing")
+ Thing
+ Value int
+}
+
+func (n NextThing) another() string {
+ return n.Member
+}
+
+// Shadows Thing.Method3
+func (n *NextThing) Method3() int {
+ return n.Value
+}
+
+var nextThing NextThing //@hover("NextThing", "NextThing", NextThing), def("NextThing", NextThing)
+
+-- @ings/hover.md --
+```go
+func Things(val []string) []Thing
+```
+
+[`a.Things` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Things)
+-- @ther/hover.md --
+```go
+var Other Thing
+```
+
+@loc(Other, "Other")
+
+
+[`a.Other` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Other)
+-- @a/hover.md --
+-- @ing/hover.md --
+```go
+type Thing struct {
+ Member string //@loc(Member, "Member")
+}
+
+func (Thing).Method(i int) string
+func (*Thing).Method2(i int, j int) (error, string)
+func (Thing).Method3()
+func (*Thing).private()
+```
+
+[`a.Thing` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing)
+-- @NextThing/hover.md --
+```go
+type NextThing struct {
+ Thing
+ Value int
+}
+
+func (*NextThing).Method3() int
+func (NextThing).another() string
+```
+
+[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/godef.test/a#NextThing)
+-- @eth/hover.md --
+```go
+func (Thing).Method(i int) string
+```
+
+[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing.Method)
+-- a/f.go --
+// Package a is a package for testing go to definition.
+package a
+
+import "fmt"
+
+func TypeStuff() {
+ var x string
+
+ switch y := interface{}(x).(type) { //@loc(y, "y"), hover("y", "y", y) , def("y", y)
+ case int: //@loc(intY, "int")
+ fmt.Printf("%v", y) //@hover("y", "y", inty), def("y", y)
+ case string: //@loc(stringY, "string")
+ fmt.Printf("%v", y) //@hover("y", "y", stringy), def("y", y)
+ }
+
+}
+-- @inty/hover.md --
+```go
+var y int
+```
+-- @stringy/hover.md --
+```go
+var y string
+```
+-- @y/hover.md --
+```go
+var y interface{}
+```
+-- a/h.go --
+package a
+
+func _() {
+ type s struct {
+ nested struct {
+ // nested number
+ number int64 //@loc(nestedNumber, "number")
+ }
+ nested2 []struct {
+ // nested string
+ str string //@loc(nestedString, "str")
+ }
+ x struct {
+ x struct {
+ x struct {
+ x struct {
+ x struct {
+ // nested map
+ m map[string]float64 //@loc(nestedMap, "m")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ var t s
+ _ = t.nested.number //@hover("number", "number", nestedNumber), def("number", nestedNumber)
+ _ = t.nested2[0].str //@hover("str", "str", nestedString), def("str", nestedString)
+ _ = t.x.x.x.x.x.m //@hover("m", "m", nestedMap), def("m", nestedMap)
+}
+
+func _() {
+ var s struct {
+ // a field
+ a int //@loc(structA, "a")
+ // b nested struct
+ b struct { //@loc(structB, "b")
+ // c field of nested struct
+ c int //@loc(structC, "c")
+ }
+ }
+ _ = s.a //@def("a", structA)
+ _ = s.b //@def("b", structB)
+ _ = s.b.c //@def("c", structC)
+
+ var arr []struct {
+ // d field
+ d int //@loc(arrD, "d")
+ // e nested struct
+ e struct { //@loc(arrE, "e")
+ // f field of nested struct
+ f int //@loc(arrF, "f")
+ }
+ }
+ _ = arr[0].d //@def("d", arrD)
+ _ = arr[0].e //@def("e", arrE)
+ _ = arr[0].e.f //@def("f", arrF)
+
+ var complex []struct {
+ c <-chan map[string][]struct {
+ // h field
+ h int //@loc(complexH, "h")
+ // i nested struct
+ i struct { //@loc(complexI, "i")
+ // j field of nested struct
+ j int //@loc(complexJ, "j")
+ }
+ }
+ }
+ _ = (<-complex[0].c)["0"][0].h //@def("h", complexH)
+ _ = (<-complex[0].c)["0"][0].i //@def("i", complexI)
+ _ = (<-complex[0].c)["0"][0].i.j //@def("j", complexJ)
+
+ var mapWithStructKey map[struct { //@diag("struct", re"invalid map key")
+ // X key field
+ x []string //@loc(mapStructKeyX, "x")
+ }]int
+ for k := range mapWithStructKey {
+ _ = k.x //@def("x", mapStructKeyX)
+ }
+
+ var mapWithStructKeyAndValue map[struct {
+ // Y key field
+ y string //@loc(mapStructKeyY, "y")
+ }]struct {
+ // X value field
+ x string //@loc(mapStructValueX, "x")
+ }
+ for k, v := range mapWithStructKeyAndValue {
+ // TODO: we don't show docs for y field because both map key and value
+ // are structs. And in this case, we parse only map value
+ _ = k.y //@hover("y", "y", hoverStructKeyY), def("y", mapStructKeyY)
+ _ = v.x //@hover("x", "x", hoverStructKeyX), def("x", mapStructValueX)
+ }
+
+ var i []map[string]interface {
+ // open method comment
+ open() error //@loc(openMethod, "open")
+ }
+ i[0]["1"].open() //@hover("pen","open", openMethod), def("open", openMethod)
+}
+
+func _() {
+ test := struct {
+ // test description
+ desc string //@loc(testDescription, "desc")
+ }{}
+ _ = test.desc //@def("desc", testDescription)
+
+ for _, tt := range []struct {
+ // test input
+ in map[string][]struct { //@loc(testInput, "in")
+ // test key
+ key string //@loc(testInputKey, "key")
+ // test value
+ value interface{} //@loc(testInputValue, "value")
+ }
+ result struct {
+ v <-chan struct {
+ // expected test value
+ value int //@loc(testResultValue, "value")
+ }
+ }
+ }{} {
+ _ = tt.in //@def("in", testInput)
+ _ = tt.in["0"][0].key //@def("key", testInputKey)
+ _ = tt.in["0"][0].value //@def("value", testInputValue)
+
+ _ = (<-tt.result.v).value //@def("value", testResultValue)
+ }
+}
+
+func _() {
+ getPoints := func() []struct {
+ // X coord
+ x int //@loc(returnX, "x")
+ // Y coord
+ y int //@loc(returnY, "y")
+ } {
+ return nil
+ }
+
+ r := getPoints()
+ _ = r[0].x //@def("x", returnX)
+ _ = r[0].y //@def("y", returnY)
+}
+-- @hoverStructKeyX/hover.md --
+```go
+field x string
+```
+
+X value field
+-- @hoverStructKeyY/hover.md --
+```go
+field y string
+```
+
+Y key field
+-- @nestedNumber/hover.md --
+```go
+field number int64
+```
+
+nested number
+-- @nestedString/hover.md --
+```go
+field str string
+```
+
+nested string
+-- @openMethod/hover.md --
+```go
+func (interface).open() error
+```
+
+open method comment
+-- @nestedMap/hover.md --
+```go
+field m map[string]float64
+```
+
+nested map
+-- b/e.go --
+package b
+
+import (
+ "fmt"
+
+ "godef.test/a"
+)
+
+func useThings() {
+ t := a.Thing{} //@loc(bStructType, "ing")
+ fmt.Print(t.Member) //@loc(bMember, "ember")
+ fmt.Print(a.Other) //@loc(bVar, "ther")
+ a.Things(nil) //@loc(bFunc, "ings")
+}
+
+/*@
+def(bStructType, Thing)
+def(bMember, Member)
+def(bVar, Other)
+def(bFunc, Things)
+*/
+
+func _() {
+ var x interface{}
+ switch x := x.(type) { //@hover("x", "x", xInterface)
+ case string: //@loc(eString, "string")
+ fmt.Println(x) //@hover("x", "x", xString)
+ case int: //@loc(eInt, "int")
+ fmt.Println(x) //@hover("x", "x", xInt)
+ }
+}
+-- @xInt/hover.md --
+```go
+var x int
+```
+-- @xInterface/hover.md --
+```go
+var x interface{}
+```
+-- @xString/hover.md --
+```go
+var x string
+```
+-- broken/unclosedIf.go --
+package broken
+
+import "fmt"
+
+func unclosedIf() {
+ if false {
+ var myUnclosedIf string //@loc(myUnclosedIf, "myUnclosedIf")
+ fmt.Printf("s = %v\n", myUnclosedIf) //@def("my", myUnclosedIf)
+}
+
+func _() {} //@diag("_", re"expected")
diff --git a/gopls/internal/regtest/marker/testdata/rename/basic.txt b/gopls/internal/regtest/marker/testdata/rename/basic.txt
index 28de07c3482..8a1d42d23ec 100644
--- a/gopls/internal/regtest/marker/testdata/rename/basic.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/basic.txt
@@ -3,36 +3,36 @@ This test performs basic coverage of 'rename' within a single package.
-- basic.go --
package p
-func f(x int) { println(x) } //@rename("x", y, xToy)
+func f(x int) { println(x) } //@rename("x", "y", xToy)
-- @xToy/basic.go --
package p
-func f(y int) { println(y) } //@rename("x", y, xToy)
+func f(y int) { println(y) } //@rename("x", "y", xToy)
-- alias.go --
package p
// from golang/go#61625
type LongNameHere struct{}
-type A = LongNameHere //@rename("A", B, AToB)
+type A = LongNameHere //@rename("A", "B", AToB)
func Foo() A
+-- errors.go --
+package p
+
+func _(x []int) { //@renameerr("_", "blank", `can't rename "_"`)
+ x = append(x, 1) //@renameerr("append", "blank", "built in and cannot be renamed")
+ x = nil //@renameerr("nil", "blank", "built in and cannot be renamed")
+ x = nil //@renameerr("x", "x", "old and new names are the same: x")
+ _ = 1 //@renameerr("1", "x", "no identifier found")
+}
+
-- @AToB/alias.go --
package p
// from golang/go#61625
type LongNameHere struct{}
-type B = LongNameHere //@rename("A", B, AToB)
+type B = LongNameHere //@rename("A", "B", AToB)
func Foo() B
--- errors.go --
-package p
-
-func _(x []int) { //@renameerr("_", blank, `can't rename "_"`)
- x = append(x, 1) //@renameerr("append", blank, "built in and cannot be renamed")
- x = nil //@renameerr("nil", blank, "built in and cannot be renamed")
- x = nil //@renameerr("x", x, "old and new names are the same: x")
- _ = 1 //@renameerr("1", x, "no identifier found")
-}
-
diff --git a/gopls/internal/regtest/marker/testdata/rename/conflict.txt b/gopls/internal/regtest/marker/testdata/rename/conflict.txt
index 18438c8a801..3d7d21cb3e4 100644
--- a/gopls/internal/regtest/marker/testdata/rename/conflict.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/conflict.txt
@@ -12,7 +12,7 @@ var x int
func f(y int) {
println(x)
- println(y) //@renameerr("y", x, errSuperBlockConflict)
+ println(y) //@renameerr("y", "x", errSuperBlockConflict)
}
-- @errSuperBlockConflict --
@@ -25,7 +25,7 @@ package sub
var a int
func f2(b int) {
- println(a) //@renameerr("a", b, errSubBlockConflict)
+ println(a) //@renameerr("a", "b", errSubBlockConflict)
println(b)
}
@@ -36,7 +36,7 @@ sub/p.go:5:9: by this intervening var definition
-- pkgname/p.go --
package pkgname
-import e1 "errors" //@renameerr("e1", errors, errImportConflict)
+import e1 "errors" //@renameerr("e1", "errors", errImportConflict)
import "errors"
var _ = errors.New
@@ -51,7 +51,7 @@ var x int
-- pkgname2/p2.go --
package pkgname2
-import "errors" //@renameerr("errors", x, errImportConflict2)
+import "errors" //@renameerr("errors", "x", errImportConflict2)
var _ = errors.New
-- @errImportConflict2 --
diff --git a/gopls/internal/regtest/marker/testdata/rename/embed.txt b/gopls/internal/regtest/marker/testdata/rename/embed.txt
index 68cf771bc0f..c0b0301fac6 100644
--- a/gopls/internal/regtest/marker/testdata/rename/embed.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/embed.txt
@@ -7,30 +7,30 @@ go 1.12
-- a/a.go --
package a
-type A int //@rename("A", A2, type)
+type A int //@rename("A", "A2", type)
-- b/b.go --
package b
import "example.com/a"
-type B struct { a.A } //@renameerr("A", A3, errAnonField)
+type B struct { a.A } //@renameerr("A", "A3", errAnonField)
-var _ = new(B).A //@renameerr("A", A4, errAnonField)
+var _ = new(B).A //@renameerr("A", "A4", errAnonField)
-- @errAnonField --
can't rename embedded fields: rename the type directly or name the field
-- @type/a/a.go --
package a
-type A2 int //@rename("A", A2, type)
+type A2 int //@rename("A", "A2", type)
-- @type/b/b.go --
package b
import "example.com/a"
-type B struct { a.A2 } //@renameerr("A", A3, errAnonField)
+type B struct { a.A2 } //@renameerr("A", "A3", errAnonField)
-var _ = new(B).A2 //@renameerr("A", A4, errAnonField)
+var _ = new(B).A2 //@renameerr("A", "A4", errAnonField)
diff --git a/gopls/internal/regtest/marker/testdata/rename/generics.txt b/gopls/internal/regtest/marker/testdata/rename/generics.txt
index 9f015ee2d08..db64bb4fbf9 100644
--- a/gopls/internal/regtest/marker/testdata/rename/generics.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/generics.txt
@@ -21,7 +21,7 @@ package a
type I int
-func (I) m() {} //@rename("m", M, mToM)
+func (I) m() {} //@rename("m", "M", mToM)
func _[P ~[]int]() {
_ = P{}
@@ -32,7 +32,7 @@ package a
type I int
-func (I) M() {} //@rename("m", M, mToM)
+func (I) M() {} //@rename("m", "M", mToM)
func _[P ~[]int]() {
_ = P{}
@@ -41,43 +41,43 @@ func _[P ~[]int]() {
-- g.go --
package a
-type S[P any] struct { //@rename("P", Q, PToQ)
+type S[P any] struct { //@rename("P", "Q", PToQ)
P P
F func(P) P
}
func F[R any](r R) {
- var _ R //@rename("R", S, RToS)
+ var _ R //@rename("R", "S", RToS)
}
-- @PToQ/g.go --
package a
-type S[Q any] struct { //@rename("P", Q, PToQ)
+type S[Q any] struct { //@rename("P", "Q", PToQ)
P Q
F func(Q) Q
}
func F[R any](r R) {
- var _ R //@rename("R", S, RToS)
+ var _ R //@rename("R", "S", RToS)
}
-- @RToS/g.go --
package a
-type S[P any] struct { //@rename("P", Q, PToQ)
+type S[P any] struct { //@rename("P", "Q", PToQ)
P P
F func(P) P
}
func F[S any](r S) {
- var _ S //@rename("R", S, RToS)
+ var _ S //@rename("R", "S", RToS)
}
-- issue61635/p.go --
package issue61635
-type builder[S ~[]F, F ~string] struct { //@rename("S", T, SToT)
+type builder[S ~[]F, F ~string] struct { //@rename("S", "T", SToT)
name string
elements S
elemData map[F][]ElemData[F]
@@ -101,7 +101,7 @@ var _ issue61635.ElemData[string]
-- @SToT/issue61635/p.go --
package issue61635
-type builder[T ~[]F, F ~string] struct { //@rename("S", T, SToT)
+type builder[T ~[]F, F ~string] struct { //@rename("S", "T", SToT)
name string
elements T
elemData map[F][]ElemData[F]
@@ -118,123 +118,123 @@ type BuilderImpl[S ~[]F, F ~string] struct{ builder[S, F] }
-- instances/type.go --
package instances
-type R[P any] struct { //@rename("R", u, Rtou)
- Next *R[P] //@rename("R", s, RTos)
+type R[P any] struct { //@rename("R", "u", Rtou)
+ Next *R[P] //@rename("R", "s", RTos)
}
-func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", Do1, DoToDo1)
+func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1)
var x R[P]
- return rv.Do(x) //@rename("Do", Do2, DoToDo2)
+ return rv.Do(x) //@rename("Do", "Do2", DoToDo2)
}
func _() {
- var x R[int] //@rename("R", r, RTor)
+ var x R[int] //@rename("R", "r", RTor)
x = x.Do(x)
}
-- @RTos/instances/type.go --
package instances
-type s[P any] struct { //@rename("R", u, Rtou)
- Next *s[P] //@rename("R", s, RTos)
+type s[P any] struct { //@rename("R", "u", Rtou)
+ Next *s[P] //@rename("R", "s", RTos)
}
-func (rv s[P]) Do(s[P]) s[P] { //@rename("Do", Do1, DoToDo1)
+func (rv s[P]) Do(s[P]) s[P] { //@rename("Do", "Do1", DoToDo1)
var x s[P]
- return rv.Do(x) //@rename("Do", Do2, DoToDo2)
+ return rv.Do(x) //@rename("Do", "Do2", DoToDo2)
}
func _() {
- var x s[int] //@rename("R", r, RTor)
+ var x s[int] //@rename("R", "r", RTor)
x = x.Do(x)
}
-- @Rtou/instances/type.go --
package instances
-type u[P any] struct { //@rename("R", u, Rtou)
- Next *u[P] //@rename("R", s, RTos)
+type u[P any] struct { //@rename("R", "u", Rtou)
+ Next *u[P] //@rename("R", "s", RTos)
}
-func (rv u[P]) Do(u[P]) u[P] { //@rename("Do", Do1, DoToDo1)
+func (rv u[P]) Do(u[P]) u[P] { //@rename("Do", "Do1", DoToDo1)
var x u[P]
- return rv.Do(x) //@rename("Do", Do2, DoToDo2)
+ return rv.Do(x) //@rename("Do", "Do2", DoToDo2)
}
func _() {
- var x u[int] //@rename("R", r, RTor)
+ var x u[int] //@rename("R", "r", RTor)
x = x.Do(x)
}
-- @DoToDo1/instances/type.go --
package instances
-type R[P any] struct { //@rename("R", u, Rtou)
- Next *R[P] //@rename("R", s, RTos)
+type R[P any] struct { //@rename("R", "u", Rtou)
+ Next *R[P] //@rename("R", "s", RTos)
}
-func (rv R[P]) Do1(R[P]) R[P] { //@rename("Do", Do1, DoToDo1)
+func (rv R[P]) Do1(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1)
var x R[P]
- return rv.Do1(x) //@rename("Do", Do2, DoToDo2)
+ return rv.Do1(x) //@rename("Do", "Do2", DoToDo2)
}
func _() {
- var x R[int] //@rename("R", r, RTor)
+ var x R[int] //@rename("R", "r", RTor)
x = x.Do1(x)
}
-- @DoToDo2/instances/type.go --
package instances
-type R[P any] struct { //@rename("R", u, Rtou)
- Next *R[P] //@rename("R", s, RTos)
+type R[P any] struct { //@rename("R", "u", Rtou)
+ Next *R[P] //@rename("R", "s", RTos)
}
-func (rv R[P]) Do2(R[P]) R[P] { //@rename("Do", Do1, DoToDo1)
+func (rv R[P]) Do2(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1)
var x R[P]
- return rv.Do2(x) //@rename("Do", Do2, DoToDo2)
+ return rv.Do2(x) //@rename("Do", "Do2", DoToDo2)
}
func _() {
- var x R[int] //@rename("R", r, RTor)
+ var x R[int] //@rename("R", "r", RTor)
x = x.Do2(x)
}
-- instances/func.go --
package instances
-func Foo[P any](p P) { //@rename("Foo", Bar, FooToBar)
- Foo(p) //@rename("Foo", Baz, FooToBaz)
+func Foo[P any](p P) { //@rename("Foo", "Bar", FooToBar)
+ Foo(p) //@rename("Foo", "Baz", FooToBaz)
}
-- @FooToBar/instances/func.go --
package instances
-func Bar[P any](p P) { //@rename("Foo", Bar, FooToBar)
- Bar(p) //@rename("Foo", Baz, FooToBaz)
+func Bar[P any](p P) { //@rename("Foo", "Bar", FooToBar)
+ Bar(p) //@rename("Foo", "Baz", FooToBaz)
}
-- @FooToBaz/instances/func.go --
package instances
-func Baz[P any](p P) { //@rename("Foo", Bar, FooToBar)
- Baz(p) //@rename("Foo", Baz, FooToBaz)
+func Baz[P any](p P) { //@rename("Foo", "Bar", FooToBar)
+ Baz(p) //@rename("Foo", "Baz", FooToBaz)
}
-- @RTor/instances/type.go --
package instances
-type r[P any] struct { //@rename("R", u, Rtou)
- Next *r[P] //@rename("R", s, RTos)
+type r[P any] struct { //@rename("R", "u", Rtou)
+ Next *r[P] //@rename("R", "s", RTos)
}
-func (rv r[P]) Do(r[P]) r[P] { //@rename("Do", Do1, DoToDo1)
+func (rv r[P]) Do(r[P]) r[P] { //@rename("Do", "Do1", DoToDo1)
var x r[P]
- return rv.Do(x) //@rename("Do", Do2, DoToDo2)
+ return rv.Do(x) //@rename("Do", "Do2", DoToDo2)
}
func _() {
- var x r[int] //@rename("R", r, RTor)
+ var x r[int] //@rename("R", "r", RTor)
x = x.Do(x)
}
diff --git a/gopls/internal/regtest/marker/testdata/rename/issue60789.txt b/gopls/internal/regtest/marker/testdata/rename/issue60789.txt
index ee2a084581b..40173320c74 100644
--- a/gopls/internal/regtest/marker/testdata/rename/issue60789.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/issue60789.txt
@@ -13,8 +13,8 @@ go 1.12
-- a/a.go --
package a
-type unexported int
-func (unexported) F() {} //@rename("F", G, fToG)
+type unexported int
+func (unexported) F() {} //@rename("F", "G", fToG)
var _ = unexported(0).F
@@ -29,8 +29,8 @@ import _ "example.com/a"
-- @fToG/a/a.go --
package a
-type unexported int
-func (unexported) G() {} //@rename("F", G, fToG)
+type unexported int
+func (unexported) G() {} //@rename("F", "G", fToG)
var _ = unexported(0).G
diff --git a/gopls/internal/regtest/marker/testdata/rename/issue61294.txt b/gopls/internal/regtest/marker/testdata/rename/issue61294.txt
index 83d68582883..3ce1dbc7670 100644
--- a/gopls/internal/regtest/marker/testdata/rename/issue61294.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/issue61294.txt
@@ -13,7 +13,7 @@ package a
func One()
-func Two(One int) //@rename("One", Three, OneToThree)
+func Two(One int) //@rename("One", "Three", OneToThree)
-- b/b.go --
package b
@@ -25,5 +25,5 @@ package a
func One()
-func Two(Three int) //@rename("One", Three, OneToThree)
+func Two(Three int) //@rename("One", "Three", OneToThree)
diff --git a/gopls/internal/regtest/marker/testdata/rename/issue61640.txt b/gopls/internal/regtest/marker/testdata/rename/issue61640.txt
index 91c2b76933d..70a6123ab32 100644
--- a/gopls/internal/regtest/marker/testdata/rename/issue61640.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/issue61640.txt
@@ -9,7 +9,7 @@ package a
// This file is adapted from the example in the issue.
type builder[S ~[]int] struct {
- elements S //@rename("elements", elements2, OneToTwo)
+ elements S //@rename("elements", "elements2", OneToTwo)
}
type BuilderImpl[S ~[]int] struct{ builder[S] }
@@ -30,7 +30,7 @@ package a
// This file is adapted from the example in the issue.
type builder[S ~[]int] struct {
- elements2 S //@rename("elements", elements2, OneToTwo)
+ elements2 S //@rename("elements", "elements2", OneToTwo)
}
type BuilderImpl[S ~[]int] struct{ builder[S] }
diff --git a/gopls/internal/regtest/marker/testdata/rename/issue61813.txt b/gopls/internal/regtest/marker/testdata/rename/issue61813.txt
index ae5162b84a4..52813f869a4 100644
--- a/gopls/internal/regtest/marker/testdata/rename/issue61813.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/issue61813.txt
@@ -5,7 +5,7 @@ package p
type P struct{}
-func (P) M() {} //@rename("M", N, MToN)
+func (P) M() {} //@rename("M", "N", MToN)
var x = []*P{{}}
-- @MToN/p.go --
@@ -13,6 +13,6 @@ package p
type P struct{}
-func (P) N() {} //@rename("M", N, MToN)
+func (P) N() {} //@rename("M", "N", MToN)
var x = []*P{{}}
diff --git a/gopls/internal/regtest/marker/testdata/rename/methods.txt b/gopls/internal/regtest/marker/testdata/rename/methods.txt
index 1bd985bcf57..05a5cd8697b 100644
--- a/gopls/internal/regtest/marker/testdata/rename/methods.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/methods.txt
@@ -12,7 +12,7 @@ package a
type A int
-func (A) F() {} //@renameerr("F", G, errAfToG)
+func (A) F() {} //@renameerr("F", "G", errAfToG)
-- b/b.go --
package b
@@ -20,7 +20,7 @@ package b
import "example.com/a"
import "example.com/c"
-type B interface { F() } //@rename("F", G, BfToG)
+type B interface { F() } //@rename("F", "G", BfToG)
var _ B = a.A(0)
var _ B = c.C(0)
@@ -30,7 +30,7 @@ package c
type C int
-func (C) F() {} //@renameerr("F", G, errCfToG)
+func (C) F() {} //@renameerr("F", "G", errCfToG)
-- d/d.go --
package d
@@ -49,7 +49,7 @@ package b
import "example.com/a"
import "example.com/c"
-type B interface { G() } //@rename("F", G, BfToG)
+type B interface { G() } //@rename("F", "G", BfToG)
var _ B = a.A(0)
var _ B = c.C(0)
diff --git a/gopls/internal/regtest/marker/testdata/rename/typeswitch.txt b/gopls/internal/regtest/marker/testdata/rename/typeswitch.txt
index 6743b99ef29..252c8db7af6 100644
--- a/gopls/internal/regtest/marker/testdata/rename/typeswitch.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/typeswitch.txt
@@ -4,11 +4,11 @@ This test covers the special case of renaming a type switch var.
package p
func _(x interface{}) {
- switch y := x.(type) { //@rename("y", z, yToZ)
+ switch y := x.(type) { //@rename("y", "z", yToZ)
case string:
- print(y) //@rename("y", z, yToZ)
+ print(y) //@rename("y", "z", yToZ)
default:
- print(y) //@rename("y", z, yToZ)
+ print(y) //@rename("y", "z", yToZ)
}
}
@@ -16,11 +16,11 @@ func _(x interface{}) {
package p
func _(x interface{}) {
- switch z := x.(type) { //@rename("y", z, yToZ)
+ switch z := x.(type) { //@rename("y", "z", yToZ)
case string:
- print(z) //@rename("y", z, yToZ)
+ print(z) //@rename("y", "z", yToZ)
default:
- print(z) //@rename("y", z, yToZ)
+ print(z) //@rename("y", "z", yToZ)
}
}
diff --git a/gopls/internal/regtest/marker/testdata/rename/unexported.txt b/gopls/internal/regtest/marker/testdata/rename/unexported.txt
index e5631fa4907..ed60f666d4b 100644
--- a/gopls/internal/regtest/marker/testdata/rename/unexported.txt
+++ b/gopls/internal/regtest/marker/testdata/rename/unexported.txt
@@ -11,7 +11,7 @@ go 1.12
-- a/a.go --
package a
-var S struct{ X int } //@renameerr("X", x, oops)
+var S struct{ X int } //@renameerr("X", "x", oops)
-- a/a_test.go --
package a_test
diff --git a/gopls/internal/regtest/marker/testdata/typedef/typedef.txt b/gopls/internal/regtest/marker/testdata/typedef/typedef.txt
new file mode 100644
index 00000000000..3bc9dabdb8b
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/typedef/typedef.txt
@@ -0,0 +1,68 @@
+This test exercises the textDocument/typeDefinition action.
+
+-- typedef.go --
+package typedef
+
+type Struct struct { //@loc(Struct, "Struct"),
+ Field string
+}
+
+type Int int //@loc(Int, "Int")
+
+func _() {
+ var (
+ value Struct
+ point *Struct
+ )
+ _ = value //@typedef("value", Struct)
+ _ = point //@typedef("point", Struct)
+
+ var (
+ array [3]Struct
+ slice []Struct
+ ch chan Struct
+ complex [3]chan *[5][]Int
+ )
+ _ = array //@typedef("array", Struct)
+ _ = slice //@typedef("slice", Struct)
+ _ = ch //@typedef("ch", Struct)
+ _ = complex //@typedef("complex", Int)
+
+ var s struct {
+ x struct {
+ xx struct {
+ field1 []Struct
+ field2 []Int
+ }
+ }
+ }
+ _ = s.x.xx.field1 //@typedef("field1", Struct)
+ _ = s.x.xx.field2 //@typedef("field2", Int)
+}
+
+func F1() Int { return 0 }
+func F2() (Int, float64) { return 0, 0 }
+func F3() (Struct, int, bool, error) { return Struct{}, 0, false, nil }
+func F4() (**int, Int, bool, *error) { return nil, 0, false, nil }
+func F5() (int, float64, error, Struct) { return 0, 0, nil, Struct{} }
+func F6() (int, float64, ***Struct, error) { return 0, 0, nil, nil }
+
+func _() {
+ F1() //@typedef("F1", Int)
+ F2() //@typedef("F2", Int)
+ F3() //@typedef("F3", Struct)
+ F4() //@typedef("F4", Int)
+ F5() //@typedef("F5", Struct)
+ F6() //@typedef("F6", Struct)
+
+ f := func() Int { return 0 }
+ f() //@typedef("f", Int)
+}
+
+// https://github.com/golang/go/issues/38589#issuecomment-620350922
+func _() {
+ type myFunc func(int) Int //@loc(myFunc, "myFunc")
+
+ var foo myFunc
+ _ = foo() //@typedef("foo", myFunc), diag(")", re"not enough arguments")
+}
diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go
index 853abcd7dff..3f9c5b941c3 100644
--- a/gopls/internal/regtest/misc/configuration_test.go
+++ b/gopls/internal/regtest/misc/configuration_test.go
@@ -42,10 +42,8 @@ var FooErr = errors.New("foo")
cfg.Settings = map[string]interface{}{
"staticcheck": true,
}
- // TODO(rfindley): support waiting on diagnostics following a configuration
- // change.
env.ChangeConfiguration(cfg)
- env.Await(
+ env.AfterChange(
Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")),
)
})
@@ -89,10 +87,8 @@ var ErrFoo = errors.New("foo")
cfg.Settings = map[string]interface{}{
"staticcheck": true,
}
- // TODO(rfindley): support waiting on diagnostics following a configuration
- // change.
env.ChangeConfiguration(cfg)
- env.Await(
+ env.AfterChange(
Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")),
)
})
diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go
index 0a36336b567..d16539f0dbb 100644
--- a/gopls/internal/regtest/misc/definition_test.go
+++ b/gopls/internal/regtest/misc/definition_test.go
@@ -351,7 +351,7 @@ func main() {}
Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- loc, err := env.Editor.GoToTypeDefinition(env.Ctx, env.RegexpSearch("main.go", tt.re))
+ loc, err := env.Editor.TypeDefinition(env.Ctx, env.RegexpSearch("main.go", tt.re))
if tt.wantError {
if err == nil {
t.Fatal("expected error, got nil")
@@ -386,10 +386,7 @@ func F[T comparable]() {}
Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- _, err := env.Editor.GoToTypeDefinition(env.Ctx, env.RegexpSearch("main.go", "comparable")) // must not panic
- if err != nil {
- t.Fatal(err)
- }
+ _ = env.TypeDefinition(env.RegexpSearch("main.go", "comparable")) // must not panic
})
}
@@ -532,3 +529,43 @@ const _ = b.K
}
})
}
+
+const embedDefinition = `
+-- go.mod --
+module mod.com
+
+-- main.go --
+package main
+
+import (
+ "embed"
+)
+
+//go:embed *.txt
+var foo embed.FS
+
+func main() {}
+
+-- skip.sql --
+SKIP
+
+-- foo.txt --
+FOO
+
+-- skip.bat --
+SKIP
+`
+
+func TestGoToEmbedDefinition(t *testing.T) {
+ Run(t, embedDefinition, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+
+ start := env.RegexpSearch("main.go", `\*.txt`)
+ loc := env.GoToDefinition(start)
+
+ name := env.Sandbox.Workdir.URIToPath(loc.URI)
+ if want := "foo.txt"; name != want {
+ t.Errorf("GoToDefinition: got file %q, want %q", name, want)
+ }
+ })
+}
diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go
index 547755fd271..0cfcab59d24 100644
--- a/gopls/internal/regtest/misc/generate_test.go
+++ b/gopls/internal/regtest/misc/generate_test.go
@@ -27,12 +27,11 @@ go 1.14
package main
import (
- "io/ioutil"
"os"
)
func main() {
- ioutil.WriteFile("generated.go", []byte("package " + os.Args[1] + "\n\nconst Answer = 21"), 0644)
+ os.WriteFile("generated.go", []byte("package " + os.Args[1] + "\n\nconst Answer = 21"), 0644)
}
-- lib1/lib.go --
diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go
index 9b2d86ebb69..7b84f8aa871 100644
--- a/gopls/internal/regtest/misc/hover_test.go
+++ b/gopls/internal/regtest/misc/hover_test.go
@@ -435,3 +435,59 @@ use (
_, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("go.work", "modb"))
})
}
+
+const embedHover = `
+-- go.mod --
+module mod.com
+go 1.19
+-- main.go --
+package main
+
+import "embed"
+
+//go:embed *.txt
+var foo embed.FS
+
+func main() {
+}
+-- foo.txt --
+FOO
+-- bar.txt --
+BAR
+-- baz.txt --
+BAZ
+-- other.sql --
+SKIPPED
+-- dir.txt/skip.txt --
+SKIPPED
+`
+
+func TestHoverEmbedDirective(t *testing.T) {
+ testenv.NeedsGo1Point(t, 19)
+ Run(t, embedHover, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ from := env.RegexpSearch("main.go", `\*.txt`)
+
+ got, _ := env.Hover(from)
+ if got == nil {
+ t.Fatalf("hover over //go:embed arg not found")
+ }
+ content := got.Value
+
+ wants := []string{"foo.txt", "bar.txt", "baz.txt"}
+ for _, want := range wants {
+ if !strings.Contains(content, want) {
+ t.Errorf("hover: %q does not contain: %q", content, want)
+ }
+ }
+
+ // A directory should never be matched, even if it happens to have a matching name.
+ // Content in subdirectories should not match on only one asterisk.
+ skips := []string{"other.sql", "dir.txt", "skip.txt"}
+ for _, skip := range skips {
+ if strings.Contains(content, skip) {
+ t.Errorf("hover: %q should not contain: %q", content, skip)
+ }
+ }
+ })
+}
diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go
index 82c05a6319b..1e1d303379d 100644
--- a/gopls/internal/regtest/misc/imports_test.go
+++ b/gopls/internal/regtest/misc/imports_test.go
@@ -5,7 +5,6 @@
package misc
import (
- "io/ioutil"
"os"
"path/filepath"
"strings"
@@ -175,7 +174,7 @@ import "example.com/x"
var _, _ = x.X, y.Y
`
- modcache, err := ioutil.TempDir("", "TestGOMODCACHE-modcache")
+ modcache, err := os.MkdirTemp("", "TestGOMODCACHE-modcache")
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/regtest/misc/prompt_test.go b/gopls/internal/regtest/misc/prompt_test.go
new file mode 100644
index 00000000000..7a262ad934e
--- /dev/null
+++ b/gopls/internal/regtest/misc/prompt_test.go
@@ -0,0 +1,231 @@
+// 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 misc
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/lsp"
+ "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+)
+
+// Test that gopls prompts for telemetry only when it is supposed to.
+func TestTelemetryPrompt_Conditions(t *testing.T) {
+ const src = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- main.go --
+package main
+
+func main() {
+}
+`
+
+ for _, enabled := range []bool{true, false} {
+ t.Run(fmt.Sprintf("telemetryPrompt=%v", enabled), func(t *testing.T) {
+ for _, initialMode := range []string{"", "off", "on"} {
+ t.Run(fmt.Sprintf("initial_mode=%s", initialMode), func(t *testing.T) {
+ modeFile := filepath.Join(t.TempDir(), "mode")
+ if initialMode != "" {
+ if err := os.WriteFile(modeFile, []byte(initialMode), 0666); err != nil {
+ t.Fatal(err)
+ }
+ }
+ WithOptions(
+ Modes(Default), // no need to run this in all modes
+ EnvVars{
+ lsp.GoplsConfigDirEnvvar: t.TempDir(),
+ lsp.FakeTelemetryModefileEnvvar: modeFile,
+ },
+ Settings{
+ "telemetryPrompt": enabled,
+ },
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ wantPrompt := enabled && (initialMode == "" || initialMode == "off")
+ expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?")
+ if !wantPrompt {
+ expectation = Not(expectation)
+ }
+ env.OnceMet(
+ CompletedWork(lsp.TelemetryPromptWorkTitle, 1, true),
+ expectation,
+ )
+ })
+ })
+ }
+ })
+ }
+}
+
+// Test that responding to the telemetry prompt results in the expected state.
+func TestTelemetryPrompt_Response(t *testing.T) {
+ const src = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- main.go --
+package main
+
+func main() {
+}
+`
+
+ tests := []struct {
+ response string // response to choose for the telemetry dialog
+ wantMode string // resulting telemetry mode
+ wantMsg string // substring contained in the follow-up popup (if empty, no popup is expected)
+ }{
+ {lsp.TelemetryYes, "on", "uploading is now enabled"},
+ {lsp.TelemetryNo, "", ""},
+ {"", "", ""},
+ }
+ for _, test := range tests {
+ t.Run(fmt.Sprintf("response=%s", test.response), func(t *testing.T) {
+ modeFile := filepath.Join(t.TempDir(), "mode")
+ msgRE := regexp.MustCompile(".*Would you like to enable Go telemetry?")
+ respond := func(m *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
+ if msgRE.MatchString(m.Message) {
+ for _, item := range m.Actions {
+ if item.Title == test.response {
+ return &item, nil
+ }
+ }
+ if test.response != "" {
+ t.Errorf("action item %q not found", test.response)
+ }
+ }
+ return nil, nil
+ }
+ WithOptions(
+ Modes(Default), // no need to run this in all modes
+ EnvVars{
+ lsp.GoplsConfigDirEnvvar: t.TempDir(),
+ lsp.FakeTelemetryModefileEnvvar: modeFile,
+ },
+ Settings{
+ "telemetryPrompt": true,
+ },
+ MessageResponder(respond),
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ var postConditions []Expectation
+ if test.wantMsg != "" {
+ postConditions = append(postConditions, ShownMessage(test.wantMsg))
+ }
+ env.OnceMet(
+ CompletedWork(lsp.TelemetryPromptWorkTitle, 1, true),
+ postConditions...,
+ )
+ gotMode := ""
+ if contents, err := os.ReadFile(modeFile); err == nil {
+ gotMode = string(contents)
+ } else if !os.IsNotExist(err) {
+ t.Fatal(err)
+ }
+ if gotMode != test.wantMode {
+ t.Errorf("after prompt, mode=%s, want %s", gotMode, test.wantMode)
+ }
+ })
+ })
+ }
+}
+
+// Test that we stop asking about telemetry after the user ignores the question
+// 5 times.
+func TestTelemetryPrompt_GivingUp(t *testing.T) {
+ const src = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- main.go --
+package main
+
+func main() {
+}
+`
+
+ // For this test, we want to share state across gopls sessions.
+ modeFile := filepath.Join(t.TempDir(), "mode")
+ configDir := t.TempDir()
+
+ const maxPrompts = 5 // internal prompt limit defined by gopls
+
+ for i := 0; i < maxPrompts+1; i++ {
+ WithOptions(
+ Modes(Default), // no need to run this in all modes
+ EnvVars{
+ lsp.GoplsConfigDirEnvvar: configDir,
+ lsp.FakeTelemetryModefileEnvvar: modeFile,
+ },
+ Settings{
+ "telemetryPrompt": true,
+ },
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ wantPrompt := i < maxPrompts
+ expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?")
+ if !wantPrompt {
+ expectation = Not(expectation)
+ }
+ env.OnceMet(
+ CompletedWork(lsp.TelemetryPromptWorkTitle, 1, true),
+ expectation,
+ )
+ })
+ }
+}
+
+// Test that gopls prompts for telemetry only when it is supposed to.
+func TestTelemetryPrompt_Conditions2(t *testing.T) {
+ const src = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- main.go --
+package main
+
+func main() {
+}
+`
+ modeFile := filepath.Join(t.TempDir(), "mode")
+ WithOptions(
+ Modes(Default), // no need to run this in all modes
+ EnvVars{
+ lsp.GoplsConfigDirEnvvar: t.TempDir(),
+ lsp.FakeTelemetryModefileEnvvar: modeFile,
+ },
+ Settings{
+ // off because we are testing
+ // if we can trigger the prompt with command.
+ "telemetryPrompt": false,
+ },
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ cmd, err := command.NewMaybePromptForTelemetryCommand("prompt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var result error
+ env.ExecuteCommand(&protocol.ExecuteCommandParams{
+ Command: cmd.Command,
+ }, &result)
+ if result != nil {
+ t.Fatal(err)
+ }
+ expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?")
+ env.OnceMet(
+ CompletedWork(lsp.TelemetryPromptWorkTitle, 2, true),
+ expectation,
+ )
+ })
+}
diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go
index a85bcc27d61..262284abc3d 100644
--- a/gopls/internal/regtest/misc/references_test.go
+++ b/gopls/internal/regtest/misc/references_test.go
@@ -133,10 +133,8 @@ var _ = unsafe.Slice(nil, 0)
loc := env.RegexpSearch("a.go", `\b`+name+`\b`)
// definition -> {builtin,unsafe}.go
- def, err := env.Editor.GoToDefinition(env.Ctx, loc)
- if err != nil {
- t.Errorf("definition(%q) failed: %v", name, err)
- } else if (!strings.HasSuffix(string(def.URI), "builtin.go") &&
+ def := env.GoToDefinition(loc)
+ if (!strings.HasSuffix(string(def.URI), "builtin.go") &&
!strings.HasSuffix(string(def.URI), "unsafe.go")) ||
def.Range.Start.Line == 0 {
t.Errorf("definition(%q) = %v, want {builtin,unsafe}.go",
@@ -144,7 +142,7 @@ var _ = unsafe.Slice(nil, 0)
}
// "references to (builtin "Foo"|unsafe.Foo) are not supported"
- _, err = env.Editor.References(env.Ctx, loc)
+ _, err := env.Editor.References(env.Ctx, loc)
gotErr := fmt.Sprint(err)
if !strings.Contains(gotErr, "references to") ||
!strings.Contains(gotErr, "not supported") ||
diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go
index 41b2549bcb2..40baf8cb017 100644
--- a/gopls/internal/regtest/misc/vuln_test.go
+++ b/gopls/internal/regtest/misc/vuln_test.go
@@ -10,20 +10,19 @@ package misc
import (
"context"
"encoding/json"
- "path/filepath"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/protocol"
. "golang.org/x/tools/gopls/internal/lsp/regtest"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/tests/compare"
"golang.org/x/tools/gopls/internal/vulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/scan"
"golang.org/x/tools/gopls/internal/vulncheck/vulntest"
"golang.org/x/tools/internal/testenv"
)
@@ -86,7 +85,7 @@ func F() { // build error incomplete
env.Await(
CompletedProgress(result.Token, &ws),
)
- wantEndMsg, wantMsgPart := "failed", "failed to load packages due to errors"
+ wantEndMsg, wantMsgPart := "failed", "There are errors with the provided package patterns:"
if ws.EndMsg != "failed" || !strings.Contains(ws.Msg, wantMsgPart) {
t.Errorf("work status = %+v, want {EndMessage: %q, Message: %q}", ws, wantEndMsg, wantMsgPart)
}
@@ -100,14 +99,14 @@ modules:
versions:
- introduced: 1.0.0
- fixed: 1.0.4
- - introduced: 1.1.2
packages:
- package: golang.org/amod/avuln
symbols:
- VulnData.Vuln1
- VulnData.Vuln2
description: >
- vuln in amod
+ vuln in amod is found
+summary: vuln in amod
references:
- href: pkg.go.dev/vuln/GO-2022-01
-- GO-2022-03.yaml --
@@ -121,7 +120,8 @@ modules:
symbols:
- nonExisting
description: >
- unaffecting vulnerability
+ unaffecting vulnerability is found
+summary: unaffecting vulnerability
-- GO-2022-02.yaml --
modules:
- module: golang.org/bmod
@@ -130,10 +130,11 @@ modules:
symbols:
- Vuln
description: |
- vuln in bmod
+ vuln in bmod is found.
This is a long description
of this vulnerability.
+summary: vuln in bmod (no fix)
references:
- href: pkg.go.dev/vuln/GO-2022-03
-- GO-2022-04.yaml --
@@ -144,7 +145,8 @@ modules:
symbols:
- Vuln
description: |
- vuln in bmod/somtrhingelse
+ vuln in bmod/somethingelse is found
+summary: vuln in bmod/somethingelse
references:
- href: pkg.go.dev/vuln/GO-2022-04
-- GOSTDLIB.yaml --
@@ -156,6 +158,7 @@ modules:
- package: archive/zip
symbols:
- OpenReader
+summary: vuln in GOSTDLIB
references:
- href: pkg.go.dev/vuln/GOSTDLIB
`
@@ -193,7 +196,7 @@ func main() {
// When fetchinging stdlib package vulnerability info,
// behave as if our go version is go1.18 for this testing.
// The default behavior is to run `go env GOVERSION` (which isn't mutable env var).
- vulncheck.GoVersionForVulnTest: "go1.18",
+ scan.GoVersionForVulnTest: "go1.18",
"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
},
Settings{
@@ -233,7 +236,7 @@ func main() {
NoDiagnostics(ForFile("go.mod")),
)
testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
- "go.mod": {IDs: []string{"GOSTDLIB"}, Mode: govulncheck.ModeGovulncheck}})
+ "go.mod": {IDs: []string{"GOSTDLIB"}, Mode: vulncheck.ModeGovulncheck}})
})
}
@@ -269,7 +272,7 @@ func main() {
"GOVULNDB": db.URI(),
// When fetchinging stdlib package vulnerability info,
// behave as if our go version is go1.18 for this testing.
- vulncheck.GoVersionForVulnTest: "go1.18",
+ scan.GoVersionForVulnTest: "go1.18",
"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
},
Settings{"ui.diagnostic.vulncheck": "Imports"},
@@ -282,7 +285,7 @@ func main() {
testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
"go.mod": {
IDs: []string{"GOSTDLIB"},
- Mode: govulncheck.ModeImports,
+ Mode: vulncheck.ModeImports,
},
})
})
@@ -290,13 +293,13 @@ func main() {
type fetchVulncheckResult struct {
IDs []string
- Mode govulncheck.AnalysisMode
+ Mode vulncheck.AnalysisMode
}
func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulncheckResult) {
t.Helper()
- var result map[protocol.DocumentURI]*govulncheck.Result
+ var result map[protocol.DocumentURI]*vulncheck.Result
fetchCmd, err := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{
URI: env.Sandbox.Workdir.URI("go.mod"),
})
@@ -313,14 +316,18 @@ func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulnc
}
got := map[string]fetchVulncheckResult{}
for k, r := range result {
- var osv []string
- for _, v := range r.Vulns {
- osv = append(osv, v.OSV.ID)
+ osv := map[string]bool{}
+ for _, v := range r.Findings {
+ osv[v.OSV] = true
}
- sort.Strings(osv)
+ ids := make([]string, 0, len(osv))
+ for id := range osv {
+ ids = append(ids, id)
+ }
+ sort.Strings(ids)
modfile := env.Sandbox.Workdir.RelPath(k.SpanURI().Filename())
got[modfile] = fetchVulncheckResult{
- IDs: osv,
+ IDs: ids,
Mode: r.Mode,
}
}
@@ -466,7 +473,7 @@ func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) {
// When fetching stdlib package vulnerability info,
// behave as if our go version is go1.18 for this testing.
// The default behavior is to run `go env GOVERSION` (which isn't mutable env var).
- vulncheck.GoVersionForVulnTest: "go1.18",
+ scan.GoVersionForVulnTest: "go1.18",
"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
"GOSUMDB": "off",
}
@@ -494,7 +501,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) {
testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
"go.mod": {
IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"},
- Mode: govulncheck.ModeImports,
+ Mode: vulncheck.ModeImports,
},
})
@@ -533,7 +540,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) {
codeActions: []string{
"Run govulncheck to verify",
},
- hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
+ hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."},
},
}
@@ -645,12 +652,10 @@ func TestRunVulncheckWarning(t *testing.T) {
)
testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
- "go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, Mode: govulncheck.ModeGovulncheck},
+ "go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, Mode: vulncheck.ModeGovulncheck},
})
env.OpenFile("x/x.go")
- lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`).Range.Start
env.OpenFile("y/y.go")
- lineY := env.RegexpSearch("y/y.go", `c\.C2\(\)\(\)`).Range.Start
wantDiagnostics := map[string]vulnDiagExpectation{
"golang.org/amod": {
applyAction: "Upgrade to v1.0.6",
@@ -664,10 +669,6 @@ func TestRunVulncheckWarning(t *testing.T) {
"Upgrade to latest",
"Reset govulncheck result",
},
- relatedInfo: []vulnRelatedInfo{
- {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1
- {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln2
- },
},
{
msg: "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.",
@@ -696,15 +697,12 @@ func TestRunVulncheckWarning(t *testing.T) {
codeActions: []string{
"Reset govulncheck result", // no fix, but we should give an option to reset.
},
- relatedInfo: []vulnRelatedInfo{
- {"y.go", uint32(lineY.Line), "[GO-2022-02]"}, // bvuln.Vuln
- },
},
},
codeActions: []string{
"Reset govulncheck result", // no fix, but we should give an option to reset.
},
- hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
+ hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."},
},
}
@@ -810,7 +808,7 @@ func TestGovulncheckInfo(t *testing.T) {
ReadDiagnostics("go.mod", gotDiagnostics),
)
- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02"}, Mode: govulncheck.ModeGovulncheck}})
+ testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02"}, Mode: vulncheck.ModeGovulncheck}})
// wantDiagnostics maps a module path in the require
// section of a go.mod to diagnostics that will be returned
// when running vulncheck.
@@ -829,7 +827,7 @@ func TestGovulncheckInfo(t *testing.T) {
codeActions: []string{
"Reset govulncheck result",
},
- hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
+ hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."},
},
}
@@ -886,10 +884,6 @@ func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagEx
if diag.Severity != w.severity || diag.Source != w.source {
t.Errorf("incorrect (severity, source) for %q, want (%s, %s) got (%s, %s)\n", w.msg, w.severity, w.source, diag.Severity, diag.Source)
}
- sort.Slice(w.relatedInfo, func(i, j int) bool { return w.relatedInfo[i].less(w.relatedInfo[j]) })
- if got, want := summarizeRelatedInfo(diag.RelatedInformation), w.relatedInfo; !cmp.Equal(got, want) {
- t.Errorf("related info for %q do not match, want %v, got %v\n", w.msg, want, got)
- }
// Check expected code actions appear.
gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag})
if diff := diffCodeActions(gotActions, w.codeActions); diff != "" {
@@ -910,22 +904,6 @@ func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagEx
return modPathDiagnostics
}
-// summarizeRelatedInfo converts protocol.DiagnosticRelatedInformation to vulnRelatedInfo
-// that captures only the part that we want to test.
-func summarizeRelatedInfo(rinfo []protocol.DiagnosticRelatedInformation) []vulnRelatedInfo {
- var res []vulnRelatedInfo
- for _, r := range rinfo {
- filename := filepath.Base(r.Location.URI.SpanURI().Filename())
- message, _, _ := strings.Cut(r.Message, " ")
- line := r.Location.Range.Start.Line
- res = append(res, vulnRelatedInfo{filename, line, message})
- }
- sort.Slice(res, func(i, j int) bool {
- return res[i].less(res[j])
- })
- return res
-}
-
type vulnRelatedInfo struct {
Filename string
Line uint32
diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go
index 855141a7b30..076366958e6 100644
--- a/gopls/internal/regtest/modfile/modfile_test.go
+++ b/gopls/internal/regtest/modfile/modfile_test.go
@@ -878,7 +878,7 @@ func hello() {}
}
// Confirm that we no longer have metadata when the file is saved.
env.SaveBufferWithoutActions("go.mod")
- _, err := env.Editor.GoToDefinition(env.Ctx, env.RegexpSearch("main.go", "hello"))
+ _, err := env.Editor.Definition(env.Ctx, env.RegexpSearch("main.go", "hello"))
if err == nil {
t.Fatalf("expected error, got none")
}
diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go
index d7d54c4d139..baa6ec1384a 100644
--- a/gopls/internal/regtest/workspace/broken_test.go
+++ b/gopls/internal/regtest/workspace/broken_test.go
@@ -109,7 +109,7 @@ const CompleteMe = 222
./package2/vendor/example.com/foo
)
`)
- env.AfterChange(NoOutstandingWork())
+ env.AfterChange(NoOutstandingWork(IgnoreTelemetryPromptWork))
// Check that definitions in package1 go to the copy vendored in package2.
location := string(env.GoToDefinition(env.RegexpSearch("package1/main.go", "CompleteMe")).URI)
@@ -220,7 +220,7 @@ package b
env.Await(
NoDiagnostics(ForFile("a/a.go")),
NoDiagnostics(ForFile("b/go.mod")),
- NoOutstandingWork(),
+ NoOutstandingWork(IgnoreTelemetryPromptWork),
)
env.ChangeWorkspaceFolders(".")
@@ -256,7 +256,7 @@ package b
env.OpenFile("a/a.go")
env.AfterChange(
NoDiagnostics(ForFile("a/a.go")),
- NoOutstandingWork(),
+ NoOutstandingWork(IgnoreTelemetryPromptWork),
)
})
})
diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go
index c9ce2f02924..3e0ea40345d 100644
--- a/gopls/internal/regtest/workspace/standalone_test.go
+++ b/gopls/internal/regtest/workspace/standalone_test.go
@@ -198,11 +198,6 @@ func main() {}
"standaloneTags": []string{"ignore"},
}
env.ChangeConfiguration(cfg)
-
- // TODO(golang/go#56158): gopls does not purge previously published
- // diagnostice when configuration changes.
- env.RegexpReplace("ignore.go", "arbitrary", "meaningless")
-
env.AfterChange(
NoDiagnostics(ForFile("ignore.go")),
Diagnostics(env.AtRegexp("standalone.go", "package (main)")),
diff --git a/gopls/internal/telemetry/latency.go b/gopls/internal/telemetry/latency.go
new file mode 100644
index 00000000000..b0e2da73165
--- /dev/null
+++ b/gopls/internal/telemetry/latency.go
@@ -0,0 +1,102 @@
+// 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 telemetry
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sort"
+ "sync"
+ "time"
+
+ "golang.org/x/telemetry/counter"
+)
+
+// latencyKey is used for looking up latency counters.
+type latencyKey struct {
+ operation, bucket string
+ isError bool
+}
+
+var (
+ latencyBuckets = []struct {
+ end time.Duration
+ name string
+ }{
+ {10 * time.Millisecond, "<10ms"},
+ {50 * time.Millisecond, "<50ms"},
+ {100 * time.Millisecond, "<100ms"},
+ {200 * time.Millisecond, "<200ms"},
+ {500 * time.Millisecond, "<500ms"},
+ {1 * time.Second, "<1s"},
+ {5 * time.Second, "<5s"},
+ {24 * time.Hour, "<24h"},
+ }
+
+ latencyCounterMu sync.Mutex
+ latencyCounters = make(map[latencyKey]*counter.Counter) // lazily populated
+)
+
+// ForEachLatencyCounter runs the provided function for each current latency
+// counter measuring the given operation.
+//
+// Exported for testing.
+func ForEachLatencyCounter(operation string, isError bool, f func(*counter.Counter)) {
+ latencyCounterMu.Lock()
+ defer latencyCounterMu.Unlock()
+
+ for k, v := range latencyCounters {
+ if k.operation == operation && k.isError == isError {
+ f(v)
+ }
+ }
+}
+
+// getLatencyCounter returns the counter used to record latency of the given
+// operation in the given bucket.
+func getLatencyCounter(operation, bucket string, isError bool) *counter.Counter {
+ latencyCounterMu.Lock()
+ defer latencyCounterMu.Unlock()
+
+ key := latencyKey{operation, bucket, isError}
+ c, ok := latencyCounters[key]
+ if !ok {
+ var name string
+ if isError {
+ name = fmt.Sprintf("gopls/%s/error-latency:%s", operation, bucket)
+ } else {
+ name = fmt.Sprintf("gopls/%s/latency:%s", operation, bucket)
+ }
+ c = counter.New(name)
+ latencyCounters[key] = c
+ }
+ return c
+}
+
+// StartLatencyTimer starts a timer for the gopls operation with the given
+// name, and returns a func to stop the timer and record the latency sample.
+//
+// If the context provided to the the resulting func is done, no observation is
+// recorded.
+func StartLatencyTimer(operation string) func(context.Context, error) {
+ start := time.Now()
+ return func(ctx context.Context, err error) {
+ if errors.Is(ctx.Err(), context.Canceled) {
+ // Ignore timing where the operation is cancelled, it may be influenced
+ // by client behavior.
+ return
+ }
+ latency := time.Since(start)
+ bucketIdx := sort.Search(len(latencyBuckets), func(i int) bool {
+ bucket := latencyBuckets[i]
+ return latency < bucket.end
+ })
+ if bucketIdx < len(latencyBuckets) { // ignore latency longer than a day :)
+ bucketName := latencyBuckets[bucketIdx].name
+ getLatencyCounter(operation, bucketName, err != nil).Inc()
+ }
+ }
+}
diff --git a/gopls/internal/telemetry/telemetry.go b/gopls/internal/telemetry/telemetry.go
index db75e1a7fbf..dc6f7c23372 100644
--- a/gopls/internal/telemetry/telemetry.go
+++ b/gopls/internal/telemetry/telemetry.go
@@ -10,11 +10,22 @@ package telemetry
import (
"fmt"
+ "golang.org/x/telemetry"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/upload"
"golang.org/x/tools/gopls/internal/lsp/protocol"
)
+// Mode calls x/telemetry.Mode.
+func Mode() string {
+ return telemetry.Mode()
+}
+
+// SetMode calls x/telemetry.SetMode.
+func SetMode(mode string) error {
+ return telemetry.SetMode(mode)
+}
+
// Start starts telemetry instrumentation.
func Start() {
counter.Open()
@@ -68,3 +79,15 @@ func RecordViewGoVersion(x int) {
name := fmt.Sprintf("gopls/goversion:1.%d", x)
counter.Inc(name)
}
+
+// AddForwardedCounters adds the given counters on behalf of clients.
+// Names and values must have the same length.
+func AddForwardedCounters(names []string, values []int64) {
+ for i, n := range names {
+ v := values[i]
+ if n == "" || v < 0 {
+ continue // Should we report an error? Who is the audience?
+ }
+ counter.Add("fwd/"+n, v)
+ }
+}
diff --git a/gopls/internal/telemetry/telemetry_go118.go b/gopls/internal/telemetry/telemetry_go118.go
index b0c1197cb77..53394002f76 100644
--- a/gopls/internal/telemetry/telemetry_go118.go
+++ b/gopls/internal/telemetry/telemetry_go118.go
@@ -9,6 +9,14 @@ package telemetry
import "golang.org/x/tools/gopls/internal/lsp/protocol"
+func Mode() string {
+ return "local"
+}
+
+func SetMode(mode string) error {
+ return nil
+}
+
func Start() {
}
@@ -17,3 +25,6 @@ func RecordClientInfo(params *protocol.ParamInitialize) {
func RecordViewGoVersion(x int) {
}
+
+func AddForwardedCounters(names []string, values []int64) {
+}
diff --git a/gopls/internal/telemetry/telemetry_test.go b/gopls/internal/telemetry/telemetry_test.go
index 93751bff1d8..25e94f6284f 100644
--- a/gopls/internal/telemetry/telemetry_test.go
+++ b/gopls/internal/telemetry/telemetry_test.go
@@ -8,6 +8,8 @@
package telemetry_test
import (
+ "context"
+ "errors"
"os"
"strconv"
"strings"
@@ -18,7 +20,10 @@ import (
"golang.org/x/telemetry/counter/countertest" // requires go1.21+
"golang.org/x/tools/gopls/internal/bug"
"golang.org/x/tools/gopls/internal/hooks"
+ "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
. "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/telemetry"
)
func TestMain(m *testing.M) {
@@ -37,14 +42,37 @@ func TestTelemetry(t *testing.T) {
editor = "vscode" // We set ClientName("Visual Studio Code") below.
)
+ // Run gopls once to determine the Go version.
+ WithOptions(
+ Modes(Default),
+ ).Run(t, "", func(_ *testing.T, env *Env) {
+ goversion = strconv.Itoa(env.GoVersion())
+ })
+
+ // counters that should be incremented once per session
+ sessionCounters := []*counter.Counter{
+ counter.New("gopls/client:" + editor),
+ counter.New("gopls/goversion:1." + goversion),
+ counter.New("fwd/vscode/linter:a"),
+ }
+ initialCounts := make([]uint64, len(sessionCounters))
+ for i, c := range sessionCounters {
+ count, err := countertest.ReadCounter(c)
+ if err != nil {
+ t.Fatalf("ReadCounter(%s): %v", c.Name(), err)
+ }
+ initialCounts[i] = count
+ }
+
// Verify that a properly configured session gets notified of a bug on the
// server.
WithOptions(
Modes(Default), // must be in-process to receive the bug report below
Settings{"showBugReports": true},
ClientName("Visual Studio Code"),
- ).Run(t, "", func(t *testing.T, env *Env) {
+ ).Run(t, "", func(_ *testing.T, env *Env) {
goversion = strconv.Itoa(env.GoVersion())
+ addForwardedCounters(env, []string{"vscode/linter:a"}, []int64{1})
const desc = "got a bug"
bug.Report(desc) // want a stack counter with the trace starting from here.
env.Await(ShownMessage(desc))
@@ -52,13 +80,12 @@ func TestTelemetry(t *testing.T) {
// gopls/editor:client
// gopls/goversion:1.x
- for _, c := range []*counter.Counter{
- counter.New("gopls/client:" + editor),
- counter.New("gopls/goversion:1." + goversion),
- } {
- count, err := countertest.ReadCounter(c)
- if err != nil || count != 1 {
- t.Errorf("ReadCounter(%q) = (%v, %v), want (1, nil)", c.Name(), count, err)
+ // fwd/vscode/linter:a
+ for i, c := range sessionCounters {
+ want := initialCounts[i] + 1
+ got, err := countertest.ReadCounter(c)
+ if err != nil || got != want {
+ t.Errorf("ReadCounter(%q) = (%v, %v), want (%v, nil)", c.Name(), got, err, want)
t.Logf("Current timestamp = %v", time.Now().UTC())
}
}
@@ -75,6 +102,23 @@ func TestTelemetry(t *testing.T) {
}
}
+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)
+ }
+ var res error
+ env.ExecuteCommand(&protocol.ExecuteCommandParams{
+ Command: command.AddTelemetryCounters.ID(),
+ Arguments: args,
+ }, res)
+ if res != nil {
+ env.T.Errorf("%v failed - %v", command.AddTelemetryCounters.ID(), res)
+ }
+}
+
func hasEntry(counts map[string]uint64, pattern string, want uint64) bool {
for k, v := range counts {
if strings.Contains(k, pattern) && v == want {
@@ -83,3 +127,89 @@ func hasEntry(counts map[string]uint64, pattern string, want uint64) bool {
}
return false
}
+
+func TestLatencyCounter(t *testing.T) {
+ const operation = "TestLatencyCounter" // a unique operation name
+
+ stop := telemetry.StartLatencyTimer(operation)
+ stop(context.Background(), nil)
+
+ for isError, want := range map[bool]uint64{false: 1, true: 0} {
+ if got := totalLatencySamples(t, operation, isError); got != want {
+ t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want)
+ }
+ }
+}
+
+func TestLatencyCounter_Error(t *testing.T) {
+ const operation = "TestLatencyCounter_Error" // a unique operation name
+
+ stop := telemetry.StartLatencyTimer(operation)
+ stop(context.Background(), errors.New("bad"))
+
+ for isError, want := range map[bool]uint64{false: 0, true: 1} {
+ if got := totalLatencySamples(t, operation, isError); got != want {
+ t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want)
+ }
+ }
+}
+
+func TestLatencyCounter_Cancellation(t *testing.T) {
+ const operation = "TestLatencyCounter_Cancellation"
+
+ stop := telemetry.StartLatencyTimer(operation)
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+ stop(ctx, nil)
+
+ for isError, want := range map[bool]uint64{false: 0, true: 0} {
+ if got := totalLatencySamples(t, operation, isError); got != want {
+ t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want)
+ }
+ }
+}
+
+func totalLatencySamples(t *testing.T, operation string, isError bool) uint64 {
+ var total uint64
+ telemetry.ForEachLatencyCounter(operation, isError, func(c *counter.Counter) {
+ count, err := countertest.ReadCounter(c)
+ if err != nil {
+ t.Errorf("ReadCounter(%s) failed: %v", c.Name(), err)
+ } else {
+ total += count
+ }
+ })
+ return total
+}
+
+func TestLatencyInstrumentation(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.test/a
+go 1.18
+-- a.go --
+package a
+
+func _() {
+ x := 0
+ _ = x
+}
+`
+
+ // Verify that a properly configured session gets notified of a bug on the
+ // server.
+ WithOptions(
+ Modes(Default), // must be in-process to receive the bug report below
+ ).Run(t, files, func(_ *testing.T, env *Env) {
+ env.OpenFile("a.go")
+ before := totalLatencySamples(t, "completion", false)
+ loc := env.RegexpSearch("a.go", "x")
+ for i := 0; i < 10; i++ {
+ env.Completion(loc)
+ }
+ after := totalLatencySamples(t, "completion", false)
+ if after-before < 10 {
+ t.Errorf("after 10 completions, completion counter went from %d to %d", before, after)
+ }
+ })
+}
diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go
deleted file mode 100644
index 4a3d3d2dcc0..00000000000
--- a/gopls/internal/vulncheck/command.go
+++ /dev/null
@@ -1,337 +0,0 @@
-// Copyright 2022 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.18
-// +build go1.18
-
-package vulncheck
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "log"
- "os"
- "sort"
- "strings"
- "sync"
-
- "golang.org/x/mod/semver"
- "golang.org/x/sync/errgroup"
- "golang.org/x/tools/go/packages"
- "golang.org/x/tools/gopls/internal/govulncheck"
- "golang.org/x/tools/gopls/internal/lsp/source"
- "golang.org/x/vuln/client"
- gvcapi "golang.org/x/vuln/exp/govulncheck"
- "golang.org/x/vuln/osv"
- "golang.org/x/vuln/vulncheck"
-)
-
-func init() {
- VulnerablePackages = vulnerablePackages
-}
-
-func findGOVULNDB(env []string) []string {
- for _, kv := range env {
- if strings.HasPrefix(kv, "GOVULNDB=") {
- return strings.Split(kv[len("GOVULNDB="):], ",")
- }
- }
- if GOVULNDB := os.Getenv("GOVULNDB"); GOVULNDB != "" {
- return strings.Split(GOVULNDB, ",")
- }
- return []string{"https://vuln.go.dev"}
-}
-
-// GoVersionForVulnTest is an internal environment variable used in gopls
-// testing to examine govulncheck behavior with a go version different
-// than what `go version` returns in the system.
-const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION"
-
-func init() {
- Main = func(cfg packages.Config, patterns ...string) error {
- // Set the mode that Source needs.
- cfg.Mode = packages.NeedName | packages.NeedImports | packages.NeedTypes |
- packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
- packages.NeedModule
- logf := log.New(os.Stderr, "", log.Ltime).Printf
- logf("Loading packages...")
- pkgs, err := packages.Load(&cfg, patterns...)
- if err != nil {
- logf("Failed to load packages: %v", err)
- return err
- }
- if n := packages.PrintErrors(pkgs); n > 0 {
- err := errors.New("failed to load packages due to errors")
- logf("%v", err)
- return err
- }
- logf("Loaded %d packages and their dependencies", len(pkgs))
- cache, err := govulncheck.DefaultCache()
- if err != nil {
- return err
- }
- cli, err := client.NewClient(findGOVULNDB(cfg.Env), client.Options{
- HTTPCache: cache,
- })
- if err != nil {
- return err
- }
- res, err := gvcapi.Source(context.Background(), &gvcapi.Config{
- Client: cli,
- GoVersion: os.Getenv(GoVersionForVulnTest),
- }, vulncheck.Convert(pkgs))
- if err != nil {
- return err
- }
- affecting := 0
- for _, v := range res.Vulns {
- if v.IsCalled() {
- affecting++
- }
- }
- logf("Found %d affecting vulns and %d unaffecting vulns in imported packages", affecting, len(res.Vulns)-affecting)
- if err := json.NewEncoder(os.Stdout).Encode(res); err != nil {
- return err
- }
- return nil
- }
-}
-
-// semverToGoTag returns the Go standard library repository tag corresponding
-// to semver, a version string without the initial "v".
-// Go tags differ from standard semantic versions in a few ways,
-// such as beginning with "go" instead of "v".
-func semverToGoTag(v string) string {
- if strings.HasPrefix(v, "v0.0.0") {
- return "master"
- }
- // Special case: v1.0.0 => go1.
- if v == "v1.0.0" {
- return "go1"
- }
- if !semver.IsValid(v) {
- return fmt.Sprintf("", v)
- }
- goVersion := semver.Canonical(v)
- prerelease := semver.Prerelease(goVersion)
- versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease)
- patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".")
- if patch == "0" {
- versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0")
- }
- goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v"))
- if prerelease != "" {
- // Go prereleases look like "beta1" instead of "beta.1".
- // "beta1" is bad for sorting (since beta10 comes before beta9), so
- // require the dot form.
- i := finalDigitsIndex(prerelease)
- if i >= 1 {
- if prerelease[i-1] != '.' {
- return fmt.Sprintf("", v)
- }
- // Remove the dot.
- prerelease = prerelease[:i-1] + prerelease[i:]
- }
- goVersion += strings.TrimPrefix(prerelease, "-")
- }
- return goVersion
-}
-
-// finalDigitsIndex returns the index of the first digit in the sequence of digits ending s.
-// If s doesn't end in digits, it returns -1.
-func finalDigitsIndex(s string) int {
- // Assume ASCII (since the semver package does anyway).
- var i int
- for i = len(s) - 1; i >= 0; i-- {
- if s[i] < '0' || s[i] > '9' {
- break
- }
- }
- if i == len(s)-1 {
- return -1
- }
- return i + 1
-}
-
-// vulnerablePackages queries the vulndb and reports which vulnerabilities
-// apply to this snapshot. The result contains a set of packages,
-// grouped by vuln ID and by module.
-func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile source.FileHandle) (*govulncheck.Result, error) {
- // We want to report the intersection of vulnerable packages in the vulndb
- // and packages transitively imported by this module ('go list -deps all').
- // We use snapshot.AllMetadata to retrieve the list of packages
- // as an approximation.
- //
- // TODO(hyangah): snapshot.AllMetadata is a superset of
- // `go list all` - e.g. when the workspace has multiple main modules
- // (multiple go.mod files), that can include packages that are not
- // used by this module. Vulncheck behavior with go.work is not well
- // defined. Figure out the meaning, and if we decide to present
- // the result as if each module is analyzed independently, make
- // gopls track a separate build list for each module and use that
- // information instead of snapshot.AllMetadata.
- metadata, err := snapshot.AllMetadata(ctx)
- if err != nil {
- return nil, err
- }
-
- // TODO(hyangah): handle vulnerabilities in the standard library.
-
- // Group packages by modules since vuln db is keyed by module.
- metadataByModule := map[source.PackagePath][]*source.Metadata{}
- for _, md := range metadata {
- mi := md.Module
- modulePath := source.PackagePath("stdlib")
- if mi != nil {
- modulePath = source.PackagePath(mi.Path)
- }
- metadataByModule[modulePath] = append(metadataByModule[modulePath], md)
- }
-
- // Request vuln entries from remote service.
- fsCache, err := govulncheck.DefaultCache()
- if err != nil {
- return nil, err
- }
- cli, err := client.NewClient(
- findGOVULNDB(snapshot.Options().EnvSlice()),
- client.Options{HTTPCache: govulncheck.NewInMemoryCache(fsCache)})
- if err != nil {
- return nil, err
- }
- // Keys are osv.Entry.IDs
- vulnsResult := map[string]*govulncheck.Vuln{}
- var (
- group errgroup.Group
- mu sync.Mutex
- )
-
- goVersion := snapshot.Options().Env[GoVersionForVulnTest]
- if goVersion == "" {
- goVersion = snapshot.View().GoVersionString()
- }
- group.SetLimit(10)
- stdlibModule := &packages.Module{
- Path: "stdlib",
- Version: goVersion,
- }
- for path, mds := range metadataByModule {
- path, mds := path, mds
- group.Go(func() error {
- effectiveModule := stdlibModule
- if m := mds[0].Module; m != nil {
- effectiveModule = m
- }
- for effectiveModule.Replace != nil {
- effectiveModule = effectiveModule.Replace
- }
- ver := effectiveModule.Version
-
- // TODO(go.dev/issues/56312): batch these requests for efficiency.
- vulns, err := cli.GetByModule(ctx, effectiveModule.Path)
- if err != nil {
- return err
- }
- if len(vulns) == 0 { // No known vulnerability.
- return nil
- }
-
- // set of packages in this module known to gopls.
- // This will be lazily initialized when we need it.
- var knownPkgs map[source.PackagePath]bool
-
- // Report vulnerabilities that affect packages of this module.
- for _, entry := range vulns {
- var vulnerablePkgs []*govulncheck.Package
-
- for _, a := range entry.Affected {
- if a.Package.Ecosystem != osv.GoEcosystem || a.Package.Name != effectiveModule.Path {
- continue
- }
- if !a.Ranges.AffectsSemver(ver) {
- continue
- }
- for _, imp := range a.EcosystemSpecific.Imports {
- if knownPkgs == nil {
- knownPkgs = toPackagePathSet(mds)
- }
- if knownPkgs[source.PackagePath(imp.Path)] {
- vulnerablePkgs = append(vulnerablePkgs, &govulncheck.Package{
- Path: imp.Path,
- })
- }
- }
- }
- if len(vulnerablePkgs) == 0 {
- continue
- }
- mu.Lock()
- vuln, ok := vulnsResult[entry.ID]
- if !ok {
- vuln = &govulncheck.Vuln{OSV: entry}
- vulnsResult[entry.ID] = vuln
- }
- vuln.Modules = append(vuln.Modules, &govulncheck.Module{
- Path: string(path),
- FoundVersion: ver,
- FixedVersion: fixedVersion(effectiveModule.Path, entry.Affected),
- Packages: vulnerablePkgs,
- })
- mu.Unlock()
- }
- return nil
- })
- }
- if err := group.Wait(); err != nil {
- return nil, err
- }
-
- vulns := make([]*govulncheck.Vuln, 0, len(vulnsResult))
- for _, v := range vulnsResult {
- vulns = append(vulns, v)
- }
- // Sort so the results are deterministic.
- sort.Slice(vulns, func(i, j int) bool {
- return vulns[i].OSV.ID < vulns[j].OSV.ID
- })
- ret := &govulncheck.Result{
- Vulns: vulns,
- Mode: govulncheck.ModeImports,
- }
- return ret, nil
-}
-
-// toPackagePathSet transforms the metadata to a set of package paths.
-func toPackagePathSet(mds []*source.Metadata) map[source.PackagePath]bool {
- pkgPaths := make(map[source.PackagePath]bool, len(mds))
- for _, md := range mds {
- pkgPaths[md.PkgPath] = true
- }
- return pkgPaths
-}
-
-func fixedVersion(modulePath string, affected []osv.Affected) string {
- fixed := govulncheck.LatestFixed(modulePath, affected)
- if fixed != "" {
- fixed = versionString(modulePath, fixed)
- }
- return fixed
-}
-
-// versionString prepends a version string prefix (`v` or `go`
-// depending on the modulePath) to the given semver-style version string.
-func versionString(modulePath, version string) string {
- if version == "" {
- return ""
- }
- v := "v" + version
- // These are internal Go module paths used by the vuln DB
- // when listing vulns in standard library and the go command.
- if modulePath == "stdlib" || modulePath == "toolchain" {
- return semverToGoTag(v)
- }
- return v
-}
diff --git a/gopls/internal/vulncheck/copier.go b/gopls/internal/vulncheck/copier.go
new file mode 100644
index 00000000000..ade5a5f6be2
--- /dev/null
+++ b/gopls/internal/vulncheck/copier.go
@@ -0,0 +1,142 @@
+// 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.
+
+//go:build ignore
+// +build ignore
+
+//go:generate go run ./copier.go
+
+// Copier is a tool to automate copy of govulncheck's internal files.
+//
+// - copy golang.org/x/vuln/internal/osv/ to osv
+// - copy golang.org/x/vuln/internal/govulncheck/ to govulncheck
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "go/parser"
+ "go/token"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "golang.org/x/tools/internal/edit"
+)
+
+func main() {
+ log.SetPrefix("copier: ")
+ log.SetFlags(log.Lshortfile)
+
+ srcMod := "golang.org/x/vuln"
+ srcModVers := "@latest"
+ srcDir, srcVer := downloadModule(srcMod + srcModVers)
+
+ cfg := rewrite{
+ banner: fmt.Sprintf("// Code generated by copying from %v@%v (go run copier.go); DO NOT EDIT.", srcMod, srcVer),
+ srcImportPath: "golang.org/x/vuln/internal",
+ dstImportPath: currentPackagePath(),
+ }
+
+ copyFiles("osv", filepath.Join(srcDir, "internal", "osv"), cfg)
+ copyFiles("govulncheck", filepath.Join(srcDir, "internal", "govulncheck"), cfg)
+}
+
+type rewrite struct {
+ // DO NOT EDIT marker to add at the beginning
+ banner string
+ // rewrite srcImportPath with dstImportPath
+ srcImportPath string
+ dstImportPath string
+}
+
+func copyFiles(dst, src string, cfg rewrite) {
+ entries, err := os.ReadDir(src)
+ if err != nil {
+ log.Fatalf("failed to read dir: %v", err)
+ }
+ if err := os.MkdirAll(dst, 0777); err != nil {
+ log.Fatalf("failed to create dir: %v", err)
+ }
+
+ for _, e := range entries {
+ fname := e.Name()
+ // we need only non-test go files.
+ if e.IsDir() || !strings.HasSuffix(fname, ".go") || strings.HasSuffix(fname, "_test.go") {
+ continue
+ }
+ data, err := os.ReadFile(filepath.Join(src, fname))
+ if err != nil {
+ log.Fatal(err)
+ }
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, fname, data, parser.ParseComments|parser.ImportsOnly)
+ if err != nil {
+ log.Fatalf("parsing source module:\n%s", err)
+ }
+
+ buf := edit.NewBuffer(data)
+ at := func(p token.Pos) int {
+ return fset.File(p).Offset(p)
+ }
+
+ // Add banner right after the copyright statement (the first comment)
+ bannerInsert, banner := f.FileStart, cfg.banner
+ if len(f.Comments) > 0 && strings.HasPrefix(f.Comments[0].Text(), "Copyright ") {
+ bannerInsert = f.Comments[0].End()
+ banner = "\n\n" + banner
+ }
+ buf.Replace(at(bannerInsert), at(bannerInsert), banner)
+
+ // Adjust imports
+ for _, spec := range f.Imports {
+ path, err := strconv.Unquote(spec.Path.Value)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if strings.HasPrefix(path, cfg.srcImportPath) {
+ newPath := strings.Replace(path, cfg.srcImportPath, cfg.dstImportPath, 1)
+ buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(newPath))
+ }
+ }
+ data = buf.Bytes()
+
+ if err := os.WriteFile(filepath.Join(dst, fname), data, 0666); err != nil {
+ log.Fatal(err)
+ }
+ }
+}
+
+func downloadModule(srcModVers string) (dir, ver string) {
+ var stdout, stderr bytes.Buffer
+ cmd := exec.Command("go", "mod", "download", "-json", srcModVers)
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ if err := cmd.Run(); err != nil {
+ log.Fatalf("go mod download -json %s: %v\n%s%s", srcModVers, err, stderr.Bytes(), stdout.Bytes())
+ }
+ var info struct {
+ Dir string
+ Version string
+ }
+ if err := json.Unmarshal(stdout.Bytes(), &info); err != nil {
+ log.Fatalf("go mod download -json %s: invalid JSON output: %v\n%s%s", srcModVers, err, stderr.Bytes(), stdout.Bytes())
+ }
+ return info.Dir, info.Version
+}
+
+func currentPackagePath() string {
+ var stdout, stderr bytes.Buffer
+ cmd := exec.Command("go", "list", ".")
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ if err := cmd.Run(); err != nil {
+ log.Fatalf("go list: %v\n%s%s", err, stderr.Bytes(), stdout.Bytes())
+ }
+ return strings.TrimSpace(stdout.String())
+}
diff --git a/gopls/internal/vulncheck/govulncheck/govulncheck.go b/gopls/internal/vulncheck/govulncheck/govulncheck.go
new file mode 100644
index 00000000000..fd0390703ae
--- /dev/null
+++ b/gopls/internal/vulncheck/govulncheck/govulncheck.go
@@ -0,0 +1,160 @@
+// 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.
+
+// Code generated by copying from golang.org/x/vuln@v1.0.1 (go run copier.go); DO NOT EDIT.
+
+// Package govulncheck contains the JSON output structs for govulncheck.
+package govulncheck
+
+import (
+ "time"
+
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
+)
+
+const (
+ // ProtocolVersion is the current protocol version this file implements
+ ProtocolVersion = "v1.0.0"
+)
+
+// Message is an entry in the output stream. It will always have exactly one
+// field filled in.
+type Message struct {
+ Config *Config `json:"config,omitempty"`
+ Progress *Progress `json:"progress,omitempty"`
+ OSV *osv.Entry `json:"osv,omitempty"`
+ Finding *Finding `json:"finding,omitempty"`
+}
+
+// Config must occur as the first message of a stream and informs the client
+// about the information used to generate the findings.
+// The only required field is the protocol version.
+type Config struct {
+ // ProtocolVersion specifies the version of the JSON protocol.
+ ProtocolVersion string `json:"protocol_version"`
+
+ // ScannerName is the name of the tool, for example, govulncheck.
+ //
+ // We expect this JSON format to be used by other tools that wrap
+ // govulncheck, which will have a different name.
+ ScannerName string `json:"scanner_name,omitempty"`
+
+ // ScannerVersion is the version of the tool.
+ ScannerVersion string `json:"scanner_version,omitempty"`
+
+ // DB is the database used by the tool, for example,
+ // vuln.go.dev.
+ DB string `json:"db,omitempty"`
+
+ // LastModified is the last modified time of the data source.
+ DBLastModified *time.Time `json:"db_last_modified,omitempty"`
+
+ // GoVersion is the version of Go used for analyzing standard library
+ // vulnerabilities.
+ GoVersion string `json:"go_version,omitempty"`
+
+ // ScanLevel instructs govulncheck to analyze at a specific level of detail.
+ // Valid values include module, package and symbol.
+ ScanLevel ScanLevel `json:"scan_level,omitempty"`
+}
+
+// Progress messages are informational only, intended to allow users to monitor
+// the progress of a long running scan.
+// A stream must remain fully valid and able to be interpreted with all progress
+// messages removed.
+type Progress struct {
+ // A time stamp for the message.
+ Timestamp *time.Time `json:"time,omitempty"`
+
+ // Message is the progress message.
+ Message string `json:"message,omitempty"`
+}
+
+// Vuln represents a single OSV entry.
+type Finding struct {
+ // OSV is the id of the detected vulnerability.
+ OSV string `json:"osv,omitempty"`
+
+ // FixedVersion is the module version where the vulnerability was
+ // fixed. This is empty if a fix is not available.
+ //
+ // If there are multiple fixed versions in the OSV report, this will
+ // be the fixed version in the latest range event for the OSV report.
+ //
+ // For example, if the range events are
+ // {introduced: 0, fixed: 1.0.0} and {introduced: 1.1.0}, the fixed version
+ // will be empty.
+ //
+ // For the stdlib, we will show the fixed version closest to the
+ // Go version that is used. For example, if a fix is available in 1.17.5 and
+ // 1.18.5, and the GOVERSION is 1.17.3, 1.17.5 will be returned as the
+ // fixed version.
+ FixedVersion string `json:"fixed_version,omitempty"`
+
+ // Trace contains an entry for each frame in the trace.
+ //
+ // Frames are sorted starting from the imported vulnerable symbol
+ // until the entry point. The first frame in Frames should match
+ // Symbol.
+ //
+ // In binary mode, trace will contain a single-frame with no position
+ // information.
+ //
+ // When a package is imported but no vulnerable symbol is called, the trace
+ // will contain a single-frame with no symbol or position information.
+ Trace []*Frame `json:"trace,omitempty"`
+}
+
+// Frame represents an entry in a finding trace.
+type Frame struct {
+ // Module is the module path of the module containing this symbol.
+ //
+ // Importable packages in the standard library will have the path "stdlib".
+ Module string `json:"module"`
+
+ // Version is the module version from the build graph.
+ Version string `json:"version,omitempty"`
+
+ // Package is the import path.
+ Package string `json:"package,omitempty"`
+
+ // Function is the function name.
+ Function string `json:"function,omitempty"`
+
+ // Receiver is the receiver type if the called symbol is a method.
+ //
+ // The client can create the final symbol name by
+ // prepending Receiver to FuncName.
+ Receiver string `json:"receiver,omitempty"`
+
+ // Position describes an arbitrary source position
+ // including the file, line, and column location.
+ // A Position is valid if the line number is > 0.
+ Position *Position `json:"position,omitempty"`
+}
+
+// Position represents arbitrary source position.
+type Position struct {
+ Filename string `json:"filename,omitempty"` // filename, if any
+ Offset int `json:"offset"` // byte offset, starting at 0
+ Line int `json:"line"` // line number, starting at 1
+ Column int `json:"column"` // column number, starting at 1 (byte count)
+}
+
+// ScanLevel represents the detail level at which a scan occurred.
+// This can be necessary to correctly interpret the findings, for instance if
+// a scan is at symbol level and a finding does not have a symbol it means the
+// vulnerability was imported but not called. If the scan however was at
+// "package" level, that determination cannot be made.
+type ScanLevel string
+
+const (
+ scanLevelModule = "module"
+ scanLevelPackage = "package"
+ scanLevelSymbol = "symbol"
+)
+
+// WantSymbols can be used to check whether the scan level is one that is able
+// to generate symbols called findings.
+func (l ScanLevel) WantSymbols() bool { return l == scanLevelSymbol }
diff --git a/gopls/internal/vulncheck/govulncheck/handler.go b/gopls/internal/vulncheck/govulncheck/handler.go
new file mode 100644
index 00000000000..4100910a3c3
--- /dev/null
+++ b/gopls/internal/vulncheck/govulncheck/handler.go
@@ -0,0 +1,61 @@
+// 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.
+
+// Code generated by copying from golang.org/x/vuln@v1.0.1 (go run copier.go); DO NOT EDIT.
+
+package govulncheck
+
+import (
+ "encoding/json"
+ "io"
+
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
+)
+
+// Handler handles messages to be presented in a vulnerability scan output
+// stream.
+type Handler interface {
+ // Config communicates introductory message to the user.
+ Config(config *Config) error
+
+ // Progress is called to display a progress message.
+ Progress(progress *Progress) error
+
+ // OSV is invoked for each osv Entry in the stream.
+ OSV(entry *osv.Entry) error
+
+ // Finding is called for each vulnerability finding in the stream.
+ Finding(finding *Finding) error
+}
+
+// HandleJSON reads the json from the supplied stream and hands the decoded
+// output to the handler.
+func HandleJSON(from io.Reader, to Handler) error {
+ dec := json.NewDecoder(from)
+ for dec.More() {
+ msg := Message{}
+ // decode the next message in the stream
+ if err := dec.Decode(&msg); err != nil {
+ return err
+ }
+ // dispatch the message
+ var err error
+ if msg.Config != nil {
+ err = to.Config(msg.Config)
+ }
+ if msg.Progress != nil {
+ err = to.Progress(msg.Progress)
+ }
+ if msg.OSV != nil {
+ err = to.OSV(msg.OSV)
+ }
+ if msg.Finding != nil {
+ err = to.Finding(msg.Finding)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/gopls/internal/vulncheck/govulncheck/jsonhandler.go b/gopls/internal/vulncheck/govulncheck/jsonhandler.go
new file mode 100644
index 00000000000..eb110a2aee9
--- /dev/null
+++ b/gopls/internal/vulncheck/govulncheck/jsonhandler.go
@@ -0,0 +1,46 @@
+// Copyright 2022 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.
+
+// Code generated by copying from golang.org/x/vuln@v1.0.1 (go run copier.go); DO NOT EDIT.
+
+package govulncheck
+
+import (
+ "encoding/json"
+
+ "io"
+
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
+)
+
+type jsonHandler struct {
+ enc *json.Encoder
+}
+
+// NewJSONHandler returns a handler that writes govulncheck output as json.
+func NewJSONHandler(w io.Writer) Handler {
+ enc := json.NewEncoder(w)
+ enc.SetIndent("", " ")
+ return &jsonHandler{enc: enc}
+}
+
+// Config writes config block in JSON to the underlying writer.
+func (h *jsonHandler) Config(config *Config) error {
+ return h.enc.Encode(Message{Config: config})
+}
+
+// Progress writes a progress message in JSON to the underlying writer.
+func (h *jsonHandler) Progress(progress *Progress) error {
+ return h.enc.Encode(Message{Progress: progress})
+}
+
+// OSV writes an osv entry in JSON to the underlying writer.
+func (h *jsonHandler) OSV(entry *osv.Entry) error {
+ return h.enc.Encode(Message{OSV: entry})
+}
+
+// Finding writes a finding in JSON to the underlying writer.
+func (h *jsonHandler) Finding(finding *Finding) error {
+ return h.enc.Encode(Message{Finding: finding})
+}
diff --git a/gopls/internal/vulncheck/osv/osv.go b/gopls/internal/vulncheck/osv/osv.go
new file mode 100644
index 00000000000..08e18abf87d
--- /dev/null
+++ b/gopls/internal/vulncheck/osv/osv.go
@@ -0,0 +1,240 @@
+// 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.
+
+// Code generated by copying from golang.org/x/vuln@v1.0.1 (go run copier.go); DO NOT EDIT.
+
+// Package osv implements the Go OSV vulnerability format
+// (https://go.dev/security/vuln/database#schema), which is a subset of
+// the OSV shared vulnerability format
+// (https://ossf.github.io/osv-schema), with database and
+// ecosystem-specific meanings and fields.
+//
+// As this package is intended for use with the Go vulnerability
+// database, only the subset of features which are used by that
+// database are implemented (for instance, only the SEMVER affected
+// range type is implemented).
+package osv
+
+import "time"
+
+// RangeType specifies the type of version range being recorded and
+// defines the interpretation of the RangeEvent object's Introduced
+// and Fixed fields.
+//
+// In this implementation, only the "SEMVER" type is supported.
+//
+// See https://ossf.github.io/osv-schema/#affectedrangestype-field.
+type RangeType string
+
+// RangeTypeSemver indicates a semantic version as defined by
+// SemVer 2.0.0, with no leading "v" prefix.
+const RangeTypeSemver RangeType = "SEMVER"
+
+// Ecosystem identifies the overall library ecosystem.
+// In this implementation, only the "Go" ecosystem is supported.
+type Ecosystem string
+
+// GoEcosystem indicates the Go ecosystem.
+const GoEcosystem Ecosystem = "Go"
+
+// Pseudo-module paths used to describe vulnerabilities
+// in the Go standard library and toolchain.
+const (
+ // GoStdModulePath is the pseudo-module path string used
+ // to describe vulnerabilities in the Go standard library.
+ GoStdModulePath = "stdlib"
+ // GoCmdModulePath is the pseudo-module path string used
+ // to describe vulnerabilities in the go command.
+ GoCmdModulePath = "toolchain"
+)
+
+// Module identifies the Go module containing the vulnerability.
+// Note that this field is called "package" in the OSV specification.
+//
+// See https://ossf.github.io/osv-schema/#affectedpackage-field.
+type Module struct {
+ // The Go module path. Required.
+ // For the Go standard library, this is "stdlib".
+ // For the Go toolchain, this is "toolchain."
+ Path string `json:"name"`
+ // The ecosystem containing the module. Required.
+ // This should always be "Go".
+ Ecosystem Ecosystem `json:"ecosystem"`
+}
+
+// RangeEvent describes a single module version that either
+// introduces or fixes a vulnerability.
+//
+// Exactly one of Introduced and Fixed must be present. Other range
+// event types (e.g, "last_affected" and "limit") are not supported in
+// this implementation.
+//
+// See https://ossf.github.io/osv-schema/#affectedrangesevents-fields.
+type RangeEvent struct {
+ // Introduced is a version that introduces the vulnerability.
+ // A special value, "0", represents a version that sorts before
+ // any other version, and should be used to indicate that the
+ // vulnerability exists from the "beginning of time".
+ Introduced string `json:"introduced,omitempty"`
+ // Fixed is a version that fixes the vulnerability.
+ Fixed string `json:"fixed,omitempty"`
+}
+
+// Range describes the affected versions of the vulnerable module.
+//
+// See https://ossf.github.io/osv-schema/#affectedranges-field.
+type Range struct {
+ // Type is the version type that should be used to interpret the
+ // versions in Events. Required.
+ // In this implementation, only the "SEMVER" type is supported.
+ Type RangeType `json:"type"`
+ // Events is a list of versions representing the ranges in which
+ // the module is vulnerable. Required.
+ // The events should be sorted, and MUST represent non-overlapping
+ // ranges.
+ // There must be at least one RangeEvent containing a value for
+ // Introduced.
+ // See https://ossf.github.io/osv-schema/#examples for examples.
+ Events []RangeEvent `json:"events"`
+}
+
+// Reference type is a reference (link) type.
+type ReferenceType string
+
+const (
+ // ReferenceTypeAdvisory is a published security advisory for
+ // the vulnerability.
+ ReferenceTypeAdvisory = ReferenceType("ADVISORY")
+ // ReferenceTypeArticle is an article or blog post describing the vulnerability.
+ ReferenceTypeArticle = ReferenceType("ARTICLE")
+ // ReferenceTypeReport is a report, typically on a bug or issue tracker, of
+ // the vulnerability.
+ ReferenceTypeReport = ReferenceType("REPORT")
+ // ReferenceTypeFix is a source code browser link to the fix (e.g., a GitHub commit).
+ ReferenceTypeFix = ReferenceType("FIX")
+ // ReferenceTypePackage is a home web page for the package.
+ ReferenceTypePackage = ReferenceType("PACKAGE")
+ // ReferenceTypeEvidence is a demonstration of the validity of a vulnerability claim.
+ ReferenceTypeEvidence = ReferenceType("EVIDENCE")
+ // ReferenceTypeWeb is a web page of some unspecified kind.
+ ReferenceTypeWeb = ReferenceType("WEB")
+)
+
+// Reference is a reference URL containing additional information,
+// advisories, issue tracker entries, etc., about the vulnerability.
+//
+// See https://ossf.github.io/osv-schema/#references-field.
+type Reference struct {
+ // The type of reference. Required.
+ Type ReferenceType `json:"type"`
+ // The fully-qualified URL of the reference. Required.
+ URL string `json:"url"`
+}
+
+// Affected gives details about a module affected by the vulnerability.
+//
+// See https://ossf.github.io/osv-schema/#affected-fields.
+type Affected struct {
+ // The affected Go module. Required.
+ // Note that this field is called "package" in the OSV specification.
+ Module Module `json:"package"`
+ // The module version ranges affected by the vulnerability.
+ Ranges []Range `json:"ranges,omitempty"`
+ // Details on the affected packages and symbols within the module.
+ EcosystemSpecific EcosystemSpecific `json:"ecosystem_specific"`
+}
+
+// Package contains additional information about an affected package.
+// This is an ecosystem-specific field for the Go ecosystem.
+type Package struct {
+ // Path is the package import path. Required.
+ Path string `json:"path,omitempty"`
+ // GOOS is the execution operating system where the symbols appear, if
+ // known.
+ GOOS []string `json:"goos,omitempty"`
+ // GOARCH specifies the execution architecture where the symbols appear, if
+ // known.
+ GOARCH []string `json:"goarch,omitempty"`
+ // Symbols is a list of function and method names affected by
+ // this vulnerability. Methods are listed as ..
+ //
+ // If included, only programs which use these symbols will be marked as
+ // vulnerable by `govulncheck`. If omitted, any program which imports this
+ // package will be marked vulnerable.
+ Symbols []string `json:"symbols,omitempty"`
+}
+
+// EcosystemSpecific contains additional information about the vulnerable
+// module for the Go ecosystem.
+//
+// See https://go.dev/security/vuln/database#schema.
+type EcosystemSpecific struct {
+ // Packages is the list of affected packages within the module.
+ Packages []Package `json:"imports,omitempty"`
+}
+
+// Entry represents a vulnerability in the Go OSV format, documented
+// in https://go.dev/security/vuln/database#schema.
+// It is a subset of the OSV schema (https://ossf.github.io/osv-schema).
+// Only fields that are published in the Go Vulnerability Database
+// are supported.
+type Entry struct {
+ // SchemaVersion is the OSV schema version used to encode this
+ // vulnerability.
+ SchemaVersion string `json:"schema_version,omitempty"`
+ // ID is a unique identifier for the vulnerability. Required.
+ // The Go vulnerability database issues IDs of the form
+ // GO--.
+ ID string `json:"id"`
+ // Modified is the time the entry was last modified. Required.
+ Modified time.Time `json:"modified,omitempty"`
+ // Published is the time the entry should be considered to have
+ // been published.
+ Published time.Time `json:"published,omitempty"`
+ // Withdrawn is the time the entry should be considered to have
+ // been withdrawn. If the field is missing, then the entry has
+ // not been withdrawn.
+ Withdrawn *time.Time `json:"withdrawn,omitempty"`
+ // Aliases is a list of IDs for the same vulnerability in other
+ // databases.
+ Aliases []string `json:"aliases,omitempty"`
+ // Summary gives a one-line, English textual summary of the vulnerability.
+ // It is recommended that this field be kept short, on the order of no more
+ // than 120 characters.
+ Summary string `json:"summary,omitempty"`
+ // Details contains additional English textual details about the vulnerability.
+ Details string `json:"details"`
+ // Affected contains information on the modules and versions
+ // affected by the vulnerability.
+ Affected []Affected `json:"affected"`
+ // References contains links to more information about the
+ // vulnerability.
+ References []Reference `json:"references,omitempty"`
+ // Credits contains credits to entities that helped find or fix the
+ // vulnerability.
+ Credits []Credit `json:"credits,omitempty"`
+ // DatabaseSpecific contains additional information about the
+ // vulnerability, specific to the Go vulnerability database.
+ DatabaseSpecific *DatabaseSpecific `json:"database_specific,omitempty"`
+}
+
+// Credit represents a credit for the discovery, confirmation, patch, or
+// other event in the life cycle of a vulnerability.
+//
+// See https://ossf.github.io/osv-schema/#credits-fields.
+type Credit struct {
+ // Name is the name, label, or other identifier of the individual or
+ // entity being credited. Required.
+ Name string `json:"name"`
+}
+
+// DatabaseSpecific contains additional information about the
+// vulnerability, specific to the Go vulnerability database.
+//
+// See https://go.dev/security/vuln/database#schema.
+type DatabaseSpecific struct {
+ // The URL of the Go advisory for this vulnerability, of the form
+ // "https://pkg.go.dev/GO-YYYY-XXXX".
+ URL string `json:"url,omitempty"`
+}
diff --git a/gopls/internal/vulncheck/scan/command.go b/gopls/internal/vulncheck/scan/command.go
new file mode 100644
index 00000000000..89d24e08b71
--- /dev/null
+++ b/gopls/internal/vulncheck/scan/command.go
@@ -0,0 +1,476 @@
+// Copyright 2022 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.18
+// +build go1.18
+
+package scan
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/mod/semver"
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/vulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/govulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
+ isem "golang.org/x/tools/gopls/internal/vulncheck/semver"
+ "golang.org/x/vuln/scan"
+)
+
+// GoVersionForVulnTest is an internal environment variable used in gopls
+// testing to examine govulncheck behavior with a go version different
+// than what `go version` returns in the system.
+const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION"
+
+// Main implements gopls vulncheck.
+func Main(ctx context.Context, args ...string) error {
+ // wrapping govulncheck.
+ cmd := scan.Command(ctx, args...)
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ return cmd.Wait()
+}
+
+// RunGovulncheck implements the codelens "Run Govulncheck"
+// that runs 'gopls vulncheck' and converts the output to gopls's internal data
+// used for diagnostics and hover message construction.
+func RunGovulncheck(ctx context.Context, pattern string, snapshot source.Snapshot, dir string, log io.Writer) (*vulncheck.Result, error) {
+ vulncheckargs := []string{
+ "vulncheck", "--",
+ "-json",
+ "-mode", "source",
+ "-scan", "symbol",
+ }
+ if dir != "" {
+ vulncheckargs = append(vulncheckargs, "-C", dir)
+ }
+ if db := getEnv(snapshot, "GOVULNDB"); db != "" {
+ vulncheckargs = append(vulncheckargs, "-db", db)
+ }
+ vulncheckargs = append(vulncheckargs, pattern)
+ // TODO: support -tags. need to compute tags args from opts.BuildFlags.
+ // TODO: support -test.
+
+ ir, iw := io.Pipe()
+ handler := &govulncheckHandler{logger: log, osvs: map[string]*osv.Entry{}}
+
+ stderr := new(bytes.Buffer)
+ var g errgroup.Group
+ // We run the govulncheck's analysis in a separate process as it can
+ // consume a lot of CPUs and memory, and terminates: a separate process
+ // is a perfect garbage collector and affords us ways to limit its resource usage.
+ g.Go(func() error {
+ defer iw.Close()
+
+ cmd := exec.CommandContext(ctx, os.Args[0], vulncheckargs...)
+ cmd.Env = getEnvSlices(snapshot)
+ if goversion := getEnv(snapshot, GoVersionForVulnTest); goversion != "" {
+ // Let govulncheck API use a different Go version using the (undocumented) hook
+ // in https://go.googlesource.com/vuln/+/v1.0.1/internal/scan/run.go#76
+ cmd.Env = append(cmd.Env, "GOVERSION="+goversion)
+ }
+ cmd.Stderr = stderr // stream vulncheck's STDERR as progress reports
+ cmd.Stdout = iw // let the other goroutine parses the result.
+
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("failed to start govulncheck: %v", err)
+ }
+ if err := cmd.Wait(); err != nil {
+ return fmt.Errorf("failed to run govulncheck: %v", err)
+ }
+ return nil
+ })
+ g.Go(func() error {
+ return govulncheck.HandleJSON(ir, handler)
+ })
+ if err := g.Wait(); err != nil {
+ if stderr.Len() > 0 {
+ log.Write(stderr.Bytes())
+ }
+ return nil, fmt.Errorf("failed to read govulncheck output: %v", err)
+ }
+
+ findings := handler.findings // sort so the findings in the result is deterministic.
+ sort.Slice(findings, func(i, j int) bool {
+ x, y := findings[i], findings[j]
+ if x.OSV != y.OSV {
+ return x.OSV < y.OSV
+ }
+ return x.Trace[0].Package < y.Trace[0].Package
+ })
+ result := &vulncheck.Result{
+ Mode: vulncheck.ModeGovulncheck,
+ AsOf: time.Now(),
+ Entries: handler.osvs,
+ Findings: findings,
+ }
+ return result, nil
+}
+
+type govulncheckHandler struct {
+ logger io.Writer // forward progress reports to logger.
+ err error
+
+ osvs map[string]*osv.Entry
+ findings []*govulncheck.Finding
+}
+
+// Config implements vulncheck.Handler.
+func (h *govulncheckHandler) Config(config *govulncheck.Config) error {
+ if config.GoVersion != "" {
+ fmt.Fprintf(h.logger, "Go: %v\n", config.GoVersion)
+ }
+ if config.ScannerName != "" {
+ scannerName := fmt.Sprintf("Scanner: %v", config.ScannerName)
+ if config.ScannerVersion != "" {
+ scannerName += "@" + config.ScannerVersion
+ }
+ fmt.Fprintln(h.logger, scannerName)
+ }
+ if config.DB != "" {
+ dbInfo := fmt.Sprintf("DB: %v", config.DB)
+ if config.DBLastModified != nil {
+ dbInfo += fmt.Sprintf(" (DB updated: %v)", config.DBLastModified.String())
+ }
+ fmt.Fprintln(h.logger, dbInfo)
+ }
+ return nil
+}
+
+// Finding implements vulncheck.Handler.
+func (h *govulncheckHandler) Finding(finding *govulncheck.Finding) error {
+ h.findings = append(h.findings, finding)
+ return nil
+}
+
+// OSV implements vulncheck.Handler.
+func (h *govulncheckHandler) OSV(entry *osv.Entry) error {
+ h.osvs[entry.ID] = entry
+ return nil
+}
+
+// Progress implements vulncheck.Handler.
+func (h *govulncheckHandler) Progress(progress *govulncheck.Progress) error {
+ if progress.Message != "" {
+ fmt.Fprintf(h.logger, "%v\n", progress.Message)
+ }
+ return nil
+}
+
+func getEnv(snapshot source.Snapshot, key string) string {
+ val, ok := snapshot.Options().Env[key]
+ if ok {
+ return val
+ }
+ return os.Getenv(key)
+}
+
+func getEnvSlices(snapshot source.Snapshot) []string {
+ return append(os.Environ(), snapshot.Options().EnvSlice()...)
+}
+
+// semverToGoTag returns the Go standard library repository tag corresponding
+// to semver, a version string without the initial "v".
+// Go tags differ from standard semantic versions in a few ways,
+// such as beginning with "go" instead of "v".
+func semverToGoTag(v string) string {
+ if strings.HasPrefix(v, "v0.0.0") {
+ return "master"
+ }
+ // Special case: v1.0.0 => go1.
+ if v == "v1.0.0" {
+ return "go1"
+ }
+ if !semver.IsValid(v) {
+ return fmt.Sprintf("", v)
+ }
+ goVersion := semver.Canonical(v)
+ prerelease := semver.Prerelease(goVersion)
+ versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease)
+ patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".")
+ if patch == "0" {
+ versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0")
+ }
+ goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v"))
+ if prerelease != "" {
+ // Go prereleases look like "beta1" instead of "beta.1".
+ // "beta1" is bad for sorting (since beta10 comes before beta9), so
+ // require the dot form.
+ i := finalDigitsIndex(prerelease)
+ if i >= 1 {
+ if prerelease[i-1] != '.' {
+ return fmt.Sprintf("", v)
+ }
+ // Remove the dot.
+ prerelease = prerelease[:i-1] + prerelease[i:]
+ }
+ goVersion += strings.TrimPrefix(prerelease, "-")
+ }
+ return goVersion
+}
+
+// finalDigitsIndex returns the index of the first digit in the sequence of digits ending s.
+// If s doesn't end in digits, it returns -1.
+func finalDigitsIndex(s string) int {
+ // Assume ASCII (since the semver package does anyway).
+ var i int
+ for i = len(s) - 1; i >= 0; i-- {
+ if s[i] < '0' || s[i] > '9' {
+ break
+ }
+ }
+ if i == len(s)-1 {
+ return -1
+ }
+ return i + 1
+}
+
+// VulnerablePackages queries the vulndb and reports which vulnerabilities
+// apply to this snapshot. The result contains a set of packages,
+// grouped by vuln ID and by module. This implements the "import-based"
+// vulnerability report on go.mod files.
+func VulnerablePackages(ctx context.Context, snapshot source.Snapshot) (*vulncheck.Result, error) {
+ // TODO(hyangah): can we let 'govulncheck' take a package list
+ // used in the workspace and implement this function?
+
+ // We want to report the intersection of vulnerable packages in the vulndb
+ // and packages transitively imported by this module ('go list -deps all').
+ // We use snapshot.AllMetadata to retrieve the list of packages
+ // as an approximation.
+ //
+ // TODO(hyangah): snapshot.AllMetadata is a superset of
+ // `go list all` - e.g. when the workspace has multiple main modules
+ // (multiple go.mod files), that can include packages that are not
+ // used by this module. Vulncheck behavior with go.work is not well
+ // defined. Figure out the meaning, and if we decide to present
+ // the result as if each module is analyzed independently, make
+ // gopls track a separate build list for each module and use that
+ // information instead of snapshot.AllMetadata.
+ metadata, err := snapshot.AllMetadata(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO(hyangah): handle vulnerabilities in the standard library.
+
+ // Group packages by modules since vuln db is keyed by module.
+ metadataByModule := map[source.PackagePath][]*source.Metadata{}
+ for _, md := range metadata {
+ modulePath := source.PackagePath(osv.GoStdModulePath)
+ if mi := md.Module; mi != nil {
+ modulePath = source.PackagePath(mi.Path)
+ }
+ metadataByModule[modulePath] = append(metadataByModule[modulePath], md)
+ }
+
+ var (
+ mu sync.Mutex
+ // Keys are osv.Entry.ID
+ osvs = map[string]*osv.Entry{}
+ findings []*govulncheck.Finding
+ )
+
+ goVersion := snapshot.Options().Env[GoVersionForVulnTest]
+ if goVersion == "" {
+ goVersion = snapshot.View().GoVersionString()
+ }
+
+ stdlibModule := &packages.Module{
+ Path: osv.GoStdModulePath,
+ Version: goVersion,
+ }
+
+ // GOVULNDB may point the test db URI.
+ db := getEnv(snapshot, "GOVULNDB")
+
+ var group errgroup.Group
+ group.SetLimit(10) // limit govulncheck api runs
+ for _, mds := range metadataByModule {
+ mds := mds
+ group.Go(func() error {
+ effectiveModule := stdlibModule
+ if m := mds[0].Module; m != nil {
+ effectiveModule = m
+ }
+ for effectiveModule.Replace != nil {
+ effectiveModule = effectiveModule.Replace
+ }
+ ver := effectiveModule.Version
+ if ver == "" || !isem.Valid(ver) {
+ // skip invalid version strings. the underlying scan api is strict.
+ return nil
+ }
+
+ // TODO(hyangah): batch these requests and add in-memory cache for efficiency.
+ vulns, err := osvsByModule(ctx, db, effectiveModule.Path+"@"+ver)
+ if err != nil {
+ return err
+ }
+ if len(vulns) == 0 { // No known vulnerability.
+ return nil
+ }
+
+ // set of packages in this module known to gopls.
+ // This will be lazily initialized when we need it.
+ var knownPkgs map[source.PackagePath]bool
+
+ // Report vulnerabilities that affect packages of this module.
+ for _, entry := range vulns {
+ var vulnerablePkgs []*govulncheck.Finding
+ fixed := fixedVersion(effectiveModule.Path, entry.Affected)
+
+ for _, a := range entry.Affected {
+ if a.Module.Ecosystem != osv.GoEcosystem || a.Module.Path != effectiveModule.Path {
+ continue
+ }
+ for _, imp := range a.EcosystemSpecific.Packages {
+ if knownPkgs == nil {
+ knownPkgs = toPackagePathSet(mds)
+ }
+ if knownPkgs[source.PackagePath(imp.Path)] {
+ vulnerablePkgs = append(vulnerablePkgs, &govulncheck.Finding{
+ OSV: entry.ID,
+ FixedVersion: fixed,
+ Trace: []*govulncheck.Frame{
+ {
+ Module: effectiveModule.Path,
+ Version: effectiveModule.Version,
+ Package: imp.Path,
+ },
+ },
+ })
+ }
+ }
+ }
+ if len(vulnerablePkgs) == 0 {
+ continue
+ }
+ mu.Lock()
+ osvs[entry.ID] = entry
+ findings = append(findings, vulnerablePkgs...)
+ mu.Unlock()
+ }
+ return nil
+ })
+ }
+ if err := group.Wait(); err != nil {
+ return nil, err
+ }
+
+ // Sort so the results are deterministic.
+ sort.Slice(findings, func(i, j int) bool {
+ x, y := findings[i], findings[j]
+ if x.OSV != y.OSV {
+ return x.OSV < y.OSV
+ }
+ return x.Trace[0].Package < y.Trace[0].Package
+ })
+ ret := &vulncheck.Result{
+ Entries: osvs,
+ Findings: findings,
+ Mode: vulncheck.ModeImports,
+ }
+ return ret, nil
+}
+
+// toPackagePathSet transforms the metadata to a set of package paths.
+func toPackagePathSet(mds []*source.Metadata) map[source.PackagePath]bool {
+ pkgPaths := make(map[source.PackagePath]bool, len(mds))
+ for _, md := range mds {
+ pkgPaths[md.PkgPath] = true
+ }
+ return pkgPaths
+}
+
+func fixedVersion(modulePath string, affected []osv.Affected) string {
+ fixed := LatestFixed(modulePath, affected)
+ if fixed != "" {
+ fixed = versionString(modulePath, fixed)
+ }
+ return fixed
+}
+
+// versionString prepends a version string prefix (`v` or `go`
+// depending on the modulePath) to the given semver-style version string.
+func versionString(modulePath, version string) string {
+ if version == "" {
+ return ""
+ }
+ v := "v" + version
+ // These are internal Go module paths used by the vuln DB
+ // when listing vulns in standard library and the go command.
+ if modulePath == "stdlib" || modulePath == "toolchain" {
+ return semverToGoTag(v)
+ }
+ return v
+}
+
+// osvsByModule runs a govulncheck database query.
+func osvsByModule(ctx context.Context, db, moduleVersion string) ([]*osv.Entry, error) {
+ var args []string
+ args = append(args, "-mode=query", "-json")
+ if db != "" {
+ args = append(args, "-db="+db)
+ }
+ args = append(args, moduleVersion)
+
+ ir, iw := io.Pipe()
+ handler := &osvReader{}
+
+ var g errgroup.Group
+ g.Go(func() error {
+ defer iw.Close() // scan API doesn't close cmd.Stderr/cmd.Stdout.
+ cmd := scan.Command(ctx, args...)
+ cmd.Stdout = iw
+ // TODO(hakim): Do we need to set cmd.Env = getEnvSlices(),
+ // or is the process environment good enough?
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ return cmd.Wait()
+ })
+ g.Go(func() error {
+ return govulncheck.HandleJSON(ir, handler)
+ })
+
+ if err := g.Wait(); err != nil {
+ return nil, err
+ }
+ return handler.entry, nil
+}
+
+// osvReader implements govulncheck.Handler.
+type osvReader struct {
+ entry []*osv.Entry
+}
+
+func (h *osvReader) OSV(entry *osv.Entry) error {
+ h.entry = append(h.entry, entry)
+ return nil
+}
+
+func (h *osvReader) Config(config *govulncheck.Config) error {
+ return nil
+}
+
+func (h *osvReader) Finding(finding *govulncheck.Finding) error {
+ return nil
+}
+
+func (h *osvReader) Progress(progress *govulncheck.Progress) error {
+ return nil
+}
diff --git a/gopls/internal/govulncheck/util.go b/gopls/internal/vulncheck/scan/util.go
similarity index 79%
rename from gopls/internal/govulncheck/util.go
rename to gopls/internal/vulncheck/scan/util.go
index 544fba2a593..2ea75a5183a 100644
--- a/gopls/internal/govulncheck/util.go
+++ b/gopls/internal/vulncheck/scan/util.go
@@ -5,12 +5,12 @@
//go:build go1.18
// +build go1.18
-package govulncheck
+package scan
import (
"golang.org/x/mod/semver"
- isem "golang.org/x/tools/gopls/internal/govulncheck/semver"
- "golang.org/x/vuln/osv"
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
+ isem "golang.org/x/tools/gopls/internal/vulncheck/semver"
)
// LatestFixed returns the latest fixed version in the list of affected ranges,
@@ -18,11 +18,11 @@ import (
func LatestFixed(modulePath string, as []osv.Affected) string {
v := ""
for _, a := range as {
- if a.Package.Name != modulePath {
+ if a.Module.Path != modulePath {
continue
}
for _, r := range a.Ranges {
- if r.Type == osv.TypeSemver {
+ if r.Type == osv.RangeTypeSemver {
for _, e := range r.Events {
if e.Fixed != "" && (v == "" ||
semver.Compare(isem.CanonicalizeSemverPrefix(e.Fixed), isem.CanonicalizeSemverPrefix(v)) > 0) {
diff --git a/gopls/internal/govulncheck/semver/semver.go b/gopls/internal/vulncheck/semver/semver.go
similarity index 88%
rename from gopls/internal/govulncheck/semver/semver.go
rename to gopls/internal/vulncheck/semver/semver.go
index 4ab298d137b..5cd1ee864d3 100644
--- a/gopls/internal/govulncheck/semver/semver.go
+++ b/gopls/internal/vulncheck/semver/semver.go
@@ -12,6 +12,8 @@ package semver
import (
"regexp"
"strings"
+
+ "golang.org/x/mod/semver"
)
// addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed
@@ -40,6 +42,12 @@ func CanonicalizeSemverPrefix(s string) string {
return addSemverPrefix(removeSemverPrefix(s))
}
+// Valid returns whether v is valid semver, allowing
+// either a "v", "go" or no prefix.
+func Valid(v string) bool {
+ return semver.IsValid(CanonicalizeSemverPrefix(v))
+}
+
var (
// Regexp for matching go tags. The groups are:
// 1 the major.minor version
diff --git a/gopls/internal/govulncheck/semver/semver_test.go b/gopls/internal/vulncheck/semver/semver_test.go
similarity index 100%
rename from gopls/internal/govulncheck/semver/semver_test.go
rename to gopls/internal/vulncheck/semver/semver_test.go
diff --git a/gopls/internal/govulncheck/types.go b/gopls/internal/vulncheck/types.go
similarity index 70%
rename from gopls/internal/govulncheck/types.go
rename to gopls/internal/vulncheck/types.go
index 2881cf4bc40..450cd961797 100644
--- a/gopls/internal/govulncheck/types.go
+++ b/gopls/internal/vulncheck/types.go
@@ -2,15 +2,25 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package govulncheck
+// go:generate go run copier.go
-import "time"
+package vulncheck
+
+import (
+ "time"
+
+ gvc "golang.org/x/tools/gopls/internal/vulncheck/govulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
+)
// Result is the result of vulnerability scanning.
type Result struct {
- // Vulns contains all vulnerabilities that are called or imported by
- // the analyzed module.
- Vulns []*Vuln `json:",omitempty"`
+ // Entries contains all vulnerabilities that are called or imported by
+ // the analyzed module. Keys are Entry.IDs.
+ Entries map[string]*osv.Entry
+ // Findings are vulnerabilities found by vulncheck or import-based analysis.
+ // Ordered by the OSV IDs and the package names.
+ Findings []*gvc.Finding
// Mode contains the source of the vulnerability info.
// Clients of the gopls.fetch_vulncheck_result command may need
diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go
deleted file mode 100644
index 3c361bd01e4..00000000000
--- a/gopls/internal/vulncheck/vulncheck.go
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2022 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 vulncheck provides an analysis command
-// that runs vulnerability analysis using data from
-// golang.org/x/vuln/vulncheck.
-// This package requires go1.18 or newer.
-package vulncheck
-
-import (
- "context"
-
- "golang.org/x/tools/go/packages"
- "golang.org/x/tools/gopls/internal/govulncheck"
- "golang.org/x/tools/gopls/internal/lsp/source"
-)
-
-// With go1.18+, this is swapped with the real implementation.
-var Main func(cfg packages.Config, patterns ...string) error = nil
-
-// VulnerablePackages queries the vulndb and reports which vulnerabilities
-// apply to this snapshot. The result contains a set of packages,
-// grouped by vuln ID and by module.
-var VulnerablePackages func(ctx context.Context, snapshot source.Snapshot, modfile source.FileHandle) (*govulncheck.Result, error) = nil
diff --git a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go
index 511a47e1ba9..a4ea54b95fc 100644
--- a/gopls/internal/vulncheck/vulntest/db.go
+++ b/gopls/internal/vulncheck/vulntest/db.go
@@ -13,7 +13,6 @@ import (
"context"
"encoding/json"
"fmt"
- "io/ioutil"
"os"
"path/filepath"
"sort"
@@ -21,9 +20,8 @@ import (
"time"
"golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
"golang.org/x/tools/txtar"
- "golang.org/x/vuln/client"
- "golang.org/x/vuln/osv"
)
// NewDatabase returns a read-only DB containing the provided
@@ -42,7 +40,7 @@ import (
// The returned DB's Clean method must be called to clean up the
// generated database.
func NewDatabase(ctx context.Context, txtarReports []byte) (*DB, error) {
- disk, err := ioutil.TempDir("", "vulndb-test")
+ disk, err := os.MkdirTemp("", "vulndb-test")
if err != nil {
return nil, err
}
@@ -64,7 +62,7 @@ type DB struct {
// URI returns the file URI that can be used for VULNDB environment
// variable.
func (db *DB) URI() string {
- u := span.URIFromPath(db.disk)
+ u := span.URIFromPath(filepath.Join(db.disk, "ID"))
return string(u)
}
@@ -73,11 +71,6 @@ func (db *DB) Clean() error {
return os.RemoveAll(db.disk)
}
-// NewClient returns a vuln DB client that works with the given DB.
-func NewClient(db *DB) (client.Client, error) {
- return client.NewClient([]string{db.URI()}, client.Options{})
-}
-
//
// The following was selectively copied from golang.org/x/vulndb/internal/database
//
@@ -89,14 +82,6 @@ const (
// listed by their IDs.
idDirectory = "ID"
- // stdFileName is the name of the .json file in the vulndb repo
- // that will contain info on standard library vulnerabilities.
- stdFileName = "stdlib"
-
- // toolchainFileName is the name of the .json file in the vulndb repo
- // that will contain info on toolchain (cmd/...) vulnerabilities.
- toolchainFileName = "toolchain"
-
// cmdModule is the name of the module containing Go toolchain
// binaries.
cmdModule = "cmd"
@@ -109,38 +94,15 @@ const (
func generateDB(ctx context.Context, txtarData []byte, jsonDir string, indent bool) error {
archive := txtar.Parse(txtarData)
- jsonVulns, entries, err := generateEntries(ctx, archive)
+ entries, err := generateEntries(ctx, archive)
if err != nil {
return err
}
-
- index := make(client.DBIndex, len(jsonVulns))
- for modulePath, vulns := range jsonVulns {
- epath, err := client.EscapeModulePath(modulePath)
- if err != nil {
- return err
- }
- if err := writeVulns(filepath.Join(jsonDir, epath), vulns, indent); err != nil {
- return err
- }
- for _, v := range vulns {
- if v.Modified.After(index[modulePath]) {
- index[modulePath] = v.Modified
- }
- }
- }
- if err := writeJSON(filepath.Join(jsonDir, "index.json"), index, indent); err != nil {
- return err
- }
- if err := writeAliasIndex(jsonDir, entries, indent); err != nil {
- return err
- }
return writeEntriesByID(filepath.Join(jsonDir, idDirectory), entries, indent)
}
-func generateEntries(_ context.Context, archive *txtar.Archive) (map[string][]osv.Entry, []osv.Entry, error) {
+func generateEntries(_ context.Context, archive *txtar.Archive) ([]osv.Entry, error) {
now := time.Now()
- jsonVulns := map[string][]osv.Entry{}
var entries []osv.Entry
for _, f := range archive.Files {
if !strings.HasSuffix(f.Name, ".yaml") {
@@ -148,17 +110,14 @@ func generateEntries(_ context.Context, archive *txtar.Archive) (map[string][]os
}
r, err := readReport(bytes.NewReader(f.Data))
if err != nil {
- return nil, nil, err
+ return nil, err
}
name := strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name))
linkName := fmt.Sprintf("%s%s", dbURL, name)
- entry, modulePaths := generateOSVEntry(name, linkName, now, *r)
- for _, modulePath := range modulePaths {
- jsonVulns[modulePath] = append(jsonVulns[modulePath], entry)
- }
+ entry := generateOSVEntry(name, linkName, now, *r)
entries = append(entries, entry)
}
- return jsonVulns, entries, nil
+ return entries, nil
}
func writeVulns(outPath string, vulns []osv.Entry, indent bool) error {
@@ -173,27 +132,13 @@ func writeEntriesByID(idDir string, entries []osv.Entry, indent bool) error {
if err := os.MkdirAll(idDir, 0755); err != nil {
return fmt.Errorf("failed to create directory %q: %v", idDir, err)
}
- var idIndex []string
for _, e := range entries {
outPath := filepath.Join(idDir, e.ID+".json")
if err := writeJSON(outPath, e, indent); err != nil {
return err
}
- idIndex = append(idIndex, e.ID)
}
- // Write an index.json in the ID directory with a list of all the IDs.
- return writeJSON(filepath.Join(idDir, "index.json"), idIndex, indent)
-}
-
-// Write a JSON file containing a map from alias to GO IDs.
-func writeAliasIndex(dir string, entries []osv.Entry, indent bool) error {
- aliasToGoIDs := map[string][]string{}
- for _, e := range entries {
- for _, a := range e.Aliases {
- aliasToGoIDs[a] = append(aliasToGoIDs[a], e.ID)
- }
- }
- return writeJSON(filepath.Join(dir, "aliases.json"), aliasToGoIDs, indent)
+ return nil
}
func writeJSON(filename string, value any, indent bool) (err error) {
@@ -214,45 +159,40 @@ func jsonMarshal(v any, indent bool) ([]byte, error) {
// generateOSVEntry create an osv.Entry for a report. In addition to the report, it
// takes the ID for the vuln and a URL that will point to the entry in the vuln DB.
// It returns the osv.Entry and a list of module paths that the vuln affects.
-func generateOSVEntry(id, url string, lastModified time.Time, r Report) (osv.Entry, []string) {
+func generateOSVEntry(id, url string, lastModified time.Time, r Report) osv.Entry {
entry := osv.Entry{
- ID: id,
- Published: r.Published,
- Modified: lastModified,
- Withdrawn: r.Withdrawn,
- Details: r.Description,
+ ID: id,
+ Published: r.Published,
+ Modified: lastModified,
+ Withdrawn: r.Withdrawn,
+ Summary: r.Summary,
+ Details: r.Description,
+ DatabaseSpecific: &osv.DatabaseSpecific{URL: url},
}
moduleMap := make(map[string]bool)
for _, m := range r.Modules {
switch m.Module {
case stdModule:
- moduleMap[stdFileName] = true
+ moduleMap[osv.GoStdModulePath] = true
case cmdModule:
- moduleMap[toolchainFileName] = true
+ moduleMap[osv.GoCmdModulePath] = true
default:
moduleMap[m.Module] = true
}
- entry.Affected = append(entry.Affected, generateAffected(m, url))
+ entry.Affected = append(entry.Affected, toAffected(m))
}
for _, ref := range r.References {
entry.References = append(entry.References, osv.Reference{
- Type: string(ref.Type),
+ Type: ref.Type,
URL: ref.URL,
})
}
-
- var modulePaths []string
- for module := range moduleMap {
- modulePaths = append(modulePaths, module)
- }
- // TODO: handle missing fields - Aliases
-
- return entry, modulePaths
+ return entry
}
-func generateAffectedRanges(versions []VersionRange) osv.Affects {
- a := osv.AffectsRange{Type: osv.TypeSemver}
+func AffectedRanges(versions []VersionRange) []osv.Range {
+ a := osv.Range{Type: osv.RangeTypeSemver}
if len(versions) == 0 || versions[0].Introduced == "" {
a.Events = append(a.Events, osv.RangeEvent{Introduced: "0"})
}
@@ -264,15 +204,15 @@ func generateAffectedRanges(versions []VersionRange) osv.Affects {
a.Events = append(a.Events, osv.RangeEvent{Fixed: v.Fixed.Canonical()})
}
}
- return osv.Affects{a}
+ return []osv.Range{a}
}
-func generateImports(m *Module) (imps []osv.EcosystemSpecificImport) {
- for _, p := range m.Packages {
+func toOSVPackages(pkgs []*Package) (imps []osv.Package) {
+ for _, p := range pkgs {
syms := append([]string{}, p.Symbols...)
syms = append(syms, p.DerivedSymbols...)
sort.Strings(syms)
- imps = append(imps, osv.EcosystemSpecificImport{
+ imps = append(imps, osv.Package{
Path: p.Package,
GOOS: p.GOOS,
GOARCH: p.GOARCH,
@@ -281,23 +221,23 @@ func generateImports(m *Module) (imps []osv.EcosystemSpecificImport) {
}
return imps
}
-func generateAffected(m *Module, url string) osv.Affected {
+
+func toAffected(m *Module) osv.Affected {
name := m.Module
switch name {
case stdModule:
- name = "stdlib"
+ name = osv.GoStdModulePath
case cmdModule:
- name = "toolchain"
+ name = osv.GoCmdModulePath
}
return osv.Affected{
- Package: osv.Package{
- Name: name,
+ Module: osv.Module{
+ Path: name,
Ecosystem: osv.GoEcosystem,
},
- Ranges: generateAffectedRanges(m.Versions),
- DatabaseSpecific: osv.DatabaseSpecific{URL: url},
+ Ranges: AffectedRanges(m.Versions),
EcosystemSpecific: osv.EcosystemSpecific{
- Imports: generateImports(m),
+ Packages: toOSVPackages(m.Packages),
},
}
}
diff --git a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulncheck/vulntest/db_test.go
index 7d939421c94..d68ba08b1eb 100644
--- a/gopls/internal/vulncheck/vulntest/db_test.go
+++ b/gopls/internal/vulncheck/vulntest/db_test.go
@@ -10,52 +10,70 @@ package vulntest
import (
"context"
"encoding/json"
+ "flag"
+ "os"
+ "path/filepath"
"testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
)
+var update = flag.Bool("update", false, "update golden files in testdata/")
+
func TestNewDatabase(t *testing.T) {
ctx := context.Background()
- in := []byte(`
--- GO-2020-0001.yaml --
-modules:
- - module: github.com/gin-gonic/gin
- versions:
- - fixed: 1.6.0
- packages:
- - package: github.com/gin-gonic/gin
- symbols:
- - defaultLogFormatter
-description: |
- Something.
-published: 2021-04-14T20:04:52Z
-references:
- - fix: https://github.com/gin-gonic/gin/pull/2237
-`)
- db, err := NewDatabase(ctx, in)
+ in, err := os.ReadFile("testdata/report.yaml")
if err != nil {
t.Fatal(err)
}
- defer db.Clean()
+ in = append([]byte("-- GO-2020-0001.yaml --\n"), in...)
- cli, err := NewClient(db)
+ db, err := NewDatabase(ctx, in)
if err != nil {
t.Fatal(err)
}
- got, err := cli.GetByID(ctx, "GO-2020-0001")
+ defer db.Clean()
+ dbpath := span.URIFromURI(db.URI()).Filename()
+
+ // The generated JSON file will be in DB/GO-2022-0001.json.
+ got := readOSVEntry(t, filepath.Join(dbpath, "GO-2020-0001.json"))
+ got.Modified = time.Time{}
+
+ if *update {
+ updateTestData(t, got, "testdata/GO-2020-0001.json")
+ }
+
+ want := readOSVEntry(t, "testdata/GO-2020-0001.json")
+ want.Modified = time.Time{}
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("mismatch (-want +got):\n%s", diff)
+ }
+}
+
+func updateTestData(t *testing.T, got *osv.Entry, fname string) {
+ content, err := json.MarshalIndent(got, "", "\t")
if err != nil {
t.Fatal(err)
}
- if got.ID != "GO-2020-0001" {
- m, _ := json.Marshal(got)
- t.Errorf("got %s\nwant GO-2020-0001 entry", m)
+ if err := os.WriteFile(fname, content, 0666); err != nil {
+ t.Fatal(err)
}
- gotAll, err := cli.GetByModule(ctx, "github.com/gin-gonic/gin")
+ t.Logf("updated %v", fname)
+}
+
+func readOSVEntry(t *testing.T, filename string) *osv.Entry {
+ t.Helper()
+ content, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
- if len(gotAll) != 1 || gotAll[0].ID != "GO-2020-0001" {
- m, _ := json.Marshal(got)
- t.Errorf("got %s\nwant GO-2020-0001 entry", m)
+ var entry osv.Entry
+ if err := json.Unmarshal(content, &entry); err != nil {
+ t.Fatal(err)
}
+ return &entry
}
diff --git a/gopls/internal/vulncheck/vulntest/report.go b/gopls/internal/vulncheck/vulntest/report.go
index e5595e8ba06..cbfd0aeb8ff 100644
--- a/gopls/internal/vulncheck/vulntest/report.go
+++ b/gopls/internal/vulncheck/vulntest/report.go
@@ -15,6 +15,7 @@ import (
"time"
"golang.org/x/mod/semver"
+ "golang.org/x/tools/gopls/internal/vulncheck/osv"
"gopkg.in/yaml.v3"
)
@@ -36,10 +37,15 @@ func readReport(in io.Reader) (*Report, error) {
}
// Report represents a vulnerability report in the vulndb.
-// Remember to update doc/format.md when this structure changes.
+// See https://go.googlesource.com/vulndb/+/refs/heads/master/doc/format.md
type Report struct {
+ ID string `yaml:",omitempty"`
+
Modules []*Module `yaml:",omitempty"`
+ // Summary is a short phrase describing the vulnerability.
+ Summary string `yaml:",omitempty"`
+
// Description is the CVE description from an existing CVE. If we are
// assigning a CVE ID ourselves, use CVEMetadata.Description instead.
Description string `yaml:",omitempty"`
@@ -153,10 +159,7 @@ var ReferenceTypes = []ReferenceType{
//
// For ease of typing, References are represented in the YAML as a
// single-element mapping of type to URL.
-type Reference struct {
- Type ReferenceType `json:"type,omitempty"`
- URL string `json:"url,omitempty"`
-}
+type Reference osv.Reference
func (r *Reference) MarshalYAML() (interface{}, error) {
return map[string]string{
@@ -170,7 +173,7 @@ func (r *Reference) UnmarshalYAML(n *yaml.Node) (err error) {
fmt.Sprintf("line %d: report.Reference must contain a mapping with one value", n.Line),
}}
}
- r.Type = ReferenceType(strings.ToUpper(n.Content[0].Value))
+ r.Type = osv.ReferenceType(strings.ToUpper(n.Content[0].Value))
r.URL = n.Content[1].Value
return nil
}
diff --git a/gopls/internal/vulncheck/vulntest/report_test.go b/gopls/internal/vulncheck/vulntest/report_test.go
index c42dae805fa..31f62aba838 100644
--- a/gopls/internal/vulncheck/vulntest/report_test.go
+++ b/gopls/internal/vulncheck/vulntest/report_test.go
@@ -10,7 +10,6 @@ package vulntest
import (
"bytes"
"io"
- "io/ioutil"
"os"
"path/filepath"
"testing"
@@ -19,7 +18,7 @@ import (
)
func readAll(t *testing.T, filename string) io.Reader {
- d, err := ioutil.ReadFile(filename)
+ d, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json b/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json
new file mode 100644
index 00000000000..db371bd6930
--- /dev/null
+++ b/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json
@@ -0,0 +1,50 @@
+{
+ "id": "GO-2020-0001",
+ "modified": "0001-01-01T00:00:00Z",
+ "published": "0001-01-01T00:00:00Z",
+ "details": "The default Formatter for the Logger middleware (LoggerConfig.Formatter),\nwhich is included in the Default engine, allows attackers to inject arbitrary\nlog entries by manipulating the request path.\n",
+ "affected": [
+ {
+ "package": {
+ "name": "github.com/gin-gonic/gin",
+ "ecosystem": "Go"
+ },
+ "ranges": [
+ {
+ "type": "SEMVER",
+ "events": [
+ {
+ "introduced": "0"
+ },
+ {
+ "fixed": "1.6.0"
+ }
+ ]
+ }
+ ],
+ "ecosystem_specific": {
+ "imports": [
+ {
+ "path": "github.com/gin-gonic/gin",
+ "symbols": [
+ "defaultLogFormatter"
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "references": [
+ {
+ "type": "FIX",
+ "url": "https://github.com/gin-gonic/gin/pull/1234"
+ },
+ {
+ "type": "FIX",
+ "url": "https://github.com/gin-gonic/gin/commit/abcdefg"
+ }
+ ],
+ "database_specific": {
+ "url": "https://pkg.go.dev/vuln/GO-2020-0001"
+ }
+}
\ No newline at end of file
diff --git a/gopls/release/release.go b/gopls/release/release.go
index dab95822eb6..b2e0b3ca847 100644
--- a/gopls/release/release.go
+++ b/gopls/release/release.go
@@ -15,7 +15,6 @@ import (
"flag"
"fmt"
"go/types"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -109,7 +108,7 @@ func validateHardcodedVersion(version string) error {
func validateGoModFile(goplsDir string) error {
filename := filepath.Join(goplsDir, "go.mod")
- data, err := ioutil.ReadFile(filename)
+ data, err := os.ReadFile(filename)
if err != nil {
return err
}
diff --git a/imports/forward.go b/imports/forward.go
index d2547c74338..cb6db8893f9 100644
--- a/imports/forward.go
+++ b/imports/forward.go
@@ -7,8 +7,8 @@
package imports // import "golang.org/x/tools/imports"
import (
- "io/ioutil"
"log"
+ "os"
"golang.org/x/tools/internal/gocommand"
intimp "golang.org/x/tools/internal/imports"
@@ -44,7 +44,7 @@ var LocalPrefix string
func Process(filename string, src []byte, opt *Options) ([]byte, error) {
var err error
if src == nil {
- src, err = ioutil.ReadFile(filename)
+ src, err = os.ReadFile(filename)
if err != nil {
return nil, err
}
diff --git a/internal/apidiff/apidiff_test.go b/internal/apidiff/apidiff_test.go
index b385b7cbbab..ecf32e4a22f 100644
--- a/internal/apidiff/apidiff_test.go
+++ b/internal/apidiff/apidiff_test.go
@@ -8,7 +8,6 @@ import (
"bufio"
"fmt"
"go/types"
- "io/ioutil"
"os"
"path/filepath"
"reflect"
@@ -21,7 +20,7 @@ import (
)
func TestChanges(t *testing.T) {
- dir, err := ioutil.TempDir("", "apidiff_test")
+ dir, err := os.MkdirTemp("", "apidiff_test")
if err != nil {
t.Fatal(err)
}
@@ -66,7 +65,7 @@ func splitIntoPackages(t *testing.T, dir string) (incompatibles, compatibles []s
if err := os.MkdirAll(filepath.Join(dir, "src", "apidiff"), 0700); err != nil {
t.Fatal(err)
}
- if err := ioutil.WriteFile(filepath.Join(dir, "src", "apidiff", "go.mod"), []byte("module apidiff\n"), 0666); err != nil {
+ if err := os.WriteFile(filepath.Join(dir, "src", "apidiff", "go.mod"), []byte("module apidiff\n"), 0666); err != nil {
t.Fatal(err)
}
diff --git a/internal/cmd/deadcode/deadcode.go b/internal/cmd/deadcode/deadcode.go
index f3388aa6161..ecb9f9f12a8 100644
--- a/internal/cmd/deadcode/deadcode.go
+++ b/internal/cmd/deadcode/deadcode.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.20
+
package main
import (
diff --git a/internal/cmd/deadcode/deadcode_test.go b/internal/cmd/deadcode/deadcode_test.go
index 417b81606d6..ab8c81c86f0 100644
--- a/internal/cmd/deadcode/deadcode_test.go
+++ b/internal/cmd/deadcode/deadcode_test.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.20
+
package main_test
import (
diff --git a/internal/compat/appendf.go b/internal/compat/appendf.go
new file mode 100644
index 00000000000..069d5171704
--- /dev/null
+++ b/internal/compat/appendf.go
@@ -0,0 +1,13 @@
+// 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.
+
+//go:build go1.19
+
+package compat
+
+import "fmt"
+
+func Appendf(b []byte, format string, a ...interface{}) []byte {
+ return fmt.Appendf(b, format, a...)
+}
diff --git a/internal/compat/appendf_118.go b/internal/compat/appendf_118.go
new file mode 100644
index 00000000000..29af353cdaf
--- /dev/null
+++ b/internal/compat/appendf_118.go
@@ -0,0 +1,13 @@
+// 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.
+
+//go:build !go1.19
+
+package compat
+
+import "fmt"
+
+func Appendf(b []byte, format string, a ...interface{}) []byte {
+ return append(b, fmt.Sprintf(format, a...)...)
+}
diff --git a/internal/compat/doc.go b/internal/compat/doc.go
new file mode 100644
index 00000000000..59c667a37a2
--- /dev/null
+++ b/internal/compat/doc.go
@@ -0,0 +1,7 @@
+// 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.
+
+// The compat package implements API shims for backward compatibility at older
+// Go versions.
+package compat
diff --git a/internal/diff/difftest/difftest_test.go b/internal/diff/difftest/difftest_test.go
index a990e522438..c64a0fa0c9f 100644
--- a/internal/diff/difftest/difftest_test.go
+++ b/internal/diff/difftest/difftest_test.go
@@ -9,7 +9,6 @@ package difftest_test
import (
"fmt"
- "io/ioutil"
"os"
"os/exec"
"strings"
@@ -41,7 +40,7 @@ func TestVerifyUnified(t *testing.T) {
}
func getDiffOutput(a, b string) (string, error) {
- fileA, err := ioutil.TempFile("", "myers.in")
+ fileA, err := os.CreateTemp("", "myers.in")
if err != nil {
return "", err
}
@@ -52,7 +51,7 @@ func getDiffOutput(a, b string) (string, error) {
if err := fileA.Close(); err != nil {
return "", err
}
- fileB, err := ioutil.TempFile("", "myers.in")
+ fileB, err := os.CreateTemp("", "myers.in")
if err != nil {
return "", err
}
diff --git a/internal/diff/lcs/old.go b/internal/diff/lcs/old.go
index 7af11fc896c..a14ae9119ca 100644
--- a/internal/diff/lcs/old.go
+++ b/internal/diff/lcs/old.go
@@ -29,7 +29,7 @@ func DiffRunes(a, b []rune) []Diff { return diff(runesSeqs{a, b}) }
func diff(seqs sequences) []Diff {
// A limit on how deeply the LCS algorithm should search. The value is just a guess.
- const maxDiffs = 30
+ const maxDiffs = 100
diff, _ := compute(seqs, twosided, maxDiffs/2)
return diff
}
diff --git a/internal/diff/lcs/old_test.go b/internal/diff/lcs/old_test.go
index 0c894316fa5..e39941ba0a5 100644
--- a/internal/diff/lcs/old_test.go
+++ b/internal/diff/lcs/old_test.go
@@ -6,9 +6,9 @@ package lcs
import (
"fmt"
- "io/ioutil"
"log"
"math/rand"
+ "os"
"strings"
"testing"
)
@@ -218,7 +218,7 @@ func genBench(set string, n int) []struct{ before, after string } {
// itself minus the last byte is faster still; I don't know why.
// There is much low-hanging fruit here for further improvement.
func BenchmarkLargeFileSmallDiff(b *testing.B) {
- data, err := ioutil.ReadFile("old.go") // large file
+ data, err := os.ReadFile("old.go") // large file
if err != nil {
log.Fatal(err)
}
diff --git a/internal/event/bench_test.go b/internal/event/bench_test.go
index 9ec7519b5d6..aae2a57b09f 100644
--- a/internal/event/bench_test.go
+++ b/internal/event/bench_test.go
@@ -6,7 +6,7 @@ package event_test
import (
"context"
- "io/ioutil"
+ "io"
"log"
"testing"
@@ -119,7 +119,7 @@ func Benchmark(b *testing.B) {
b.Run(t.name+"Noop", t.test)
}
- event.SetExporter(export.Spans(export.LogWriter(ioutil.Discard, false)))
+ event.SetExporter(export.Spans(export.LogWriter(io.Discard, false)))
for _, t := range benchmarks {
b.Run(t.name, t.test)
}
@@ -150,7 +150,7 @@ func (hooks Hooks) runBenchmark(b *testing.B) {
}
func init() {
- log.SetOutput(ioutil.Discard)
+ log.SetOutput(io.Discard)
}
func noopExporter(ctx context.Context, ev core.Event, lm label.Map) context.Context {
diff --git a/internal/event/export/ocagent/ocagent_test.go b/internal/event/export/ocagent/ocagent_test.go
index 88730b10adf..38a52faede5 100644
--- a/internal/event/export/ocagent/ocagent_test.go
+++ b/internal/event/export/ocagent/ocagent_test.go
@@ -9,7 +9,7 @@ import (
"context"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"sync"
"testing"
@@ -191,7 +191,7 @@ func (s *fakeSender) RoundTrip(req *http.Request) (*http.Response, error) {
if s.data == nil {
s.data = make(map[string][]byte)
}
- data, err := ioutil.ReadAll(req.Body)
+ data, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
diff --git a/internal/facts/facts.go b/internal/facts/facts.go
index ec11d5e0af1..8480ea062f7 100644
--- a/internal/facts/facts.go
+++ b/internal/facts/facts.go
@@ -40,7 +40,7 @@ import (
"encoding/gob"
"fmt"
"go/types"
- "io/ioutil"
+ "io"
"log"
"reflect"
"sort"
@@ -356,7 +356,7 @@ func (s *Set) Encode(skipMethodSorting bool) []byte {
if err := gob.NewEncoder(&buf).Encode(gobFacts); err != nil {
// Fact encoding should never fail. Identify the culprit.
for _, gf := range gobFacts {
- if err := gob.NewEncoder(ioutil.Discard).Encode(gf); err != nil {
+ if err := gob.NewEncoder(io.Discard).Encode(gf); err != nil {
fact := gf.Fact
pkgpath := reflect.TypeOf(fact).Elem().PkgPath()
log.Panicf("internal error: gob encoding of analysis fact %s failed: %v; please report a bug against fact %T in package %q",
diff --git a/internal/fastwalk/fastwalk_portable.go b/internal/fastwalk/fastwalk_portable.go
index 085d311600b..27e860243e1 100644
--- a/internal/fastwalk/fastwalk_portable.go
+++ b/internal/fastwalk/fastwalk_portable.go
@@ -8,7 +8,6 @@
package fastwalk
import (
- "io/ioutil"
"os"
)
@@ -17,16 +16,20 @@ import (
// If fn returns a non-nil error, readDir returns with that error
// immediately.
func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error {
- fis, err := ioutil.ReadDir(dirName)
+ fis, err := os.ReadDir(dirName)
if err != nil {
return err
}
skipFiles := false
for _, fi := range fis {
- if fi.Mode().IsRegular() && skipFiles {
+ info, err := fi.Info()
+ if err != nil {
+ return err
+ }
+ if info.Mode().IsRegular() && skipFiles {
continue
}
- if err := fn(dirName, fi.Name(), fi.Mode()&os.ModeType); err != nil {
+ if err := fn(dirName, fi.Name(), info.Mode()&os.ModeType); err != nil {
if err == ErrSkipFiles {
skipFiles = true
continue
diff --git a/internal/fastwalk/fastwalk_test.go b/internal/fastwalk/fastwalk_test.go
index d896aebc956..b5c82bc5293 100644
--- a/internal/fastwalk/fastwalk_test.go
+++ b/internal/fastwalk/fastwalk_test.go
@@ -8,7 +8,6 @@ import (
"bytes"
"flag"
"fmt"
- "io/ioutil"
"os"
"path/filepath"
"reflect"
@@ -35,7 +34,7 @@ func formatFileModes(m map[string]os.FileMode) string {
}
func testFastWalk(t *testing.T, files map[string]string, callback func(path string, typ os.FileMode) error, want map[string]os.FileMode) {
- tempdir, err := ioutil.TempDir("", "test-fast-walk")
+ tempdir, err := os.MkdirTemp("", "test-fast-walk")
if err != nil {
t.Fatal(err)
}
@@ -51,7 +50,7 @@ func testFastWalk(t *testing.T, files map[string]string, callback func(path stri
if strings.HasPrefix(contents, "LINK:") {
symlinks[file] = filepath.FromSlash(strings.TrimPrefix(contents, "LINK:"))
} else {
- err = ioutil.WriteFile(file, []byte(contents), 0644)
+ err = os.WriteFile(file, []byte(contents), 0644)
}
if err != nil {
t.Fatal(err)
@@ -63,7 +62,7 @@ func testFastWalk(t *testing.T, files map[string]string, callback func(path stri
for file, dst := range symlinks {
err = os.Symlink(dst, file)
if err != nil {
- if writeErr := ioutil.WriteFile(file, []byte(dst), 0644); writeErr == nil {
+ if writeErr := os.WriteFile(file, []byte(dst), 0644); writeErr == nil {
// Couldn't create symlink, but could write the file.
// Probably this filesystem doesn't support symlinks.
// (Perhaps we are on an older Windows and not running as administrator.)
diff --git a/internal/fuzzy/self_test.go b/internal/fuzzy/self_test.go
new file mode 100644
index 00000000000..fae0aeae249
--- /dev/null
+++ b/internal/fuzzy/self_test.go
@@ -0,0 +1,39 @@
+// 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 fuzzy_test
+
+import (
+ "testing"
+
+ . "golang.org/x/tools/internal/fuzzy"
+)
+
+func BenchmarkSelf_Matcher(b *testing.B) {
+ idents := collectIdentifiers(b)
+ patterns := generatePatterns()
+
+ for i := 0; i < b.N; i++ {
+ for _, pattern := range patterns {
+ sm := NewMatcher(pattern)
+ for _, ident := range idents {
+ _ = sm.Score(ident)
+ }
+ }
+ }
+}
+
+func BenchmarkSelf_SymbolMatcher(b *testing.B) {
+ idents := collectIdentifiers(b)
+ patterns := generatePatterns()
+
+ for i := 0; i < b.N; i++ {
+ for _, pattern := range patterns {
+ sm := NewSymbolMatcher(pattern)
+ for _, ident := range idents {
+ _, _ = sm.Match([]string{ident})
+ }
+ }
+ }
+}
diff --git a/internal/fuzzy/symbol.go b/internal/fuzzy/symbol.go
index bf93041521b..5fe2ce3e2a3 100644
--- a/internal/fuzzy/symbol.go
+++ b/internal/fuzzy/symbol.go
@@ -5,6 +5,9 @@
package fuzzy
import (
+ "bytes"
+ "fmt"
+ "log"
"unicode"
)
@@ -36,10 +39,12 @@ type SymbolMatcher struct {
segments [256]uint8 // how many segments from the right is each rune
}
+// Rune roles.
const (
- segmentStart uint32 = 1 << iota
- wordStart
- separator
+ segmentStart uint32 = 1 << iota // input rune starts a segment (i.e. follows '/' or '.')
+ wordStart // input rune starts a word, per camel-case naming rules
+ separator // input rune is a separator ('/' or '.')
+ upper // input rune is an upper case letter
)
// NewSymbolMatcher creates a SymbolMatcher that may be used to match the given
@@ -61,17 +66,17 @@ func NewSymbolMatcher(pattern string) *SymbolMatcher {
return m
}
-// Match looks for the right-most match of the search pattern within the symbol
-// represented by concatenating the given chunks, returning its offset and
-// score.
+// Match searches for the right-most match of the search pattern within the
+// symbol represented by concatenating the given chunks.
//
-// If a match is found, the first return value will hold the absolute byte
-// offset within all chunks for the start of the symbol. In other words, the
-// index of the match within strings.Join(chunks, ""). If no match is found,
-// the first return value will be -1.
+// If a match is found, the first result holds the absolute byte offset within
+// all chunks for the start of the symbol. In other words, the index of the
+// match within strings.Join(chunks, "").
//
// The second return value will be the score of the match, which is always
// between 0 and 1, inclusive. A score of 0 indicates no match.
+//
+// If no match is found, Match returns (-1, 0).
func (m *SymbolMatcher) Match(chunks []string) (int, float64) {
// Explicit behavior for an empty pattern.
//
@@ -81,11 +86,25 @@ func (m *SymbolMatcher) Match(chunks []string) (int, float64) {
return -1, 0
}
- // First phase: populate the input buffer with lower-cased runes.
+ // Matching implements a heavily optimized linear scoring algorithm on the
+ // input. This is not guaranteed to produce the highest score, but works well
+ // enough, particularly due to the right-to-left significance of qualified
+ // symbols.
+ //
+ // Matching proceeds in three passes through the input:
+ // - The first pass populates the input buffer and collects rune roles.
+ // - The second pass proceeds right-to-left to find the right-most match.
+ // - The third pass proceeds left-to-right from the start of the right-most
+ // match, to find the most *compact* match, and computes the score of this
+ // match.
+ //
+ // See below for more details of each pass, as well as the scoring algorithm.
+
+ // First pass: populate the input buffer out of the provided chunks
+ // (lower-casing in the process), and collect rune roles.
//
// We could also check for a forward match here, but since we'd have to write
// the entire input anyway this has negligible impact on performance.
-
var (
inputLen = uint8(0)
modifiers = wordStart | segmentStart
@@ -107,7 +126,16 @@ input:
l = unicode.ToLower(r)
}
if l != r {
- modifiers |= wordStart
+ modifiers |= upper
+
+ // If the current rune is capitalized *and the preceding rune was not*,
+ // mark this as a word start. This avoids spuriously high ranking of
+ // non-camelcase naming schemas, such as the
+ // yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE example of
+ // golang/go#60201.
+ if inputLen == 0 || m.roles[inputLen-1]&upper == 0 {
+ modifiers |= wordStart
+ }
}
m.inputBuffer[inputLen] = l
m.roles[inputLen] = modifiers
@@ -125,14 +153,13 @@ input:
}
}
- // Second phase: find the right-most match, and count segments from the
+ // Second pass: find the right-most match, and count segments from the
// right.
-
var (
pi = uint8(m.patternLen - 1) // pattern index
p = m.pattern[pi] // pattern rune
start = -1 // start offset of match
- rseg = uint8(0)
+ rseg = uint8(0) // effective "depth" from the right of the current rune in consideration
)
const maxSeg = 3 // maximum number of segments from the right to count, for scoring purposes.
@@ -144,6 +171,8 @@ input:
m.segments[ii] = rseg
if p == r {
if pi == 0 {
+ // TODO(rfindley): BUG: the docstring for Match says that it returns an
+ // absolute byte offset, but clearly it is returning a rune offset here.
start = int(ii)
break
}
@@ -161,85 +190,120 @@ input:
return -1, 0
}
- // Third phase: find the shortest match, and compute the score.
+ // Third pass: find the shortest match and compute the score.
- // Score is the average score for each character.
+ // Score is the average score for each rune.
//
- // A character score is the multiple of:
- // 1. 1.0 if the character starts a segment or is preceded by a matching
- // character, 0.9 if the character starts a mid-segment word, else 0.6.
+ // A rune score is the multiple of:
+ // 1. The base score, which is 1.0 if the rune starts a segment, 0.9 if the
+ // rune starts a mid-segment word, else 0.6.
//
- // Note that characters preceded by a matching character get the max
- // score of 1.0 so that sequential or exact matches are preferred, even
- // if they don't start/end at a segment or word boundary. For example, a
- // match for "func" in intfuncs should have a higher score than in
- // ifunmatched.
+ // Runes preceded by a matching rune are treated the same as the start
+ // of a mid-segment word (with a 0.9 score), so that sequential or exact
+ // matches are preferred. We call this a sequential bonus.
//
- // For the final character match, the multiplier from (1) is reduced to
- // 0.9 if the next character in the input is a mid-segment word, or 0.6
- // if the next character in the input is not a word or segment start.
- // This ensures that we favor whole-word or whole-segment matches over
- // prefix matches.
+ // For the final rune match, this sequential bonus is reduced to 0.8 if
+ // the next rune in the input is a mid-segment word, or 0.7 if the next
+ // rune in the input is not a word or segment start. This ensures that
+ // we favor whole-word or whole-segment matches over prefix matches.
//
- // 2. 1.0 if the character is part of the last segment, otherwise
+ // 2. 1.0 if the rune is part of the last segment, otherwise
// 1.0-0.1*, with a max segment count of 3.
// Notably 1.0-0.1*3 = 0.7 > 0.6, so that foo/_/_/_/_ (a match very
- // early in a qualified symbol name) still scores higher than _f_o_o_
- // (a completely split match).
+ // early in a qualified symbol name) still scores higher than _f_o_o_ (a
+ // completely split match).
//
// This is a naive algorithm, but it is fast. There's lots of prior art here
// that could be leveraged. For example, we could explicitly consider
- // character distance, and exact matches of words or segments.
+ // rune distance, and exact matches of words or segments.
//
// Also note that this might not actually find the highest scoring match, as
// doing so could require a non-linear algorithm, depending on how the score
// is calculated.
+ // debugging support
+ const debug = false // enable to log debugging information
+ var (
+ runeScores []float64
+ runeIdxs []int
+ )
+
pi = 0
p = m.pattern[pi]
const (
- segStreak = 1.0 // start of segment or sequential match
- wordStreak = 0.9 // start of word match
- noStreak = 0.6
- perSegment = 0.1 // we count at most 3 segments above
+ segStartScore = 1.0 // base score of runes starting a segment
+ wordScore = 0.9 // base score of runes starting or continuing a word
+ noStreak = 0.6
+ perSegment = 0.1 // we count at most 3 segments above
)
- streakBonus := noStreak
totScore := 0.0
+ lastMatch := uint8(255)
for ii := uint8(start); ii < inputLen; ii++ {
r := m.inputBuffer[ii]
if r == p {
pi++
+ finalRune := pi >= m.patternLen
p = m.pattern[pi]
- // Note: this could be optimized with some bit operations.
+
+ baseScore := noStreak
+
+ // Calculate the sequence bonus based on preceding matches.
+ //
+ // We do this first as it is overridden by role scoring below.
+ if lastMatch == ii-1 {
+ baseScore = wordScore
+ // Reduce the sequence bonus for the final rune of the pattern based on
+ // whether it borders a new segment or word.
+ if finalRune {
+ switch {
+ case ii == inputLen-1 || m.roles[ii+1]&separator != 0:
+ // Full segment: no reduction
+ case m.roles[ii+1]&wordStart != 0:
+ baseScore = wordScore - 0.1
+ default:
+ baseScore = wordScore - 0.2
+ }
+ }
+ }
+ lastMatch = ii
+
+ // Calculate the rune's role score. If the rune starts a segment or word,
+ // this overrides the sequence score, as the rune starts a new sequence.
switch {
- case m.roles[ii]&segmentStart != 0 && segStreak > streakBonus:
- streakBonus = segStreak
- case m.roles[ii]&wordStart != 0 && wordStreak > streakBonus:
- streakBonus = wordStreak
+ case m.roles[ii]&segmentStart != 0:
+ baseScore = segStartScore
+ case m.roles[ii]&wordStart != 0:
+ baseScore = wordScore
}
- finalChar := pi >= m.patternLen
- // finalCost := 1.0
- if finalChar && streakBonus > noStreak {
- switch {
- case ii == inputLen-1 || m.roles[ii+1]&segmentStart != 0:
- // Full segment: no reduction
- case m.roles[ii+1]&wordStart != 0:
- streakBonus = wordStreak
- default:
- streakBonus = noStreak
- }
+
+ // Apply the segment-depth penalty (segments from the right).
+ runeScore := baseScore * (1.0 - float64(m.segments[ii])*perSegment)
+ if debug {
+ runeScores = append(runeScores, runeScore)
+ runeIdxs = append(runeIdxs, int(ii))
}
- totScore += streakBonus * (1.0 - float64(m.segments[ii])*perSegment)
- if finalChar {
+ totScore += runeScore
+ if finalRune {
break
}
- streakBonus = segStreak // see above: sequential characters get the max score
- } else {
- streakBonus = noStreak
}
}
+ if debug {
+ // Format rune roles and scores in line:
+ // fo[o:.52].[b:1]a[r:.6]
+ var summary bytes.Buffer
+ last := 0
+ for i, idx := range runeIdxs {
+ summary.WriteString(string(m.inputBuffer[last:idx])) // encode runes
+ fmt.Fprintf(&summary, "[%s:%.2g]", string(m.inputBuffer[idx]), runeScores[i])
+ last = idx + 1
+ }
+ summary.WriteString(string(m.inputBuffer[last:inputLen])) // encode runes
+ log.Println(summary.String())
+ }
+
return start, totScore / float64(m.patternLen)
}
diff --git a/internal/fuzzy/symbol_test.go b/internal/fuzzy/symbol_test.go
index 2a9d9b663bd..43e629d51ef 100644
--- a/internal/fuzzy/symbol_test.go
+++ b/internal/fuzzy/symbol_test.go
@@ -5,8 +5,12 @@
package fuzzy_test
import (
+ "go/ast"
+ "go/token"
+ "sort"
"testing"
+ "golang.org/x/tools/go/packages"
. "golang.org/x/tools/internal/fuzzy"
)
@@ -34,30 +38,173 @@ func TestSymbolMatchIndex(t *testing.T) {
}
func TestSymbolRanking(t *testing.T) {
- matcher := NewSymbolMatcher("test")
- // symbols to match, in ascending order of ranking.
- symbols := []string{
- "this.is.better.than.most",
- "test.foo.bar",
- "thebest",
- "test.foo",
- "test.foo",
- "atest",
- "testage",
- "tTest",
- "foo.test",
- "test",
+ // query -> symbols to match, in ascending order of score
+ queryRanks := map[string][]string{
+ "test": {
+ "this.is.better.than.most",
+ "test.foo.bar",
+ "thebest",
+ "atest",
+ "test.foo",
+ "testage",
+ "tTest",
+ "foo.test",
+ },
+ "parseside": { // golang/go#60201
+ "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE",
+ "parseContext.parse_sidebyside",
+ },
+ "cvb": {
+ "filecache_test.testIPCValueB",
+ "cover.Boundary",
+ },
+ "dho": {
+ "gocommand.DebugHangingGoCommands",
+ "protocol.DocumentHighlightOptions",
+ },
+ "flg": {
+ "completion.FALLTHROUGH",
+ "main.flagGoCmd",
+ },
+ "fvi": {
+ "godoc.fileIndexVersion",
+ "macho.FlagSubsectionsViaSymbols",
+ },
}
- prev := 0.0
- for _, sym := range symbols {
- _, score := matcher.Match([]string{sym})
- t.Logf("Match(%q) = %v", sym, score)
- if score < prev {
- t.Errorf("Match(%q) = _, %v, want > %v", sym, score, prev)
+
+ for query, symbols := range queryRanks {
+ t.Run(query, func(t *testing.T) {
+ matcher := NewSymbolMatcher(query)
+ prev := 0.0
+ for _, sym := range symbols {
+ _, score := matcher.Match([]string{sym})
+ t.Logf("Match(%q) = %v", sym, score)
+ if score <= prev {
+ t.Errorf("Match(%q) = _, %v, want > %v", sym, score, prev)
+ }
+ prev = score
+ }
+ })
+ }
+}
+
+func TestMatcherSimilarities(t *testing.T) {
+ // This test compares the fuzzy matcher with the symbol matcher on a corpus
+ // of qualified identifiers extracted from x/tools.
+ //
+ // These two matchers are not expected to agree, but inspecting differences
+ // can be useful for finding interesting ranking edge cases.
+ t.Skip("unskip this test to compare matchers")
+
+ idents := collectIdentifiers(t)
+ t.Logf("collected %d unique identifiers", len(idents))
+
+ // TODO: use go1.21 slices.MaxFunc.
+ topMatch := func(score func(string) float64) string {
+ top := ""
+ topScore := 0.0
+ for _, cand := range idents {
+ if s := score(cand); s > topScore {
+ top = cand
+ topScore = s
+ }
}
- prev = score
+ return top
}
+
+ agreed := 0
+ total := 0
+ bad := 0
+ patterns := generatePatterns()
+ for _, pattern := range patterns {
+ total++
+
+ fm := NewMatcher(pattern)
+ topFuzzy := topMatch(func(input string) float64 {
+ return float64(fm.Score(input))
+ })
+ sm := NewSymbolMatcher(pattern)
+ topSymbol := topMatch(func(input string) float64 {
+ _, score := sm.Match([]string{input})
+ return score
+ })
+ switch {
+ case topFuzzy == "" && topSymbol != "":
+ if false {
+ // The fuzzy matcher has a bug where it misses some matches; for this
+ // test we only care about the symbol matcher.
+ t.Logf("%q matched %q but no fuzzy match", pattern, topSymbol)
+ }
+ total--
+ bad++
+ case topFuzzy != "" && topSymbol == "":
+ t.Fatalf("%q matched %q but no symbol match", pattern, topFuzzy)
+ case topFuzzy == topSymbol:
+ agreed++
+ default:
+ // Enable this log to see mismatches.
+ if false {
+ t.Logf("mismatch for %q: fuzzy: %q, symbol: %q", pattern, topFuzzy, topSymbol)
+ }
+ }
+ }
+ t.Logf("fuzzy matchers agreed on %d out of %d queries (%d bad)", agreed, total, bad)
+}
+
+func collectIdentifiers(tb testing.TB) []string {
+ cfg := &packages.Config{
+ Mode: packages.NeedName | packages.NeedSyntax | packages.NeedFiles,
+ Tests: true,
+ }
+ pkgs, err := packages.Load(cfg, "golang.org/x/tools/...")
+ if err != nil {
+ tb.Fatal(err)
+ }
+ uniqueIdents := make(map[string]bool)
+ decls := 0
+ for _, pkg := range pkgs {
+ for _, f := range pkg.Syntax {
+ for _, decl := range f.Decls {
+ decls++
+ switch decl := decl.(type) {
+ case *ast.GenDecl:
+ for _, spec := range decl.Specs {
+ switch decl.Tok {
+ case token.IMPORT:
+ case token.TYPE:
+ name := spec.(*ast.TypeSpec).Name.Name
+ qualified := pkg.Name + "." + name
+ uniqueIdents[qualified] = true
+ case token.CONST, token.VAR:
+ for _, n := range spec.(*ast.ValueSpec).Names {
+ qualified := pkg.Name + "." + n.Name
+ uniqueIdents[qualified] = true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ var idents []string
+ for k := range uniqueIdents {
+ idents = append(idents, k)
+ }
+ sort.Strings(idents)
+ return idents
+}
+
+func generatePatterns() []string {
+ var patterns []string
+ for x := 'a'; x <= 'z'; x++ {
+ for y := 'a'; y <= 'z'; y++ {
+ for z := 'a'; z <= 'z'; z++ {
+ patterns = append(patterns, string(x)+string(y)+string(z))
+ }
+ }
+ }
+ return patterns
}
// Test that we strongly prefer exact matches.
@@ -89,9 +236,8 @@ func TestSymbolRanking_Issue60027(t *testing.T) {
func TestChunkedMatch(t *testing.T) {
matcher := NewSymbolMatcher("test")
-
+ _, want := matcher.Match([]string{"test"})
chunked := [][]string{
- {"test"},
{"", "test"},
{"test", ""},
{"te", "st"},
@@ -99,7 +245,7 @@ func TestChunkedMatch(t *testing.T) {
for _, chunks := range chunked {
offset, score := matcher.Match(chunks)
- if offset != 0 || score != 1.0 {
+ if offset != 0 || score != want {
t.Errorf("Match(%v) = %v, %v, want 0, 1.0", chunks, offset, score)
}
}
diff --git a/internal/gcimporter/gcimporter.go b/internal/gcimporter/gcimporter.go
index b1223713b94..2d078ccb19c 100644
--- a/internal/gcimporter/gcimporter.go
+++ b/internal/gcimporter/gcimporter.go
@@ -29,7 +29,6 @@ import (
"go/token"
"go/types"
"io"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -221,7 +220,7 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func
switch hdr {
case "$$B\n":
var data []byte
- data, err = ioutil.ReadAll(buf)
+ data, err = io.ReadAll(buf)
if err != nil {
break
}
diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go
index 1407e90849e..3af088b23d8 100644
--- a/internal/gcimporter/gcimporter_test.go
+++ b/internal/gcimporter/gcimporter_test.go
@@ -17,7 +17,6 @@ import (
goparser "go/parser"
"go/token"
"go/types"
- "io/ioutil"
"os"
"os/exec"
"path"
@@ -105,7 +104,7 @@ func testPath(t *testing.T, path, srcDir string) *types.Package {
}
func mktmpdir(t *testing.T) string {
- tmpdir, err := ioutil.TempDir("", "gcimporter_test")
+ tmpdir, err := os.MkdirTemp("", "gcimporter_test")
if err != nil {
t.Fatal("mktmpdir:", err)
}
@@ -286,7 +285,7 @@ func TestVersionHandling(t *testing.T) {
needsCompiler(t, "gc")
const dir = "./testdata/versions"
- list, err := ioutil.ReadDir(dir)
+ list, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
@@ -321,7 +320,7 @@ func TestVersionHandling(t *testing.T) {
// create file with corrupted export data
// 1) read file
- data, err := ioutil.ReadFile(filepath.Join(dir, name))
+ data, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
t.Fatal(err)
}
@@ -338,7 +337,7 @@ func TestVersionHandling(t *testing.T) {
// 4) write the file
pkgpath += "_corrupted"
filename := filepath.Join(corruptdir, pkgpath) + ".a"
- ioutil.WriteFile(filename, data, 0666)
+ os.WriteFile(filename, data, 0666)
// test that importing the corrupted file results in an error
_, err = Import(make(map[string]*types.Package), pkgpath, corruptdir, nil)
diff --git a/internal/gcimporter/iexport_test.go b/internal/gcimporter/iexport_test.go
index 7f77a796077..4ee79dac9d0 100644
--- a/internal/gcimporter/iexport_test.go
+++ b/internal/gcimporter/iexport_test.go
@@ -19,7 +19,7 @@ import (
"go/parser"
"go/token"
"go/types"
- "io/ioutil"
+ "io"
"math/big"
"os"
"reflect"
@@ -55,7 +55,7 @@ func readExportFile(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected byte: %v", ch)
}
- return ioutil.ReadAll(buf)
+ return io.ReadAll(buf)
}
func iexport(fset *token.FileSet, version int, pkg *types.Package) ([]byte, error) {
diff --git a/internal/gopathwalk/walk_test.go b/internal/gopathwalk/walk_test.go
index fa4ebdc32b2..58abdcff6b3 100644
--- a/internal/gopathwalk/walk_test.go
+++ b/internal/gopathwalk/walk_test.go
@@ -5,7 +5,6 @@
package gopathwalk
import (
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -22,7 +21,7 @@ func TestShouldTraverse(t *testing.T) {
t.Skipf("skipping symlink-requiring test on %s", runtime.GOOS)
}
- dir, err := ioutil.TempDir("", "goimports-")
+ dir, err := os.MkdirTemp("", "goimports-")
if err != nil {
t.Fatal(err)
}
@@ -90,7 +89,7 @@ func TestShouldTraverse(t *testing.T) {
// TestSkip tests that various goimports rules are followed in non-modules mode.
func TestSkip(t *testing.T) {
- dir, err := ioutil.TempDir("", "goimports-")
+ dir, err := os.MkdirTemp("", "goimports-")
if err != nil {
t.Fatal(err)
}
@@ -125,7 +124,7 @@ func TestSkip(t *testing.T) {
// TestSkipFunction tests that scan successfully skips directories from user callback.
func TestSkipFunction(t *testing.T) {
- dir, err := ioutil.TempDir("", "goimports-")
+ dir, err := os.MkdirTemp("", "goimports-")
if err != nil {
t.Fatal(err)
}
@@ -165,7 +164,7 @@ func mapToDir(destDir string, files map[string]string) error {
if strings.HasPrefix(contents, "LINK:") {
err = os.Symlink(strings.TrimPrefix(contents, "LINK:"), file)
} else {
- err = ioutil.WriteFile(file, []byte(contents), 0644)
+ err = os.WriteFile(file, []byte(contents), 0644)
}
if err != nil {
return err
diff --git a/internal/imports/fix.go b/internal/imports/fix.go
index d4f1b4e8a0f..01e8ba5fa2d 100644
--- a/internal/imports/fix.go
+++ b/internal/imports/fix.go
@@ -13,6 +13,7 @@ import (
"go/build"
"go/parser"
"go/token"
+ "io/fs"
"io/ioutil"
"os"
"path"
@@ -107,7 +108,7 @@ func parseOtherFiles(fset *token.FileSet, srcDir, filename string) []*ast.File {
considerTests := strings.HasSuffix(filename, "_test.go")
fileBase := filepath.Base(filename)
- packageFileInfos, err := ioutil.ReadDir(srcDir)
+ packageFileInfos, err := os.ReadDir(srcDir)
if err != nil {
return nil
}
@@ -1469,11 +1470,11 @@ func VendorlessPath(ipath string) string {
func loadExportsFromFiles(ctx context.Context, env *ProcessEnv, dir string, includeTest bool) (string, []string, error) {
// Look for non-test, buildable .go files which could provide exports.
- all, err := ioutil.ReadDir(dir)
+ all, err := os.ReadDir(dir)
if err != nil {
return "", nil, err
}
- var files []os.FileInfo
+ var files []fs.DirEntry
for _, fi := range all {
name := fi.Name()
if !strings.HasSuffix(name, ".go") || (!includeTest && strings.HasSuffix(name, "_test.go")) {
diff --git a/internal/imports/fix_test.go b/internal/imports/fix_test.go
index ba81affdb14..7096ff25c56 100644
--- a/internal/imports/fix_test.go
+++ b/internal/imports/fix_test.go
@@ -9,8 +9,8 @@ import (
"flag"
"fmt"
"go/build"
- "io/ioutil"
"log"
+ "os"
"path/filepath"
"reflect"
"sort"
@@ -1700,7 +1700,7 @@ func (t *goimportTest) process(module, file string, contents []byte, opts *Optio
func (t *goimportTest) processNonModule(file string, contents []byte, opts *Options) ([]byte, error) {
if contents == nil {
var err error
- contents, err = ioutil.ReadFile(file)
+ contents, err = os.ReadFile(file)
if err != nil {
return nil, err
}
diff --git a/internal/imports/mkindex.go b/internal/imports/mkindex.go
index 36a532b0ca3..2ecc9e45e9f 100644
--- a/internal/imports/mkindex.go
+++ b/internal/imports/mkindex.go
@@ -19,7 +19,6 @@ import (
"go/format"
"go/parser"
"go/token"
- "io/ioutil"
"log"
"os"
"path"
@@ -88,7 +87,7 @@ func main() {
}
// Write out source file.
- err = ioutil.WriteFile("pkgindex.go", src, 0644)
+ err = os.WriteFile("pkgindex.go", src, 0644)
if err != nil {
log.Fatal(err)
}
diff --git a/internal/imports/mkstdlib.go b/internal/imports/mkstdlib.go
index 470b93f1df2..3896872c234 100644
--- a/internal/imports/mkstdlib.go
+++ b/internal/imports/mkstdlib.go
@@ -17,7 +17,6 @@ import (
"go/format"
"go/token"
"io"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -101,7 +100,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
- err = ioutil.WriteFile("zstdlib.go", fmtbuf, 0666)
+ err = os.WriteFile("zstdlib.go", fmtbuf, 0666)
if err != nil {
log.Fatal(err)
}
diff --git a/internal/imports/mod.go b/internal/imports/mod.go
index 977d2389da1..5f4d435d3cc 100644
--- a/internal/imports/mod.go
+++ b/internal/imports/mod.go
@@ -9,7 +9,6 @@ import (
"context"
"encoding/json"
"fmt"
- "io/ioutil"
"os"
"path"
"path/filepath"
@@ -265,7 +264,7 @@ func (r *ModuleResolver) findPackage(importPath string) (*gocommand.ModuleJSON,
}
// Not cached. Read the filesystem.
- pkgFiles, err := ioutil.ReadDir(pkgDir)
+ pkgFiles, err := os.ReadDir(pkgDir)
if err != nil {
continue
}
@@ -370,7 +369,7 @@ func (r *ModuleResolver) dirIsNestedModule(dir string, mod *gocommand.ModuleJSON
func (r *ModuleResolver) modInfo(dir string) (modDir string, modName string) {
readModName := func(modFile string) string {
- modBytes, err := ioutil.ReadFile(modFile)
+ modBytes, err := os.ReadFile(modFile)
if err != nil {
return ""
}
diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go
index 46831d46623..26dac639062 100644
--- a/internal/imports/mod_test.go
+++ b/internal/imports/mod_test.go
@@ -8,7 +8,6 @@ import (
"archive/zip"
"context"
"fmt"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -197,7 +196,7 @@ import _ "rsc.io/quote"
if err := os.Chmod(filepath.Join(found.dir, "go.mod"), 0644); err != nil {
t.Fatal(err)
}
- if err := ioutil.WriteFile(filepath.Join(found.dir, "go.mod"), []byte("module bad.com\n"), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(found.dir, "go.mod"), []byte("module bad.com\n"), 0644); err != nil {
t.Fatal(err)
}
@@ -205,10 +204,10 @@ import _ "rsc.io/quote"
mt.assertScanFinds("rsc.io/quote", "quote")
// Rewrite the main package so that rsc.io/quote is not in scope.
- if err := ioutil.WriteFile(filepath.Join(mt.env.WorkingDir, "go.mod"), []byte("module x\n"), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(mt.env.WorkingDir, "go.mod"), []byte("module x\n"), 0644); err != nil {
t.Fatal(err)
}
- if err := ioutil.WriteFile(filepath.Join(mt.env.WorkingDir, "x.go"), []byte("package x\n"), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(mt.env.WorkingDir, "x.go"), []byte("package x\n"), 0644); err != nil {
t.Fatal(err)
}
@@ -1000,7 +999,7 @@ func setup(t *testing.T, extraEnv map[string]string, main, wd string) *modTest {
proxyOnce.Do(func() {
var err error
- proxyDir, err = ioutil.TempDir("", "proxy-")
+ proxyDir, err = os.MkdirTemp("", "proxy-")
if err != nil {
t.Fatal(err)
}
@@ -1009,7 +1008,7 @@ func setup(t *testing.T, extraEnv map[string]string, main, wd string) *modTest {
}
})
- dir, err := ioutil.TempDir("", t.Name())
+ dir, err := os.MkdirTemp("", t.Name())
if err != nil {
t.Fatal(err)
}
@@ -1070,7 +1069,7 @@ func writeModule(dir, ar string) error {
return err
}
- if err := ioutil.WriteFile(fpath, f.Data, 0644); err != nil {
+ if err := os.WriteFile(fpath, f.Data, 0644); err != nil {
return err
}
}
@@ -1080,7 +1079,7 @@ func writeModule(dir, ar string) error {
// writeProxy writes all the txtar-formatted modules in arDir to a proxy
// directory in dir.
func writeProxy(dir, arDir string) error {
- files, err := ioutil.ReadDir(arDir)
+ files, err := os.ReadDir(arDir)
if err != nil {
return err
}
@@ -1123,7 +1122,7 @@ func writeProxyModule(base, arPath string) error {
z := zip.NewWriter(f)
for _, f := range a.Files {
if f.Name[0] == '.' {
- if err := ioutil.WriteFile(filepath.Join(dir, ver+f.Name), f.Data, 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(dir, ver+f.Name), f.Data, 0644); err != nil {
return err
}
} else {
@@ -1194,7 +1193,7 @@ import _ "rsc.io/quote"
func TestInvalidModCache(t *testing.T) {
testenv.NeedsTool(t, "go")
- dir, err := ioutil.TempDir("", t.Name())
+ dir, err := os.MkdirTemp("", t.Name())
if err != nil {
t.Fatal(err)
}
@@ -1204,7 +1203,7 @@ func TestInvalidModCache(t *testing.T) {
if err := os.MkdirAll(filepath.Join(dir, "gopath/pkg/mod/sabotage"), 0777); err != nil {
t.Fatal(err)
}
- if err := ioutil.WriteFile(filepath.Join(dir, "gopath/pkg/mod/sabotage/x.go"), []byte("package foo\n"), 0777); err != nil {
+ if err := os.WriteFile(filepath.Join(dir, "gopath/pkg/mod/sabotage/x.go"), []byte("package foo\n"), 0777); err != nil {
t.Fatal(err)
}
env := &ProcessEnv{
diff --git a/internal/persistent/map.go b/internal/persistent/map.go
index 02389f89dc5..64cd500c65a 100644
--- a/internal/persistent/map.go
+++ b/internal/persistent/map.go
@@ -30,8 +30,8 @@ import (
// Map is an associative mapping from keys to values.
//
// Maps can be Cloned in constant time.
-// Get, Store, and Delete operations are done on average in logarithmic time.
-// Maps can be Updated in O(m log(n/m)) time for maps of size n and m, where m < n.
+// Get, Set, and Delete operations are done on average in logarithmic time.
+// Maps can be merged (via SetAll) in O(m log(n/m)) time for maps of size n and m, where m < n.
//
// Values are reference counted, and a client-supplied release function
// is called when a value is no longer referenced by a map or any clone.
diff --git a/internal/proxydir/proxydir.go b/internal/proxydir/proxydir.go
index 5180204064b..ffec81c264c 100644
--- a/internal/proxydir/proxydir.go
+++ b/internal/proxydir/proxydir.go
@@ -11,7 +11,6 @@ import (
"archive/zip"
"fmt"
"io"
- "io/ioutil"
"os"
"path/filepath"
"strings"
@@ -44,13 +43,13 @@ func WriteModuleVersion(rootDir, module, ver string, files map[string][]byte) (r
if !ok {
modContents = []byte("module " + module)
}
- if err := ioutil.WriteFile(filepath.Join(dir, ver+".mod"), modContents, 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(dir, ver+".mod"), modContents, 0644); err != nil {
return err
}
// info file, just the bare bones.
infoContents := []byte(fmt.Sprintf(`{"Version": "%v", "Time":"2017-12-14T13:08:43Z"}`, ver))
- if err := ioutil.WriteFile(filepath.Join(dir, ver+".info"), infoContents, 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(dir, ver+".info"), infoContents, 0644); err != nil {
return err
}
diff --git a/internal/proxydir/proxydir_test.go b/internal/proxydir/proxydir_test.go
index 54401fb1647..c8137229b04 100644
--- a/internal/proxydir/proxydir_test.go
+++ b/internal/proxydir/proxydir_test.go
@@ -7,7 +7,7 @@ package proxydir
import (
"archive/zip"
"fmt"
- "io/ioutil"
+ "io"
"os"
"path/filepath"
"strings"
@@ -43,7 +43,7 @@ func TestWriteModuleVersion(t *testing.T) {
},
},
}
- dir, err := ioutil.TempDir("", "proxydirtest-")
+ dir, err := os.MkdirTemp("", "proxydirtest-")
if err != nil {
t.Fatal(err)
}
@@ -54,7 +54,7 @@ func TestWriteModuleVersion(t *testing.T) {
t.Fatal(err)
}
rootDir := filepath.Join(dir, filepath.FromSlash(test.modulePath), "@v")
- gomod, err := ioutil.ReadFile(filepath.Join(rootDir, test.version+".mod"))
+ gomod, err := os.ReadFile(filepath.Join(rootDir, test.version+".mod"))
if err != nil {
t.Fatal(err)
}
@@ -77,7 +77,7 @@ func TestWriteModuleVersion(t *testing.T) {
t.Fatal(err)
}
defer r.Close()
- content, err := ioutil.ReadAll(r)
+ content, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
@@ -101,7 +101,7 @@ func TestWriteModuleVersion(t *testing.T) {
for _, test := range lists {
fp := filepath.Join(dir, filepath.FromSlash(test.modulePath), "@v", "list")
- list, err := ioutil.ReadFile(fp)
+ list, err := os.ReadFile(fp)
if err != nil {
t.Fatal(err)
}
diff --git a/internal/refactor/inline/analyzer/analyzer.go b/internal/refactor/inline/analyzer/analyzer.go
index 2356fa484e7..bb7d9d0d512 100644
--- a/internal/refactor/inline/analyzer/analyzer.go
+++ b/internal/refactor/inline/analyzer/analyzer.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.20
+
package analyzer
import (
@@ -70,7 +72,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err)
continue
}
- callee, err := inline.AnalyzeCallee(pass.Fset, pass.Pkg, pass.TypesInfo, decl, content)
+ 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)
continue
@@ -124,7 +126,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
Call: call,
Content: content,
}
- got, err := inline.Inline(caller, callee)
+ got, err := inline.Inline(discard, caller, callee)
if err != nil {
pass.Reportf(call.Lparen, "%v", err)
return
@@ -159,3 +161,5 @@ type inlineMeFact struct{ callee *inline.Callee }
func (f *inlineMeFact) String() string { return "inlineme " + f.callee.String() }
func (*inlineMeFact) AFact() {}
+
+func discard(string, ...any) {}
diff --git a/internal/refactor/inline/analyzer/analyzer_test.go b/internal/refactor/inline/analyzer/analyzer_test.go
index 5ad85cfb821..05daac901f7 100644
--- a/internal/refactor/inline/analyzer/analyzer_test.go
+++ b/internal/refactor/inline/analyzer/analyzer_test.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.20
+
package analyzer_test
import (
diff --git a/internal/refactor/inline/analyzer/testdata/src/a/a.go b/internal/refactor/inline/analyzer/testdata/src/a/a.go
index e661515b7c7..294278670f2 100644
--- a/internal/refactor/inline/analyzer/testdata/src/a/a.go
+++ b/internal/refactor/inline/analyzer/testdata/src/a/a.go
@@ -1,7 +1,8 @@
package a
func f() {
- One() // want `inline call of a.One`
+ One() // want `inline call of a.One`
+
new(T).Two() // want `inline call of \(a.T\).Two`
}
diff --git a/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden b/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden
index fe9877b69c1..1a214fc9148 100644
--- a/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden
+++ b/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden
@@ -1,8 +1,9 @@
package a
func f() {
- _ = one // want `inline call of a.One`
- func(_ T) int { return 2 }(*new(T)) // want `inline call of \(a.T\).Two`
+ _ = one // want `inline call of a.One`
+
+ _ = 2 // want `inline call of \(a.T\).Two`
}
type T struct{}
diff --git a/internal/refactor/inline/analyzer/testdata/src/b/b.go.golden b/internal/refactor/inline/analyzer/testdata/src/b/b.go.golden
index 61b7bd9b349..b871b4b5100 100644
--- a/internal/refactor/inline/analyzer/testdata/src/b/b.go.golden
+++ b/internal/refactor/inline/analyzer/testdata/src/b/b.go.golden
@@ -5,5 +5,5 @@ import "a"
func f() {
a.One() // want `cannot inline call to a.One because body refers to non-exported one`
- func(_ a.T) int { return 2 }(*new(a.T)) // want `inline call of \(a.T\).Two`
+ _ = 2 // want `inline call of \(a.T\).Two`
}
diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go
index 291971cf6d8..dc74eab4e1a 100644
--- a/internal/refactor/inline/callee.go
+++ b/internal/refactor/inline/callee.go
@@ -14,6 +14,7 @@ import (
"go/parser"
"go/token"
"go/types"
+ "strings"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/typeutil"
@@ -30,37 +31,41 @@ func (callee *Callee) String() string { return callee.impl.Name }
type gobCallee struct {
Content []byte // file content, compacted to a single func decl
- // syntax derived from compacted Content (not serialized)
- fset *token.FileSet
- decl *ast.FuncDecl
-
// results of type analysis (does not reach go/types data structures)
- PkgPath string // package path of declaring package
- Name string // user-friendly name for error messages
- Unexported []string // names of free objects that are unexported
- FreeRefs []freeRef // locations of references to free objects
- FreeObjs []object // descriptions of free objects
- BodyIsReturnExpr bool // function body is "return expr(s)"
- ValidForCallStmt bool // => bodyIsReturnExpr and sole expr is f() or <-ch
- NumResults int // number of results (according to type, not ast.FieldList)
+ PkgPath string // package path of declaring package
+ Name string // user-friendly name for error messages
+ Unexported []string // names of free objects that are unexported
+ FreeRefs []freeRef // locations of references to free objects
+ FreeObjs []object // descriptions of free objects
+ ValidForCallStmt bool // function body is "return expr" where expr is f() or <-ch
+ NumResults int // number of results (according to type, not ast.FieldList)
+ Params []*paramInfo // information about parameters (incl. receiver)
+ Results []*paramInfo // information about result variables
+ Effects []int // order in which parameters are evaluated (see calleefx)
+ HasDefer bool // uses defer
+ HasBareReturn bool // uses bare return in non-void function
+ TotalReturns int // number of return statements
+ TrivialReturns int // number of return statements with trivial result conversions
+ Labels []string // names of all control labels
+ Falcon falconResult // falcon constraint system
}
// A freeRef records a reference to a free object. Gob-serializable.
+// (This means free relative to the FuncDecl as a whole, i.e. excluding parameters.)
type freeRef struct {
- Start, End int // Callee.content[start:end] is extent of the reference
- Object int // index into Callee.freeObjs
+ Offset int // byte offset of the reference relative to the FuncDecl
+ Object int // index into Callee.freeObjs
}
// An object abstracts a free types.Object referenced by the callee. Gob-serializable.
type object struct {
- Name string // Object.Name()
- Kind string // one of {var,func,const,type,pkgname,nil,builtin}
- PkgPath string // pkgpath of object (or of imported package if kind="pkgname")
- ValidPos bool // Object.Pos().IsValid()
+ Name string // Object.Name()
+ Kind string // one of {var,func,const,type,pkgname,nil,builtin}
+ PkgPath string // pkgpath of object (or of imported package if kind="pkgname")
+ ValidPos bool // Object.Pos().IsValid()
+ Shadow map[string]bool // names shadowed at one of the object's refs
}
-func (callee *gobCallee) offset(pos token.Pos) int { return offsetOf(callee.fset, pos) }
-
// AnalyzeCallee analyzes a function that is a candidate for inlining
// and returns a Callee that describes it. The Callee object, which is
// serializable, can be passed to one or more subsequent calls to
@@ -69,13 +74,20 @@ func (callee *gobCallee) offset(pos token.Pos) int { return offsetOf(callee.fset
// This design allows separate analysis of callers and callees in the
// golang.org/x/tools/go/analysis framework: the inlining information
// about a callee can be recorded as a "fact".
-func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, decl *ast.FuncDecl, content []byte) (*Callee, error) {
+//
+// The content should be the actual input to the compiler, not the
+// apparent source file according to any //line directives that
+// may be present within it.
+func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Package, info *types.Info, decl *ast.FuncDecl, content []byte) (*Callee, error) {
+ checkInfoFields(info)
// The client is expected to have determined that the callee
// is a function with a declaration (not a built-in or var).
fn := info.Defs[decl.Name].(*types.Func)
sig := fn.Type().(*types.Signature)
+ logf("analyzeCallee %v @ %v", fn, fset.PositionFor(decl.Pos(), false))
+
// Create user-friendly name ("pkg.Func" or "(pkg.T).Method")
var name string
if sig.Recv() == nil {
@@ -95,19 +107,28 @@ func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, de
// ident or qualified ident to prevent "if x == struct{}"
// parsing ambiguity, or "T(x)" where T = "*int" or "func()"
// from misparsing.
- if decl.Type.TypeParams != nil {
+ if funcHasTypeParams(decl) {
return nil, fmt.Errorf("cannot inline generic function %s: type parameters are not yet supported", name)
}
- // Record the location of all free references in the callee body.
+ // Record the location of all free references in the FuncDecl.
+ // (Parameters are not free by this definition.)
var (
freeObjIndex = make(map[types.Object]int)
freeObjs []object
freeRefs []freeRef // free refs that may need renaming
unexported []string // free refs to unexported objects, for later error checks
)
- var visit func(n ast.Node) bool
- visit = func(n ast.Node) bool {
+ var f func(n ast.Node) bool
+ visit := func(n ast.Node) { ast.Inspect(n, f) }
+ var stack []ast.Node
+ stack = append(stack, decl.Type) // for scope of function itself
+ f = func(n ast.Node) bool {
+ if n != nil {
+ stack = append(stack, n) // push
+ } else {
+ stack = stack[:len(stack)-1] // pop
+ }
switch n := n.(type) {
case *ast.SelectorExpr:
// Check selections of free fields/methods.
@@ -127,6 +148,9 @@ func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, de
// whether keyed or unkeyed. (Logic assumes well-typedness.)
litType := deref(info.TypeOf(n))
if s, ok := typeparams.CoreType(litType).(*types.Struct); ok {
+ if n.Type != nil {
+ visit(n.Type)
+ }
for i, elt := range n.Elts {
var field *types.Var
var value ast.Expr
@@ -163,8 +187,8 @@ func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, de
unexported = append(unexported, n.Name)
}
- // Record free reference.
- if !within(obj.Pos(), decl) {
+ // Record free reference (incl. self-reference).
+ if obj == fn || !within(obj.Pos(), decl) {
objidx, ok := freeObjIndex[obj]
if !ok {
objidx = len(freeObjIndex)
@@ -182,9 +206,11 @@ func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, de
})
freeObjIndex[obj] = objidx
}
+
+ freeObjs[objidx].Shadow = addShadows(freeObjs[objidx].Shadow, info, obj.Name(), stack)
+
freeRefs = append(freeRefs, freeRef{
- Start: offsetOf(fset, n.Pos()),
- End: offsetOf(fset, n.End()),
+ Offset: int(n.Pos() - decl.Pos()),
Object: objidx,
})
}
@@ -192,44 +218,36 @@ func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, de
}
return true
}
- ast.Inspect(decl, visit)
+ visit(decl)
- // Analyze callee body for "return results" form, where
- // results is one or more expressions or an n-ary call.
+ // Analyze callee body for "return expr" form,
+ // where expr is f() or <-ch. These forms are
+ // safe to inline as a standalone statement.
validForCallStmt := false
- bodyIsReturnExpr := decl.Type.Results != nil && len(decl.Type.Results.List) > 0 &&
- len(decl.Body.List) == 1 &&
- is[*ast.ReturnStmt](decl.Body.List[0]) &&
- len(decl.Body.List[0].(*ast.ReturnStmt).Results) > 0
- if bodyIsReturnExpr {
- ret := decl.Body.List[0].(*ast.ReturnStmt)
-
- // Ascertain whether the results expression(s)
- // would be safe to inline as a standalone statement.
- // (This is true only for a single call or receive expression.)
+ if len(decl.Body.List) != 1 {
+ // not just a return statement
+ } else if ret, ok := decl.Body.List[0].(*ast.ReturnStmt); ok && len(ret.Results) == 1 {
validForCallStmt = func() bool {
- if len(ret.Results) == 1 {
- switch expr := astutil.Unparen(ret.Results[0]).(type) {
- case *ast.CallExpr: // f(x)
- callee := typeutil.Callee(info, expr)
- if callee == nil {
- return false // conversion T(x)
- }
+ switch expr := astutil.Unparen(ret.Results[0]).(type) {
+ case *ast.CallExpr: // f(x)
+ callee := typeutil.Callee(info, expr)
+ if callee == nil {
+ return false // conversion T(x)
+ }
- // The only non-void built-in functions that may be
- // called as a statement are copy and recover
- // (though arguably a call to recover should never
- // be inlined as that changes its behavior).
- if builtin, ok := callee.(*types.Builtin); ok {
- return builtin.Name() == "copy" ||
- builtin.Name() == "recover"
- }
+ // The only non-void built-in functions that may be
+ // called as a statement are copy and recover
+ // (though arguably a call to recover should never
+ // be inlined as that changes its behavior).
+ if builtin, ok := callee.(*types.Builtin); ok {
+ return builtin.Name() == "copy" ||
+ builtin.Name() == "recover"
+ }
- return true // ordinary call f()
+ return true // ordinary call f()
- case *ast.UnaryExpr: // <-x
- return expr.Op == token.ARROW // channel receive <-ch
- }
+ case *ast.UnaryExpr: // <-x
+ return expr.Op == token.ARROW // channel receive <-ch
}
// No other expressions are valid statements.
@@ -237,65 +255,239 @@ func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, de
}()
}
+ // Record information about control flow in the callee
+ // (but not any nested functions).
+ var (
+ hasDefer = false
+ hasBareReturn = false
+ totalReturns = 0
+ trivialReturns = 0
+ labels []string
+ )
+ ast.Inspect(decl.Body, func(n ast.Node) bool {
+ switch n := n.(type) {
+ case *ast.FuncLit:
+ return false // prune traversal
+ case *ast.DeferStmt:
+ hasDefer = true
+ case *ast.LabeledStmt:
+ labels = append(labels, n.Label.Name)
+ case *ast.ReturnStmt:
+ totalReturns++
+
+ // Are implicit assignment conversions
+ // to result variables all trivial?
+ trivial := true
+ if len(n.Results) > 0 {
+ argType := func(i int) types.Type {
+ return info.TypeOf(n.Results[i])
+ }
+ if len(n.Results) == 1 && sig.Results().Len() > 1 {
+ // Spread return: return f() where f.Results > 1.
+ tuple := info.TypeOf(n.Results[0]).(*types.Tuple)
+ argType = func(i int) types.Type {
+ return tuple.At(i).Type()
+ }
+ }
+ for i := 0; i < sig.Results().Len(); i++ {
+ if !trivialConversion(argType(i), sig.Results().At(i)) {
+ trivial = false
+ break
+ }
+ }
+ } else if sig.Results().Len() > 0 {
+ hasBareReturn = true
+ }
+ if trivial {
+ trivialReturns++
+ }
+ }
+ return true
+ })
+
+ // Reject attempts to inline cgo-generated functions.
+ for _, obj := range freeObjs {
+ // There are others (iconst fconst sconst fpvar macro)
+ // but this is probably sufficient.
+ if strings.HasPrefix(obj.Name, "_Cfunc_") ||
+ strings.HasPrefix(obj.Name, "_Ctype_") ||
+ strings.HasPrefix(obj.Name, "_Cvar_") {
+ return nil, fmt.Errorf("cannot inline cgo-generated functions")
+ }
+ }
+
+ // Compact content to just the FuncDecl.
+ //
// As a space optimization, we don't retain the complete
// callee file content; all we need is "package _; func f() { ... }".
// This reduces the size of analysis facts.
//
- // The FileSet file/line info is no longer meaningful
- // and should not be used in error messages.
- // But the FileSet offsets are valid w.r.t. the content.
- //
- // (For ease of debugging we could insert a //line directive after
- // the package decl but it seems more trouble than it's worth.)
- {
- start, end := offsetOf(fset, decl.Pos()), offsetOf(fset, decl.End())
-
- var compact bytes.Buffer
- compact.WriteString("package _\n")
- compact.Write(content[start:end])
- content = compact.Bytes()
-
- // Re-parse the compacted content.
- var err error
- decl, err = parseCompact(fset, content)
- if err != nil {
- return nil, err
- }
-
- // (content, decl) are now updated.
+ // Offsets in the callee information are "relocatable"
+ // since they are all relative to the FuncDecl.
- // Adjust the freeRefs offsets.
- delta := int(offsetOf(fset, decl.Pos()) - start)
- for i := range freeRefs {
- freeRefs[i].Start += delta
- freeRefs[i].End += delta
- }
+ content = append([]byte("package _\n"),
+ content[offsetOf(fset, decl.Pos()):offsetOf(fset, decl.End())]...)
+ // Sanity check: re-parse the compacted content.
+ if _, _, err := parseCompact(content); err != nil {
+ return nil, err
}
+ params, results, effects, falcon := analyzeParams(logf, fset, info, decl)
return &Callee{gobCallee{
Content: content,
- fset: fset,
- decl: decl,
PkgPath: pkg.Path(),
Name: name,
Unexported: unexported,
FreeObjs: freeObjs,
FreeRefs: freeRefs,
- BodyIsReturnExpr: bodyIsReturnExpr,
ValidForCallStmt: validForCallStmt,
NumResults: sig.Results().Len(),
+ Params: params,
+ Results: results,
+ Effects: effects,
+ HasDefer: hasDefer,
+ HasBareReturn: hasBareReturn,
+ TotalReturns: totalReturns,
+ TrivialReturns: trivialReturns,
+ Labels: labels,
+ Falcon: falcon,
}}, nil
}
// parseCompact parses a Go source file of the form "package _\n func f() { ... }"
// and returns the sole function declaration.
-func parseCompact(fset *token.FileSet, content []byte) (*ast.FuncDecl, error) {
+func parseCompact(content []byte) (*token.FileSet, *ast.FuncDecl, error) {
+ fset := token.NewFileSet()
const mode = parser.ParseComments | parser.SkipObjectResolution | parser.AllErrors
f, err := parser.ParseFile(fset, "callee.go", content, mode)
if err != nil {
- return nil, fmt.Errorf("internal error: cannot compact file: %v", err)
+ return nil, nil, fmt.Errorf("internal error: cannot compact file: %v", err)
+ }
+ return fset, f.Decls[0].(*ast.FuncDecl), nil
+}
+
+// A paramInfo records information about a callee receiver, parameter, or result variable.
+type paramInfo struct {
+ Name string // parameter name (may be blank, or even "")
+ Index int // index within signature
+ IsResult bool // false for receiver or parameter, true for result variable
+ Assigned bool // parameter appears on left side of an assignment statement
+ Escapes bool // parameter has its address taken
+ Refs []int // FuncDecl-relative byte offset of parameter ref within body
+ Shadow map[string]bool // names shadowed at one of the above refs
+ FalconType string // name of this parameter's type (if basic) in the falcon system
+}
+
+// analyzeParams computes information about parameters of function fn,
+// including a simple "address taken" escape analysis.
+//
+// It returns two new arrays, one of the receiver and parameters, and
+// the other of the result variables of function fn.
+//
+// The input must be well-typed.
+func analyzeParams(logf func(string, ...any), fset *token.FileSet, info *types.Info, decl *ast.FuncDecl) (params, results []*paramInfo, effects []int, _ falconResult) {
+ fnobj, ok := info.Defs[decl.Name]
+ if !ok {
+ panic(fmt.Sprintf("%s: no func object for %q",
+ fset.PositionFor(decl.Name.Pos(), false), decl.Name)) // ill-typed?
+ }
+
+ paramInfos := make(map[*types.Var]*paramInfo)
+ {
+ sig := fnobj.Type().(*types.Signature)
+ newParamInfo := func(param *types.Var, isResult bool) *paramInfo {
+ info := ¶mInfo{
+ Name: param.Name(),
+ IsResult: isResult,
+ Index: len(paramInfos),
+ }
+ paramInfos[param] = info
+ return info
+ }
+ if sig.Recv() != nil {
+ params = append(params, newParamInfo(sig.Recv(), false))
+ }
+ for i := 0; i < sig.Params().Len(); i++ {
+ params = append(params, newParamInfo(sig.Params().At(i), false))
+ }
+ for i := 0; i < sig.Results().Len(); i++ {
+ results = append(results, newParamInfo(sig.Results().At(i), true))
+ }
}
- return f.Decls[0].(*ast.FuncDecl), nil
+
+ // Search function body for operations &x, x.f(), and x = y
+ // where x is a parameter, and record it.
+ escape(info, decl, func(v *types.Var, escapes bool) {
+ if info := paramInfos[v]; info != nil {
+ if escapes {
+ info.Escapes = true
+ } else {
+ info.Assigned = true
+ }
+ }
+ })
+
+ // Record locations of all references to parameters.
+ // And record the set of intervening definitions for each parameter.
+ //
+ // TODO(adonovan): combine this traversal with the one that computes
+ // FreeRefs. The tricky part is that calleefx needs this one first.
+ var stack []ast.Node
+ stack = append(stack, decl.Type) // for scope of function itself
+ ast.Inspect(decl.Body, func(n ast.Node) bool {
+ if n != nil {
+ stack = append(stack, n) // push
+ } else {
+ stack = stack[:len(stack)-1] // pop
+ }
+
+ if id, ok := n.(*ast.Ident); ok {
+ if v, ok := info.Uses[id].(*types.Var); ok {
+ if pinfo, ok := paramInfos[v]; ok {
+ // Record location of ref to parameter/result
+ // and any intervening (shadowing) names.
+ offset := int(n.Pos() - decl.Pos())
+ pinfo.Refs = append(pinfo.Refs, offset)
+ pinfo.Shadow = addShadows(pinfo.Shadow, info, pinfo.Name, stack)
+ }
+ }
+ }
+ return true
+ })
+
+ // Compute subset and order of parameters that are strictly evaluated.
+ // (Depends on Refs computed above.)
+ effects = calleefx(info, decl.Body, paramInfos)
+ logf("effects list = %v", effects)
+
+ falcon := falcon(logf, fset, paramInfos, info, decl)
+
+ return params, results, effects, falcon
+}
+
+// -- callee helpers --
+
+// addShadows returns the shadows set augmented by the set of names
+// locally shadowed at the location of the reference in the callee
+// (identified by the stack). The name of the reference itself is
+// excluded.
+//
+// These shadowed names may not be used in a replacement expression
+// for the reference.
+func addShadows(shadows map[string]bool, info *types.Info, exclude string, stack []ast.Node) map[string]bool {
+ for _, n := range stack {
+ if scope := scopeFor(info, n); scope != nil {
+ for _, name := range scope.Names() {
+ if name != exclude {
+ if shadows == nil {
+ shadows = make(map[string]bool)
+ }
+ shadows[name] = true
+ }
+ }
+ }
+ }
+ return shadows
}
// deref removes a pointer type constructor from the core type of t.
@@ -336,15 +528,5 @@ func (callee *Callee) GobEncode() ([]byte, error) {
}
func (callee *Callee) GobDecode(data []byte) error {
- if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&callee.impl); err != nil {
- return err
- }
- fset := token.NewFileSet()
- decl, err := parseCompact(fset, callee.impl.Content)
- if err != nil {
- return err
- }
- callee.impl.fset = fset
- callee.impl.decl = decl
- return nil
+ return gob.NewDecoder(bytes.NewReader(data)).Decode(&callee.impl)
}
diff --git a/internal/refactor/inline/calleefx.go b/internal/refactor/inline/calleefx.go
new file mode 100644
index 00000000000..6e3dc7994be
--- /dev/null
+++ b/internal/refactor/inline/calleefx.go
@@ -0,0 +1,334 @@
+// 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 inline
+
+// This file defines the analysis of callee effects.
+
+import (
+ "go/ast"
+ "go/token"
+ "go/types"
+)
+
+const (
+ rinf = -1 // R∞: arbitrary read from memory
+ winf = -2 // W∞: arbitrary write to memory (or unknown control)
+)
+
+// calleefx returns a list of parameter indices indicating the order
+// in which parameters are first referenced during evaluation of the
+// callee, relative both to each other and to other effects of the
+// callee (if any), such as arbitrary reads (rinf) and arbitrary
+// effects (winf), including unknown control flow. Each parameter
+// that is referenced appears once in the list.
+//
+// For example, the effects list of this function:
+//
+// func f(x, y, z int) int {
+// return y + x + g() + z
+// }
+//
+// is [1 0 -2 2], indicating reads of y and x, followed by the unknown
+// effects of the g() call. and finally the read of parameter z. This
+// information is used during inlining to ascertain when it is safe
+// for parameter references to be replaced by their corresponding
+// argument expressions. Such substitutions are permitted only when
+// they do not cause "write" operations (those with effects) to
+// commute with "read" operations (those that have no effect but are
+// not pure). Impure operations may be reordered with other impure
+// operations, and pure operations may be reordered arbitrarily.
+//
+// The analysis ignores the effects of runtime panics, on the
+// assumption that well-behaved programs shouldn't encounter them.
+func calleefx(info *types.Info, body *ast.BlockStmt, paramInfos map[*types.Var]*paramInfo) []int {
+ // This traversal analyzes the callee's statements (in syntax
+ // form, though one could do better with SSA) to compute the
+ // sequence of events of the following kinds:
+ //
+ // 1 read of a parameter variable.
+ // 2. reads from other memory.
+ // 3. writes to memory
+
+ var effects []int // indices of parameters, or rinf/winf (-ve)
+ seen := make(map[int]bool)
+ effect := func(i int) {
+ if !seen[i] {
+ seen[i] = true
+ effects = append(effects, i)
+ }
+ }
+
+ // unknown is called for statements of unknown effects (or control).
+ unknown := func() {
+ effect(winf)
+
+ // Ensure that all remaining parameters are "seen"
+ // after we go into the unknown (unless they are
+ // unreferenced by the function body). This lets us
+ // not bother implementing the complete traversal into
+ // control structures.
+ //
+ // TODO(adonovan): add them in a deterministic order.
+ // (This is not a bug but determinism is good.)
+ for _, pinfo := range paramInfos {
+ if !pinfo.IsResult && len(pinfo.Refs) > 0 {
+ effect(pinfo.Index)
+ }
+ }
+ }
+
+ var visitExpr func(n ast.Expr)
+ var visitStmt func(n ast.Stmt) bool
+ visitExpr = func(n ast.Expr) {
+ switch n := n.(type) {
+ case *ast.Ident:
+ if v, ok := info.Uses[n].(*types.Var); ok && !v.IsField() {
+ // Use of global?
+ if v.Parent() == v.Pkg().Scope() {
+ effect(rinf) // read global var
+ }
+
+ // Use of parameter?
+ if pinfo, ok := paramInfos[v]; ok && !pinfo.IsResult {
+ effect(pinfo.Index) // read parameter var
+ }
+
+ // Use of local variables is ok.
+ }
+
+ case *ast.BasicLit:
+ // no effect
+
+ case *ast.FuncLit:
+ // A func literal has no read or write effect
+ // until called, and (most) function calls are
+ // considered to have arbitrary effects.
+ // So, no effect.
+
+ case *ast.CompositeLit:
+ for _, elt := range n.Elts {
+ visitExpr(elt) // note: visits KeyValueExpr
+ }
+
+ case *ast.ParenExpr:
+ visitExpr(n.X)
+
+ case *ast.SelectorExpr:
+ if sel, ok := info.Selections[n]; ok {
+ visitExpr(n.X)
+ if sel.Indirect() {
+ effect(rinf) // indirect read x.f of heap variable
+ }
+ } else {
+ // qualified identifier: treat like unqualified
+ visitExpr(n.Sel)
+ }
+
+ case *ast.IndexExpr:
+ if tv := info.Types[n.Index]; tv.IsType() {
+ // no effect (G[T] instantiation)
+ } else {
+ visitExpr(n.X)
+ visitExpr(n.Index)
+ switch tv.Type.Underlying().(type) {
+ case *types.Slice, *types.Pointer: // []T, *[n]T (not string, [n]T)
+ effect(rinf) // indirect read of slice/array element
+ }
+ }
+
+ case *ast.IndexListExpr:
+ // no effect (M[K,V] instantiation)
+
+ case *ast.SliceExpr:
+ visitExpr(n.X)
+ visitExpr(n.Low)
+ visitExpr(n.High)
+ visitExpr(n.Max)
+
+ case *ast.TypeAssertExpr:
+ visitExpr(n.X)
+
+ case *ast.CallExpr:
+ if info.Types[n.Fun].IsType() {
+ // conversion T(x)
+ visitExpr(n.Args[0])
+ } else {
+ // call f(args)
+ visitExpr(n.Fun)
+ for i, arg := range n.Args {
+ if i == 0 && info.Types[arg].IsType() {
+ continue // new(T), make(T, n)
+ }
+ visitExpr(arg)
+ }
+
+ // The pure built-ins have no effects beyond
+ // those of their operands (not even memory reads).
+ // All other calls have unknown effects.
+ if !callsPureBuiltin(info, n) {
+ unknown() // arbitrary effects
+ }
+ }
+
+ case *ast.StarExpr:
+ visitExpr(n.X)
+ effect(rinf) // *ptr load or store depends on state of heap
+
+ case *ast.UnaryExpr: // + - ! ^ & ~ <-
+ visitExpr(n.X)
+ if n.Op == token.ARROW {
+ unknown() // effect: channel receive
+ }
+
+ case *ast.BinaryExpr:
+ visitExpr(n.X)
+ visitExpr(n.Y)
+
+ case *ast.KeyValueExpr:
+ visitExpr(n.Key) // may be a struct field
+ visitExpr(n.Value)
+
+ case *ast.BadExpr:
+ // no effect
+
+ case nil:
+ // optional subtree
+
+ default:
+ // type syntax: unreachable given traversal
+ panic(n)
+ }
+ }
+
+ // visitStmt's result indicates the continuation:
+ // false for return, true for the next statement.
+ //
+ // We could treat return as an unknown, but this way
+ // yields definite effects for simple sequences like
+ // {S1; S2; return}, so unreferenced parameters are
+ // not spuriously added to the effects list, and thus
+ // not spuriously disqualified from elimination.
+ visitStmt = func(n ast.Stmt) bool {
+ switch n := n.(type) {
+ case *ast.DeclStmt:
+ decl := n.Decl.(*ast.GenDecl)
+ for _, spec := range decl.Specs {
+ switch spec := spec.(type) {
+ case *ast.ValueSpec:
+ for _, v := range spec.Values {
+ visitExpr(v)
+ }
+
+ case *ast.TypeSpec:
+ // no effect
+ }
+ }
+
+ case *ast.LabeledStmt:
+ return visitStmt(n.Stmt)
+
+ case *ast.ExprStmt:
+ visitExpr(n.X)
+
+ case *ast.SendStmt:
+ visitExpr(n.Chan)
+ visitExpr(n.Value)
+ unknown() // effect: channel send
+
+ case *ast.IncDecStmt:
+ visitExpr(n.X)
+ unknown() // effect: variable increment
+
+ case *ast.AssignStmt:
+ for _, lhs := range n.Lhs {
+ visitExpr(lhs)
+ }
+ for _, rhs := range n.Rhs {
+ visitExpr(rhs)
+ }
+ for _, lhs := range n.Lhs {
+ id, _ := lhs.(*ast.Ident)
+ if id != nil && id.Name == "_" {
+ continue // blank assign has no effect
+ }
+ if n.Tok == token.DEFINE && id != nil && info.Defs[id] != nil {
+ continue // new var declared by := has no effect
+ }
+ unknown() // assignment to existing var
+ break
+ }
+
+ case *ast.GoStmt:
+ visitExpr(n.Call.Fun)
+ for _, arg := range n.Call.Args {
+ visitExpr(arg)
+ }
+ unknown() // effect: create goroutine
+
+ case *ast.DeferStmt:
+ visitExpr(n.Call.Fun)
+ for _, arg := range n.Call.Args {
+ visitExpr(arg)
+ }
+ unknown() // effect: push defer
+
+ case *ast.ReturnStmt:
+ for _, res := range n.Results {
+ visitExpr(res)
+ }
+ return false
+
+ case *ast.BlockStmt:
+ for _, stmt := range n.List {
+ if !visitStmt(stmt) {
+ return false
+ }
+ }
+
+ case *ast.BranchStmt:
+ unknown() // control flow
+
+ case *ast.IfStmt:
+ visitStmt(n.Init)
+ visitExpr(n.Cond)
+ unknown() // control flow
+
+ case *ast.SwitchStmt:
+ visitStmt(n.Init)
+ visitExpr(n.Tag)
+ unknown() // control flow
+
+ case *ast.TypeSwitchStmt:
+ visitStmt(n.Init)
+ visitStmt(n.Assign)
+ unknown() // control flow
+
+ case *ast.SelectStmt:
+ unknown() // control flow
+
+ case *ast.ForStmt:
+ visitStmt(n.Init)
+ visitExpr(n.Cond)
+ unknown() // control flow
+
+ case *ast.RangeStmt:
+ visitExpr(n.X)
+ unknown() // control flow
+
+ case *ast.EmptyStmt, *ast.BadStmt:
+ // no effect
+
+ case nil:
+ // optional subtree
+
+ default:
+ panic(n)
+ }
+ return true
+ }
+ visitStmt(body)
+
+ return effects
+}
diff --git a/internal/refactor/inline/calleefx_test.go b/internal/refactor/inline/calleefx_test.go
new file mode 100644
index 00000000000..1fc16aebaac
--- /dev/null
+++ b/internal/refactor/inline/calleefx_test.go
@@ -0,0 +1,159 @@
+// 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 inline_test
+
+import (
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "testing"
+
+ "golang.org/x/tools/internal/refactor/inline"
+)
+
+// TestCalleeEffects is a unit test of the calleefx analysis.
+func TestCalleeEffects(t *testing.T) {
+ // Each callee must declare a function or method named f.
+ const funcName = "f"
+
+ var tests = []struct {
+ descr string
+ callee string // Go source file (sans package decl) containing callee decl
+ want string // expected effects string (-1=R∞ -2=W∞)
+ }{
+ {
+ "Assignments have unknown effects.",
+ `func f(x, y int) { x = y }`,
+ `[0 1 -2]`,
+ },
+ {
+ "Reads from globals are impure.",
+ `func f() { _ = g }; var g int`,
+ `[-1]`,
+ },
+ {
+ "Writes to globals have effects.",
+ `func f() { g = 0 }; var g int`,
+ `[-1 -2]`, // the -1 is spurious but benign
+ },
+ {
+ "Blank assign has no effect.",
+ `func f(x int) { _ = x }`,
+ `[0]`,
+ },
+ {
+ "Short decl of new var has has no effect.",
+ `func f(x int) { y := x; _ = y }`,
+ `[0]`,
+ },
+ {
+ "Short decl of existing var (y) is an assignment.",
+ `func f(x int) { y := x; y, z := 1, 2; _, _ = y, z }`,
+ `[0 -2]`,
+ },
+ {
+ "Unreferenced parameters are excluded.",
+ `func f(x, y, z int) { _ = z + x }`,
+ `[2 0]`,
+ },
+ {
+ "Built-in len has no effect.",
+ `func f(x, y string) { _ = len(y) + len(x) }`,
+ `[1 0]`,
+ },
+ {
+ "Built-in println has effects.",
+ `func f(x, y int) { println(y, x) }`,
+ `[1 0 -2]`,
+ },
+ {
+ "Return has no effect, and no control successor.",
+ `func f(x, y int) int { return x + y; panic(1) }`,
+ `[0 1]`,
+ },
+ {
+ "Loops (etc) have unknown effects.",
+ `func f(x, y bool) { for x { _ = y } }`,
+ `[0 -2 1]`,
+ },
+ {
+ "Calls have unknown effects.",
+ `func f(x, y int) { _, _, _ = x, g(), y }; func g() int`,
+ `[0 -2 1]`,
+ },
+ {
+ "Calls to some built-ins are pure.",
+ `func f(x, y int) { _, _, _ = x, len("hi"), y }`,
+ `[0 1]`,
+ },
+ {
+ "Calls to some built-ins are pure (variant).",
+ `func f(x, y int) { s := "hi"; _, _, _ = x, len(s), y; s = "bye" }`,
+ `[0 1 -2]`,
+ },
+ {
+ "Calls to some built-ins are pure (another variants).",
+ `func f(x, y int) { s := "hi"; _, _, _ = x, len(s), y }`,
+ `[0 1]`,
+ },
+ {
+ "Reading a local var is impure but does not have effects.",
+ `func f(x, y bool) { for x { _ = y } }`,
+ `[0 -2 1]`,
+ },
+ }
+ for _, test := range tests {
+ test := test
+ t.Run(test.descr, func(t *testing.T) {
+ fset := token.NewFileSet()
+ mustParse := func(filename string, content any) *ast.File {
+ f, err := parser.ParseFile(fset, filename, content, parser.ParseComments|parser.SkipObjectResolution)
+ if err != nil {
+ t.Fatalf("ParseFile: %v", err)
+ }
+ return f
+ }
+
+ // Parse callee file and find first func decl named f.
+ calleeContent := "package p\n" + test.callee
+ calleeFile := mustParse("callee.go", calleeContent)
+ var decl *ast.FuncDecl
+ for _, d := range calleeFile.Decls {
+ if d, ok := d.(*ast.FuncDecl); ok && d.Name.Name == funcName {
+ decl = d
+ break
+ }
+ }
+ if decl == nil {
+ t.Fatalf("declaration of func %s not found: %s", funcName, test.callee)
+ }
+
+ info := &types.Info{
+ Defs: make(map[*ast.Ident]types.Object),
+ Uses: make(map[*ast.Ident]types.Object),
+ Types: make(map[ast.Expr]types.TypeAndValue),
+ Implicits: make(map[ast.Node]types.Object),
+ Selections: make(map[*ast.SelectorExpr]*types.Selection),
+ Scopes: make(map[ast.Node]*types.Scope),
+ }
+ conf := &types.Config{Error: func(err error) { t.Error(err) }}
+ pkg, err := conf.Check("p", fset, []*ast.File{calleeFile}, info)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ callee, err := inline.AnalyzeCallee(t.Logf, fset, pkg, info, decl, []byte(calleeContent))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := fmt.Sprint(callee.Effects()); got != test.want {
+ t.Errorf("for effects of %s, got %s want %s",
+ test.callee, got, test.want)
+ }
+ })
+ }
+}
diff --git a/internal/refactor/inline/doc.go b/internal/refactor/inline/doc.go
new file mode 100644
index 00000000000..b13241f1ec6
--- /dev/null
+++ b/internal/refactor/inline/doc.go
@@ -0,0 +1,295 @@
+// 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 inline implements inlining of Go function calls.
+
+The client provides information about the caller and callee,
+including the source text, syntax tree, and type information, and
+the inliner returns the modified source file for the caller, or an
+error if the inlining operation is invalid (for example because the
+function body refers to names that are inaccessible to the caller).
+
+Although this interface demands more information from the client
+than might seem necessary, it enables smoother integration with
+existing batch and interactive tools that have their own ways of
+managing the processes of reading, parsing, and type-checking
+packages. In particular, this package does not assume that the
+caller and callee belong to the same token.FileSet or
+types.Importer realms.
+
+There are many aspects to a function call. It is the only construct
+that can simultaneously bind multiple variables of different
+explicit types, with implicit assignment conversions. (Neither var
+nor := declarations can do that.) It defines the scope of control
+labels, of return statements, and of defer statements. Arguments
+and results of function calls may be tuples even though tuples are
+not first-class values in Go, and a tuple-valued call expression
+may be "spread" across the argument list of a call or the operands
+of a return statement. All these unique features mean that in the
+general case, not everything that can be expressed by a function
+call can be expressed without one.
+
+So, in general, inlining consists of modifying a function or method
+call expression f(a1, ..., an) so that the name of the function f
+is replaced ("literalized") by a literal copy of the function
+declaration, with free identifiers suitably modified to use the
+locally appropriate identifiers or perhaps constant argument
+values.
+
+Inlining must not change the semantics of the call. Semantics
+preservation is crucial for clients such as codebase maintenance
+tools that automatically inline all calls to designated functions
+on a large scale. Such tools must not introduce subtle behavior
+changes. (Fully inlining a call is dynamically observable using
+reflection over the call stack, but this exception to the rule is
+explicitly allowed.)
+
+In many cases it is possible to entirely replace ("reduce") the
+call by a copy of the function's body in which parameters have been
+replaced by arguments. The inliner supports a number of reduction
+strategies, and we expect this set to grow. Nonetheless, sound
+reduction is surprisingly tricky.
+
+The inliner is in some ways like an optimizing compiler. A compiler
+is considered correct if it doesn't change the meaning of the
+program in translation from source language to target language. An
+optimizing compiler exploits the particulars of the input to
+generate better code, where "better" usually means more efficient.
+When a case is found in which it emits suboptimal code, the
+compiler is improved to recognize more cases, or more rules, and
+more exceptions to rules; this process has no end. Inlining is
+similar except that "better" code means tidier code. The baseline
+translation (literalization) is correct, but there are endless
+rules--and exceptions to rules--by which the output can be
+improved.
+
+The following section lists some of the challenges, and ways in
+which they can be addressed.
+
+ - All effects of the call argument expressions must be preserved,
+ both in their number (they must not be eliminated or repeated),
+ and in their order (both with respect to other arguments, and any
+ effects in the callee function).
+
+ This must be the case even if the corresponding parameters are
+ never referenced, are referenced multiple times, referenced in
+ a different order from the arguments, or referenced within a
+ nested function that may be executed an arbitrary number of
+ times.
+
+ Currently, parameter replacement is not applied to arguments
+ with effects, but with further analysis of the sequence of
+ strict effects within the callee we could relax this constraint.
+
+ - When not all parameters can be substituted by their arguments
+ (e.g. due to possible effects), if the call appears in a
+ statement context, the inliner may introduce a var declaration
+ that declares the parameter variables (with the correct types)
+ and assigns them to their corresponding argument values.
+ The rest of the function body may then follow.
+ For example, the call
+
+ f(1, 2)
+
+ to the function
+
+ func f(x, y int32) { stmts }
+
+ may be reduced to
+
+ { var x, y int32 = 1, 2; stmts }.
+
+ There are many reasons why this is not always possible. For
+ example, true parameters are statically resolved in the same
+ scope, and are dynamically assigned their arguments in
+ parallel; but each spec in a var declaration is statically
+ resolved in sequence and dynamically executed in sequence, so
+ earlier parameters may shadow references in later ones.
+
+ - Even an argument expression as simple as ptr.x may not be
+ referentially transparent, because another argument may have the
+ effect of changing the value of ptr.
+
+ This constraint could be relaxed by some kind of alias or
+ escape analysis that proves that ptr cannot be mutated during
+ the call.
+
+ - Although constants are referentially transparent, as a matter of
+ style we do not wish to duplicate literals that are referenced
+ multiple times in the body because this undoes proper factoring.
+ Also, string literals may be arbitrarily large.
+
+ - If the function body consists of statements other than just
+ "return expr", in some contexts it may be syntactically
+ impossible to reduce the call. Consider:
+
+ if x := f(); cond { ... }
+
+ Go has no equivalent to Lisp's progn or Rust's blocks,
+ nor ML's let expressions (let param = arg in body);
+ its closest equivalent is func(param){body}(arg).
+ Reduction strategies must therefore consider the syntactic
+ context of the call.
+
+ In such situations we could work harder to extract a statement
+ context for the call, by transforming it to:
+
+ { x := f(); if cond { ... } }
+
+ - Similarly, without the equivalent of Rust-style blocks and
+ first-class tuples, there is no general way to reduce a call
+ to a function such as
+
+ func(params)(args)(results) { stmts; return expr }
+
+ to an expression such as
+
+ { var params = args; stmts; expr }
+
+ or even a statement such as
+
+ results = { var params = args; stmts; expr }
+
+ Consequently the declaration and scope of the result variables,
+ and the assignment and control-flow implications of the return
+ statement, must be dealt with by cases.
+
+ - A standalone call statement that calls a function whose body is
+ "return expr" cannot be simply replaced by the body expression
+ if it is not itself a call or channel receive expression; it is
+ necessary to explicitly discard the result using "_ = expr".
+
+ Similarly, if the body is a call expression, only calls to some
+ built-in functions with no result (such as copy or panic) are
+ permitted as statements, whereas others (such as append) return
+ a result that must be used, even if just by discarding.
+
+ - If a parameter or result variable is updated by an assignment
+ within the function body, it cannot always be safely replaced
+ by a variable in the caller. For example, given
+
+ func f(a int) int { a++; return a }
+
+ The call y = f(x) cannot be replaced by { x++; y = x } because
+ this would change the value of the caller's variable x.
+ Only if the caller is finished with x is this safe.
+
+ A similar argument applies to parameter or result variables
+ that escape: by eliminating a variable, inlining would change
+ the identity of the variable that escapes.
+
+ - If the function body uses 'defer' and the inlined call is not a
+ tail-call, inlining may delay the deferred effects.
+
+ - Because the scope of a control label is the entire function, a
+ call cannot be reduced if the caller and callee have intersecting
+ sets of control labels. (It is possible to α-rename any
+ conflicting ones, but our colleagues building C++ refactoring
+ tools report that, when tools must choose new identifiers, they
+ generally do a poor job.)
+
+ - Given
+
+ func f() uint8 { return 0 }
+
+ var x any = f()
+
+ reducing the call to var x any = 0 is unsound because it
+ discards the implicit conversion to uint8. We may need to make
+ each argument-to-parameter conversion explicit if the types
+ differ. Assignments to variadic parameters may need to
+ explicitly construct a slice.
+
+ An analogous problem applies to the implicit assignments in
+ return statements:
+
+ func g() any { return f() }
+
+ Replacing the call f() with 0 would silently lose a
+ conversion to uint8 and change the behavior of the program.
+
+ - When inlining a call f(1, x, g()) where those parameters are
+ unreferenced, we should be able to avoid evaluating 1 and x
+ since they are pure and thus have no effect. But x may be the
+ last reference to a local variable in the caller, so removing
+ it would cause a compilation error. Parameter substitution must
+ avoid making the caller's local variables unreferenced (or must
+ be prepared to eliminate the declaration too---this is where an
+ iterative framework for simplification would really help).
+
+ - An expression such as s[i] may be valid if s and i are
+ variables but invalid if either or both of them are constants.
+ For example, a negative constant index s[-1] is always out of
+ bounds, and even a non-negative constant index may be out of
+ bounds depending on the particular string constant (e.g.
+ "abc"[4]).
+
+ So, if a parameter participates in any expression that is
+ subject to additional compile-time checks when its operands are
+ constant, it may be unsafe to substitute that parameter by a
+ constant argument value (#62664).
+
+More complex callee functions are inlinable with more elaborate and
+invasive changes to the statements surrounding the call expression.
+
+TODO(adonovan): future work:
+
+ - Handle more of the above special cases by careful analysis,
+ thoughtful factoring of the large design space, and thorough
+ test coverage.
+
+ - Compute precisely (not conservatively) when parameter
+ substitution would remove the last reference to a caller local
+ variable, and blank out the local instead of retreating from
+ the substitution.
+
+ - Afford the client more control such as a limit on the total
+ increase in line count, or a refusal to inline using the
+ general approach (replacing name by function literal). This
+ could be achieved by returning metadata alongside the result
+ and having the client conditionally discard the change.
+
+ - Is it acceptable to skip effects that are limited to runtime
+ panics? Can we avoid evaluating an argument x.f
+ or a[i] when the corresponding parameter is unused?
+
+ - Support inlining of generic functions, replacing type parameters
+ by their instantiations.
+
+ - Support inlining of calls to function literals ("closures").
+ But note that the existing algorithm makes widespread assumptions
+ that the callee is a package-level function or method.
+
+ - Eliminate parens and braces inserted conservatively when they
+ are redundant.
+
+ - Eliminate explicit conversions of "untyped" literals inserted
+ conservatively when they are redundant. For example, the
+ conversion int32(1) is redundant when this value is used only as a
+ slice index; but it may be crucial if it is used in x := int32(1)
+ as it changes the type of x, which may have further implications.
+ The conversions may also be important to the falcon analysis.
+
+ - Allow non-'go' build systems such as Bazel/Blaze a chance to
+ decide whether an import is accessible using logic other than
+ "/internal/" path segments. This could be achieved by returning
+ the list of added import paths instead of a text diff.
+
+ - Inlining a function from another module may change the
+ effective version of the Go language spec that governs it. We
+ should probably make the client responsible for rejecting
+ attempts to inline from newer callees to older callers, since
+ there's no way for this package to access module versions.
+
+ - Use an alternative implementation of the import-organizing
+ operation that doesn't require operating on a complete file
+ (and reformatting). Then return the results in a higher-level
+ form as a set of import additions and deletions plus a single
+ diff that encloses the call expression. This interface could
+ perhaps be implemented atop imports.Process by post-processing
+ its result to obtain the abstract import changes and discarding
+ its formatted output.
+*/
+package inline
diff --git a/internal/refactor/inline/escape.go b/internal/refactor/inline/escape.go
new file mode 100644
index 00000000000..d05d2b927c0
--- /dev/null
+++ b/internal/refactor/inline/escape.go
@@ -0,0 +1,97 @@
+// 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 inline
+
+import (
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+)
+
+// escape implements a simple "address-taken" escape analysis. It
+// calls f for each local variable that appears on the left side of an
+// assignment (escapes=false) or has its address taken (escapes=true).
+// The initialization of a variable by its declaration does not count
+// as an assignment.
+func escape(info *types.Info, root ast.Node, f func(v *types.Var, escapes bool)) {
+
+ // lvalue is called for each address-taken expression or LHS of assignment.
+ // Supported forms are: x, (x), x[i], x.f, *x, T{}.
+ var lvalue func(e ast.Expr, escapes bool)
+ lvalue = func(e ast.Expr, escapes bool) {
+ switch e := e.(type) {
+ case *ast.Ident:
+ if v, ok := info.Uses[e].(*types.Var); ok {
+ if !isPkgLevel(v) {
+ f(v, escapes)
+ }
+ }
+ case *ast.ParenExpr:
+ lvalue(e.X, escapes)
+ case *ast.IndexExpr:
+ // TODO(adonovan): support generics without assuming e.X has a core type.
+ // Consider:
+ //
+ // func Index[T interface{ [3]int | []int }](t T, i int) *int {
+ // return &t[i]
+ // }
+ //
+ // We must traverse the normal terms and check
+ // whether any of them is an array.
+ if _, ok := info.TypeOf(e.X).Underlying().(*types.Array); ok {
+ lvalue(e.X, escapes) // &a[i] on array
+ }
+ case *ast.SelectorExpr:
+ if _, ok := info.TypeOf(e.X).Underlying().(*types.Struct); ok {
+ lvalue(e.X, escapes) // &s.f on struct
+ }
+ case *ast.StarExpr:
+ // *ptr indirects an existing pointer
+ case *ast.CompositeLit:
+ // &T{...} creates a new variable
+ default:
+ panic(fmt.Sprintf("&x on %T", e)) // unreachable in well-typed code
+ }
+ }
+
+ // Search function body for operations &x, x.f(), x++, and x = y
+ // where x is a parameter. Each of these treats x as an address.
+ ast.Inspect(root, func(n ast.Node) bool {
+ switch n := n.(type) {
+ case *ast.UnaryExpr:
+ if n.Op == token.AND {
+ lvalue(n.X, true) // &x
+ }
+
+ case *ast.CallExpr:
+ // implicit &x in method call x.f(),
+ // where x has type T and method is (*T).f
+ if sel, ok := n.Fun.(*ast.SelectorExpr); ok {
+ if seln, ok := info.Selections[sel]; ok &&
+ seln.Kind() == types.MethodVal &&
+ !seln.Indirect() &&
+ is[*types.Pointer](seln.Obj().Type().(*types.Signature).Recv().Type()) {
+ lvalue(sel.X, true) // &x.f
+ }
+ }
+
+ case *ast.AssignStmt:
+ for _, lhs := range n.Lhs {
+ if id, ok := lhs.(*ast.Ident); ok &&
+ info.Defs[id] != nil &&
+ n.Tok == token.DEFINE {
+ // declaration: doesn't count
+ } else {
+ lvalue(lhs, false)
+ }
+ }
+
+ case *ast.IncDecStmt:
+ lvalue(n.X, false)
+ }
+ return true
+ })
+}
diff --git a/internal/refactor/inline/everything_test.go b/internal/refactor/inline/everything_test.go
new file mode 100644
index 00000000000..f6111495a48
--- /dev/null
+++ b/internal/refactor/inline/everything_test.go
@@ -0,0 +1,237 @@
+// 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 inline_test
+
+import (
+ "flag"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/types"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/diff"
+ "golang.org/x/tools/internal/refactor/inline"
+ "golang.org/x/tools/internal/testenv"
+)
+
+var packagesFlag = flag.String("packages", "", "set of packages for TestEverything")
+
+// TestEverything invokes the inliner on every single call site in a
+// given package. and checks that it produces either a reasonable
+// error, or output that parses and type-checks.
+//
+// It does nothing during ordinary testing, but may be used to find
+// inlining bugs in large corpora.
+//
+// Use this command to inline everything in golang.org/x/tools:
+//
+// $ go test ./internal/refactor/inline/ -run=Everything -packages=../../../
+//
+// And these commands to inline everything in the kubernetes repository:
+//
+// $ go test -c -o /tmp/everything ./internal/refactor/inline/
+// $ (cd kubernetes && /tmp/everything -test.run=Everything -packages=./...)
+//
+// TODO(adonovan):
+// - report counters (number of attempts, failed AnalyzeCallee, failed
+// Inline, etc.)
+// - Make a pretty log of the entire output so that we can peruse it
+// for opportunities for systematic improvement.
+func TestEverything(t *testing.T) {
+ testenv.NeedsGoPackages(t)
+ if testing.Short() {
+ t.Skipf("skipping slow test in -short mode")
+ }
+ if *packagesFlag == "" {
+ return
+ }
+
+ // Load this package plus dependencies from typed syntax.
+ cfg := &packages.Config{
+ Mode: packages.LoadAllSyntax,
+ Env: append(os.Environ(),
+ "GO111MODULES=on",
+ "GOPATH=",
+ "GOWORK=off",
+ "GOPROXY=off"),
+ }
+ pkgs, err := packages.Load(cfg, *packagesFlag)
+ if err != nil {
+ t.Errorf("Load: %v", err)
+ }
+ // Report parse/type errors.
+ // Also, build transitive dependency mapping.
+ deps := make(map[string]*packages.Package) // key is PkgPath
+ packages.Visit(pkgs, nil, func(pkg *packages.Package) {
+ deps[pkg.Types.Path()] = pkg
+ for _, err := range pkg.Errors {
+ t.Fatal(err)
+ }
+ })
+
+ // Memoize repeated calls for same file.
+ fileContent := make(map[string][]byte)
+ readFile := func(filename string) ([]byte, error) {
+ content, ok := fileContent[filename]
+ if !ok {
+ var err error
+ content, err = os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ fileContent[filename] = content
+ }
+ return content, nil
+ }
+
+ for _, callerPkg := range pkgs {
+ // Find all static function calls in the package.
+ for _, callerFile := range callerPkg.Syntax {
+ noMutCheck := checkNoMutation(callerFile)
+ ast.Inspect(callerFile, func(n ast.Node) bool {
+ call, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+ fn := typeutil.StaticCallee(callerPkg.TypesInfo, call)
+ if fn == nil {
+ return true
+ }
+
+ // Prepare caller info.
+ callPosn := callerPkg.Fset.PositionFor(call.Lparen, false)
+ callerContent, err := readFile(callPosn.Filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ caller := &inline.Caller{
+ Fset: callerPkg.Fset,
+ Types: callerPkg.Types,
+ Info: callerPkg.TypesInfo,
+ File: callerFile,
+ Call: call,
+ Content: callerContent,
+ }
+
+ // Analyze callee.
+ calleePkg, ok := deps[fn.Pkg().Path()]
+ if !ok {
+ t.Fatalf("missing package for callee %v", fn)
+ }
+ calleePosn := callerPkg.Fset.PositionFor(fn.Pos(), false)
+ calleeDecl, err := findFuncByPosition(calleePkg, calleePosn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ calleeContent, err := readFile(calleePosn.Filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create a subtest for each inlining operation.
+ name := fmt.Sprintf("%s@%v", fn.Name(), filepath.Base(callPosn.String()))
+ t.Run(name, func(t *testing.T) {
+ // TODO(adonovan): add a panic handler.
+
+ t.Logf("callee declared at %v",
+ filepath.Base(calleePosn.String()))
+
+ t.Logf("run this command to reproduce locally:\n$ gopls fix -a -d %s:#%d refactor.inline",
+ callPosn.Filename, callPosn.Offset)
+
+ callee, err := inline.AnalyzeCallee(
+ t.Logf,
+ calleePkg.Fset,
+ calleePkg.Types,
+ calleePkg.TypesInfo,
+ calleeDecl,
+ calleeContent)
+ if err != nil {
+ // Ignore the expected kinds of errors.
+ for _, ignore := range []string{
+ "has no body",
+ "type parameters are not yet",
+ "line directives",
+ "cgo-generated",
+ } {
+ if strings.Contains(err.Error(), ignore) {
+ return
+ }
+ }
+ t.Fatalf("AnalyzeCallee: %v", err)
+ }
+ if err := checkTranscode(callee); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := inline.Inline(t.Logf, caller, callee)
+ if err != nil {
+ // Write error to a log, but this ok.
+ t.Log(err)
+ return
+ }
+
+ // Print the diff.
+ t.Logf("Got diff:\n%s",
+ diff.Unified("old", "new", string(callerContent), string(got)))
+
+ // Parse and type-check the transformed source.
+ f, err := parser.ParseFile(caller.Fset, callPosn.Filename, got, parser.SkipObjectResolution)
+ if err != nil {
+ t.Fatalf("transformed source does not parse: %v", err)
+ }
+ // Splice into original file list.
+ syntax := append([]*ast.File(nil), callerPkg.Syntax...)
+ for i := range callerPkg.Syntax {
+ if syntax[i] == callerFile {
+ syntax[i] = f
+ break
+ }
+ }
+
+ var typeErrors []string
+ conf := &types.Config{
+ Error: func(err error) {
+ typeErrors = append(typeErrors, err.Error())
+ },
+ Importer: importerFunc(func(importPath string) (*types.Package, error) {
+ // Note: deps is properly keyed by package path,
+ // not import path, but we can't assume
+ // Package.Imports[importPath] exists in the
+ // case of newly added imports of indirect
+ // dependencies. Seems not to matter to this test.
+ dep, ok := deps[importPath]
+ if ok {
+ return dep.Types, nil
+ }
+ return nil, fmt.Errorf("missing package: %q", importPath)
+ }),
+ }
+ if _, err := conf.Check("p", caller.Fset, syntax, nil); err != nil {
+ t.Fatalf("transformed package has type errors:\n\n%s\n\nTransformed file:\n\n%s",
+ strings.Join(typeErrors, "\n"),
+ got)
+ }
+ })
+ return true
+ })
+ noMutCheck()
+ }
+ }
+ log.Printf("Analyzed %d packages", len(pkgs))
+}
+
+type importerFunc func(path string) (*types.Package, error)
+
+func (f importerFunc) Import(path string) (*types.Package, error) {
+ return f(path)
+}
diff --git a/internal/refactor/inline/export_test.go b/internal/refactor/inline/export_test.go
new file mode 100644
index 00000000000..7b2cec7f19d
--- /dev/null
+++ b/internal/refactor/inline/export_test.go
@@ -0,0 +1,9 @@
+// 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 inline
+
+// This file opens back doors for testing.
+
+func (callee *Callee) Effects() []int { return callee.impl.Effects }
diff --git a/internal/refactor/inline/falcon.go b/internal/refactor/inline/falcon.go
new file mode 100644
index 00000000000..9863e8dbcfb
--- /dev/null
+++ b/internal/refactor/inline/falcon.go
@@ -0,0 +1,892 @@
+// 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 inline
+
+// This file defines the callee side of the "fallible constant" analysis.
+
+import (
+ "fmt"
+ "go/ast"
+ "go/constant"
+ "go/format"
+ "go/token"
+ "go/types"
+ "strconv"
+ "strings"
+
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/typeparams"
+)
+
+// falconResult is the result of the analysis of the callee.
+type falconResult struct {
+ Types []falconType // types for falcon constraint environment
+ Constraints []string // constraints (Go expressions) on values of fallible constants
+}
+
+// A falconType specifies the name and underlying type of a synthetic
+// defined type for use in falcon constraints.
+//
+// Unique types from callee code are bijectively mapped onto falcon
+// types so that constraints are independent of callee type
+// information but preserve type equivalence classes.
+//
+// Fresh names are deliberately obscure to avoid shadowing even if a
+// callee parameter has a nanme like "int" or "any".
+type falconType struct {
+ Name string
+ Kind types.BasicKind // string/number/bool
+}
+
+// falcon identifies "fallible constant" expressions, which are
+// expressions that may fail to compile if one or more of their
+// operands is changed from non-constant to constant.
+//
+// Consider:
+//
+// func sub(s string, i, j int) string { return s[i:j] }
+//
+// If parameters are replaced by constants, the compiler is
+// required to perform these additional checks:
+//
+// - if i is constant, 0 <= i.
+// - if s and i are constant, i <= len(s).
+// - ditto for j.
+// - if i and j are constant, i <= j.
+//
+// s[i:j] is thus a "fallible constant" expression dependent on {s, i,
+// j}. Each falcon creates a set of conditional constraints across one
+// or more parameter variables.
+//
+// - When inlining a call such as sub("abc", -1, 2), the parameter i
+// cannot be eliminated by substitution as its argument value is
+// negative.
+//
+// - When inlining sub("", 2, 1), all three parameters cannot be be
+// simultaneously eliminated by substitution without violating i
+// <= len(s) and j <= len(s), but the parameters i and j could be
+// safely eliminated without s.
+//
+// Parameters that cannot be eliminated must remain non-constant,
+// either in the form of a binding declaration:
+//
+// { var i int = -1; return "abc"[i:2] }
+//
+// or a parameter of a literalization:
+//
+// func (i int) string { return "abc"[i:2] }(-1)
+//
+// These example expressions are obviously doomed to fail at run
+// time, but in realistic cases such expressions are dominated by
+// appropriate conditions that make them reachable only when safe:
+//
+// if 0 <= i && i <= j && j <= len(s) { _ = s[i:j] }
+//
+// (In principle a more sophisticated inliner could entirely eliminate
+// such unreachable blocks based on the condition being always-false
+// for the given parameter substitution, but this is tricky to do safely
+// because the type-checker considers only a single configuration.
+// Consider: if runtime.GOOS == "linux" { ... }.)
+//
+// We believe this is an exhaustive list of "fallible constant" operations:
+//
+// - switch z { case x: case y } // duplicate case values
+// - s[i], s[i:j], s[i:j:k] // index out of bounds (0 <= i <= j <= k <= len(s))
+// - T{x: 0} // index out of bounds, duplicate index
+// - x/y, x%y, x/=y, x%=y // integer division by zero; minint/-1 overflow
+// - x+y, x-y, x*y // arithmetic overflow
+// - x< 1 {
+ var elts []ast.Expr
+ for _, elem := range elems {
+ elts = append(elts, &ast.KeyValueExpr{
+ Key: elem,
+ Value: makeIntLit(0),
+ })
+ }
+ st.emit(&ast.CompositeLit{
+ Type: typ,
+ Elts: elts,
+ })
+ }
+}
+
+// -- traversal --
+
+// The traversal functions scan the callee body for expressions that
+// are not constant but would become constant if the parameter vars
+// were redeclared as constants, and emits for each one a constraint
+// (a Go expression) with the property that it will not type-check
+// (using types.CheckExpr) if the particular argument values are
+// unsuitable.
+//
+// These constraints are checked by Inline with the actual
+// constant argument values. Violations cause it to reject
+// parameters as candidates for substitution.
+
+func (st *falconState) stmt(s ast.Stmt) {
+ ast.Inspect(s, func(n ast.Node) bool {
+ switch n := n.(type) {
+ case ast.Expr:
+ _ = st.expr(n)
+ return false // skip usual traversal
+
+ case *ast.AssignStmt:
+ switch n.Tok {
+ case token.QUO_ASSIGN, token.REM_ASSIGN:
+ // x /= y
+ // Possible "integer division by zero"
+ // Emit constraint: 1/y.
+ _ = st.expr(n.Lhs[0])
+ kY := st.expr(n.Rhs[0])
+ if kY, ok := kY.(ast.Expr); ok {
+ op := token.QUO
+ if n.Tok == token.REM_ASSIGN {
+ op = token.REM
+ }
+ st.emit(&ast.BinaryExpr{
+ Op: op,
+ X: makeIntLit(1),
+ Y: kY,
+ })
+ }
+ return false // skip usual traversal
+ }
+
+ case *ast.SwitchStmt:
+ if n.Init != nil {
+ st.stmt(n.Init)
+ }
+ tBool := types.Type(types.Typ[types.Bool])
+ tagType := tBool // default: true
+ if n.Tag != nil {
+ st.expr(n.Tag)
+ tagType = st.info.TypeOf(n.Tag)
+ }
+
+ // Possible "duplicate case value".
+ // Emit constraint map[T]int{v1: 0, ..., vN:0}
+ // to ensure all maybe-constant case values are unique
+ // (unless switch tag is boolean, which is relaxed).
+ var unique []ast.Expr
+ for _, clause := range n.Body.List {
+ clause := clause.(*ast.CaseClause)
+ for _, caseval := range clause.List {
+ if k := st.expr(caseval); k != nil {
+ unique = append(unique, st.toExpr(k))
+ }
+ }
+ for _, stmt := range clause.Body {
+ st.stmt(stmt)
+ }
+ }
+ if unique != nil && !types.Identical(tagType.Underlying(), tBool) {
+ tname := st.any
+ if !types.IsInterface(tagType) {
+ tname = st.typename(tagType)
+ }
+ t := &ast.MapType{
+ Key: makeIdent(tname),
+ Value: makeIdent(st.int),
+ }
+ st.emitUnique(t, unique)
+ }
+ }
+ return true
+ })
+}
+
+// fieldTypes visits the .Type of each field in the list.
+func (st *falconState) fieldTypes(fields *ast.FieldList) {
+ if fields != nil {
+ for _, field := range fields.List {
+ _ = st.expr(field.Type)
+ }
+ }
+}
+
+// expr visits the expression (or type) and returns a
+// non-nil result if the expression is constant or would
+// become constant if all suitable function parameters were
+// redeclared as constants.
+//
+// If the expression is constant, st.expr returns its type
+// and value (types.TypeAndValue). If the expression would
+// become constant, st.expr returns an ast.Expr tree whose
+// leaves are literals and parameter references, and whose
+// interior nodes are operations that may become constant,
+// such as -x, x+y, f(x), and T(x). We call these would-be
+// constant expressions "fallible constants", since they may
+// fail to type-check for some values of x, i, and j. (We
+// refer to the non-nil cases collectively as "maybe
+// constant", and the nil case as "definitely non-constant".)
+//
+// As a side effect, st.expr emits constraints for each
+// fallible constant expression; this is its main purpose.
+//
+// Consequently, st.expr must visit the entire subtree so
+// that all necessary constraints are emitted. It may not
+// short-circuit the traversal when it encounters a constant
+// subexpression as constants may contain arbitrary other
+// syntax that may impose constraints. Consider (as always)
+// this contrived but legal example of a type parameter (!)
+// that contains statement syntax:
+//
+// func f[T [unsafe.Sizeof(func() { stmts })]int]()
+//
+// There is no need to emit constraints for (e.g.) s[i] when s
+// and i are already constants, because we know the expression
+// is sound, but it is sometimes easier to emit these
+// redundant constraints than to avoid them.
+func (st *falconState) expr(e ast.Expr) (res any) { // = types.TypeAndValue | ast.Expr
+ tv := st.info.Types[e]
+ if tv.Value != nil {
+ // A constant value overrides any other result.
+ defer func() { res = tv }()
+ }
+
+ switch e := e.(type) {
+ case *ast.Ident:
+ if v, ok := st.info.Uses[e].(*types.Var); ok {
+ if _, ok := st.params[v]; ok && isBasic(v.Type(), types.IsConstType) {
+ return e // reference to constable parameter
+ }
+ }
+ // (References to *types.Const are handled by the defer.)
+
+ case *ast.BasicLit:
+ // constant
+
+ case *ast.ParenExpr:
+ return st.expr(e.X)
+
+ case *ast.FuncLit:
+ _ = st.expr(e.Type)
+ st.stmt(e.Body)
+ // definitely non-constant
+
+ case *ast.CompositeLit:
+ // T{k: v, ...}, where T ∈ {array,*array,slice,map},
+ // imposes a constraint that all constant k are
+ // distinct and, for arrays [n]T, within range 0-n.
+ //
+ // Types matter, not just values. For example,
+ // an interface-keyed map may contain keys
+ // that are numerically equal so long as they
+ // are of distinct types. For example:
+ //
+ // type myint int
+ // map[any]bool{1: true, 1: true} // error: duplicate key
+ // map[any]bool{1: true, int16(1): true} // ok
+ // map[any]bool{1: true, myint(1): true} // ok
+ //
+ // This can be asserted by emitting a
+ // constraint of the form T{k1: 0, ..., kN: 0}.
+ if e.Type != nil {
+ _ = st.expr(e.Type)
+ }
+ t := deref(typeparams.CoreType(deref(tv.Type)))
+ var uniques []ast.Expr
+ for _, elt := range e.Elts {
+ if kv, ok := elt.(*ast.KeyValueExpr); ok {
+ if !is[*types.Struct](t) {
+ if k := st.expr(kv.Key); k != nil {
+ uniques = append(uniques, st.toExpr(k))
+ }
+ }
+ _ = st.expr(kv.Value)
+ } else {
+ _ = st.expr(elt)
+ }
+ }
+ if uniques != nil {
+ // Inv: not a struct.
+
+ // The type T in constraint T{...} depends on the CompLit:
+ // - for a basic-keyed map, use map[K]int;
+ // - for an interface-keyed map, use map[any]int;
+ // - for a slice, use []int;
+ // - for an array or *array, use [n]int.
+ // The last two entail progressively stronger index checks.
+ var ct ast.Expr // type syntax for constraint
+ switch t := t.(type) {
+ case *types.Map:
+ if types.IsInterface(t.Key()) {
+ ct = &ast.MapType{
+ Key: makeIdent(st.any),
+ Value: makeIdent(st.int),
+ }
+ } else {
+ ct = &ast.MapType{
+ Key: makeIdent(st.typename(t.Key())),
+ Value: makeIdent(st.int),
+ }
+ }
+ case *types.Array: // or *array
+ ct = &ast.ArrayType{
+ Len: makeIntLit(t.Len()),
+ Elt: makeIdent(st.int),
+ }
+ default:
+ panic(t)
+ }
+ st.emitUnique(ct, uniques)
+ }
+ // definitely non-constant
+
+ case *ast.SelectorExpr:
+ _ = st.expr(e.X)
+ _ = st.expr(e.Sel)
+ // The defer is sufficient to handle
+ // qualified identifiers (pkg.Const).
+ // All other cases are definitely non-constant.
+
+ case *ast.IndexExpr:
+ if tv.IsType() {
+ // type C[T]
+ _ = st.expr(e.X)
+ _ = st.expr(e.Index)
+ } else {
+ // term x[i]
+ //
+ // Constraints (if x is slice/string/array/*array, not map):
+ // - i >= 0
+ // if i is a fallible constant
+ // - i < len(x)
+ // if x is array/*array and
+ // i is a fallible constant;
+ // or if s is a string and both i,
+ // s are maybe-constants,
+ // but not both are constants.
+ kX := st.expr(e.X)
+ kI := st.expr(e.Index)
+ if kI != nil && !is[*types.Map](st.info.TypeOf(e.X).Underlying()) {
+ if kI, ok := kI.(ast.Expr); ok {
+ st.emitNonNegative(kI)
+ }
+ // Emit constraint to check indices against known length.
+ // TODO(adonovan): factor with SliceExpr logic.
+ var x ast.Expr
+ if kX != nil {
+ // string
+ x = st.toExpr(kX)
+ } else if arr, ok := deref(st.info.TypeOf(e.X).Underlying()).(*types.Array); ok {
+ // array, *array
+ x = &ast.CompositeLit{
+ Type: &ast.ArrayType{
+ Len: makeIntLit(arr.Len()),
+ Elt: makeIdent(st.int),
+ },
+ }
+ }
+ if x != nil {
+ st.emit(&ast.IndexExpr{
+ X: x,
+ Index: st.toExpr(kI),
+ })
+ }
+ }
+ }
+ // definitely non-constant
+
+ case *ast.SliceExpr:
+ // x[low:high:max]
+ //
+ // Emit non-negative constraints for each index,
+ // plus low <= high <= max <= len(x)
+ // for each pair that are maybe-constant
+ // but not definitely constant.
+
+ kX := st.expr(e.X)
+ var kLow, kHigh, kMax any
+ if e.Low != nil {
+ kLow = st.expr(e.Low)
+ if kLow != nil {
+ if kLow, ok := kLow.(ast.Expr); ok {
+ st.emitNonNegative(kLow)
+ }
+ }
+ }
+ if e.High != nil {
+ kHigh = st.expr(e.High)
+ if kHigh != nil {
+ if kHigh, ok := kHigh.(ast.Expr); ok {
+ st.emitNonNegative(kHigh)
+ }
+ if kLow != nil {
+ st.emitMonotonic(st.toExpr(kLow), st.toExpr(kHigh))
+ }
+ }
+ }
+ if e.Max != nil {
+ kMax = st.expr(e.Max)
+ if kMax != nil {
+ if kMax, ok := kMax.(ast.Expr); ok {
+ st.emitNonNegative(kMax)
+ }
+ if kHigh != nil {
+ st.emitMonotonic(st.toExpr(kHigh), st.toExpr(kMax))
+ }
+ }
+ }
+
+ // Emit constraint to check indices against known length.
+ var x ast.Expr
+ if kX != nil {
+ // string
+ x = st.toExpr(kX)
+ } else if arr, ok := deref(st.info.TypeOf(e.X).Underlying()).(*types.Array); ok {
+ // array, *array
+ x = &ast.CompositeLit{
+ Type: &ast.ArrayType{
+ Len: makeIntLit(arr.Len()),
+ Elt: makeIdent(st.int),
+ },
+ }
+ }
+ if x != nil {
+ // Avoid slice[::max] if kHigh is nonconstant (nil).
+ high, max := st.toExpr(kHigh), st.toExpr(kMax)
+ if high == nil {
+ high = max // => slice[:max:max]
+ }
+ st.emit(&ast.SliceExpr{
+ X: x,
+ Low: st.toExpr(kLow),
+ High: high,
+ Max: max,
+ })
+ }
+ // definitely non-constant
+
+ case *ast.TypeAssertExpr:
+ _ = st.expr(e.X)
+ if e.Type != nil {
+ _ = st.expr(e.Type)
+ }
+
+ case *ast.CallExpr:
+ _ = st.expr(e.Fun)
+ if tv, ok := st.info.Types[e.Fun]; ok && tv.IsType() {
+ // conversion T(x)
+ //
+ // Possible "value out of range".
+ kX := st.expr(e.Args[0])
+ if kX != nil && isBasic(tv.Type, types.IsConstType) {
+ conv := convert(makeIdent(st.typename(tv.Type)), st.toExpr(kX))
+ if is[ast.Expr](kX) {
+ st.emit(conv)
+ }
+ return conv
+ }
+ return nil // definitely non-constant
+ }
+
+ // call f(x)
+
+ all := true // all args are possibly-constant
+ kArgs := make([]ast.Expr, len(e.Args))
+ for i, arg := range e.Args {
+ if kArg := st.expr(arg); kArg != nil {
+ kArgs[i] = st.toExpr(kArg)
+ } else {
+ all = false
+ }
+ }
+
+ // Calls to built-ins with fallibly constant arguments
+ // may become constant. All other calls are either
+ // constant or non-constant
+ if id, ok := e.Fun.(*ast.Ident); ok && all && tv.Value == nil {
+ if builtin, ok := st.info.Uses[id].(*types.Builtin); ok {
+ switch builtin.Name() {
+ case "len", "imag", "real", "complex", "min", "max":
+ return &ast.CallExpr{
+ Fun: id,
+ Args: kArgs,
+ Ellipsis: e.Ellipsis,
+ }
+ }
+ }
+ }
+
+ case *ast.StarExpr: // *T, *ptr
+ _ = st.expr(e.X)
+
+ case *ast.UnaryExpr:
+ // + - ! ^ & <- ~
+ //
+ // Possible "negation of minint".
+ // Emit constraint: -x
+ kX := st.expr(e.X)
+ if kX != nil && !is[types.TypeAndValue](kX) {
+ if e.Op == token.SUB {
+ st.emit(&ast.UnaryExpr{
+ Op: e.Op,
+ X: st.toExpr(kX),
+ })
+ }
+
+ return &ast.UnaryExpr{
+ Op: e.Op,
+ X: st.toExpr(kX),
+ }
+ }
+
+ case *ast.BinaryExpr:
+ kX := st.expr(e.X)
+ kY := st.expr(e.Y)
+ switch e.Op {
+ case token.QUO, token.REM:
+ // x/y, x%y
+ //
+ // Possible "integer division by zero" or
+ // "minint / -1" overflow.
+ // Emit constraint: x/y or 1/y
+ if kY != nil {
+ if kX == nil {
+ kX = makeIntLit(1)
+ }
+ st.emit(&ast.BinaryExpr{
+ Op: e.Op,
+ X: st.toExpr(kX),
+ Y: st.toExpr(kY),
+ })
+ }
+
+ case token.ADD, token.SUB, token.MUL:
+ // x+y, x-y, x*y
+ //
+ // Possible "arithmetic overflow".
+ // Emit constraint: x+y
+ if kX != nil && kY != nil {
+ st.emit(&ast.BinaryExpr{
+ Op: e.Op,
+ X: st.toExpr(kX),
+ Y: st.toExpr(kY),
+ })
+ }
+
+ case token.SHL, token.SHR:
+ // x << y, x >> y
+ //
+ // Possible "constant shift too large".
+ // Either operand may be too large individually,
+ // and they may be too large together.
+ // Emit constraint:
+ // x << y (if both maybe-constant)
+ // x << 0 (if y is non-constant)
+ // 1 << y (if x is non-constant)
+ if kX != nil || kY != nil {
+ x := st.toExpr(kX)
+ if x == nil {
+ x = makeIntLit(1)
+ }
+ y := st.toExpr(kY)
+ if y == nil {
+ y = makeIntLit(0)
+ }
+ st.emit(&ast.BinaryExpr{
+ Op: e.Op,
+ X: x,
+ Y: y,
+ })
+ }
+
+ case token.LSS, token.GTR, token.EQL, token.NEQ, token.LEQ, token.GEQ:
+ // < > == != <= <=
+ //
+ // A "x cmp y" expression with constant operands x, y is
+ // itself constant, but I can't see how a constant bool
+ // could be fallible: the compiler doesn't reject duplicate
+ // boolean cases in a switch, presumably because boolean
+ // switches are less like n-way branches and more like
+ // sequential if-else chains with possibly overlapping
+ // conditions; and there is (sadly) no way to convert a
+ // boolean constant to an int constant.
+ }
+ if kX != nil && kY != nil {
+ return &ast.BinaryExpr{
+ Op: e.Op,
+ X: st.toExpr(kX),
+ Y: st.toExpr(kY),
+ }
+ }
+
+ // types
+ //
+ // We need to visit types (and even type parameters)
+ // in order to reach all the places where things could go wrong:
+ //
+ // const (
+ // s = ""
+ // i = 0
+ // )
+ // type C[T [unsafe.Sizeof(func() { _ = s[i] })]int] bool
+
+ case *ast.IndexListExpr:
+ _ = st.expr(e.X)
+ for _, expr := range e.Indices {
+ _ = st.expr(expr)
+ }
+
+ case *ast.Ellipsis:
+ if e.Elt != nil {
+ _ = st.expr(e.Elt)
+ }
+
+ case *ast.ArrayType:
+ if e.Len != nil {
+ _ = st.expr(e.Len)
+ }
+ _ = st.expr(e.Elt)
+
+ case *ast.StructType:
+ st.fieldTypes(e.Fields)
+
+ case *ast.FuncType:
+ st.fieldTypes(e.TypeParams)
+ st.fieldTypes(e.Params)
+ st.fieldTypes(e.Results)
+
+ case *ast.InterfaceType:
+ st.fieldTypes(e.Methods)
+
+ case *ast.MapType:
+ _ = st.expr(e.Key)
+ _ = st.expr(e.Value)
+
+ case *ast.ChanType:
+ _ = st.expr(e.Value)
+ }
+ return
+}
+
+// toExpr converts the result of visitExpr to a falcon expression.
+// (We don't do this in visitExpr as we first need to discriminate
+// constants from maybe-constants.)
+func (st *falconState) toExpr(x any) ast.Expr {
+ switch x := x.(type) {
+ case nil:
+ return nil
+
+ case types.TypeAndValue:
+ lit := makeLiteral(x.Value)
+ if !isBasic(x.Type, types.IsUntyped) {
+ // convert to "typed" type
+ lit = &ast.CallExpr{
+ Fun: makeIdent(st.typename(x.Type)),
+ Args: []ast.Expr{lit},
+ }
+ }
+ return lit
+
+ case ast.Expr:
+ return x
+
+ default:
+ panic(x)
+ }
+}
+
+func makeLiteral(v constant.Value) ast.Expr {
+ switch v.Kind() {
+ case constant.Bool:
+ // Rather than refer to the true or false built-ins,
+ // which could be shadowed by poorly chosen parameter
+ // names, we use 0 == 0 for true and 0 != 0 for false.
+ op := token.EQL
+ if !constant.BoolVal(v) {
+ op = token.NEQ
+ }
+ return &ast.BinaryExpr{
+ Op: op,
+ X: makeIntLit(0),
+ Y: makeIntLit(0),
+ }
+
+ case constant.String:
+ return &ast.BasicLit{
+ Kind: token.STRING,
+ Value: v.ExactString(),
+ }
+
+ case constant.Int:
+ return &ast.BasicLit{
+ Kind: token.INT,
+ Value: v.ExactString(),
+ }
+
+ case constant.Float:
+ return &ast.BasicLit{
+ Kind: token.FLOAT,
+ Value: v.ExactString(),
+ }
+
+ case constant.Complex:
+ // The components could be float or int.
+ y := makeLiteral(constant.Imag(v))
+ y.(*ast.BasicLit).Value += "i" // ugh
+ if re := constant.Real(v); !consteq(re, kZeroInt) {
+ // complex: x + yi
+ y = &ast.BinaryExpr{
+ Op: token.ADD,
+ X: makeLiteral(re),
+ Y: y,
+ }
+ }
+ return y
+
+ default:
+ panic(v.Kind())
+ }
+}
+
+func makeIntLit(x int64) *ast.BasicLit {
+ return &ast.BasicLit{
+ Kind: token.INT,
+ Value: strconv.FormatInt(x, 10),
+ }
+}
+
+func isBasic(t types.Type, info types.BasicInfo) bool {
+ basic, ok := t.Underlying().(*types.Basic)
+ return ok && basic.Info()&info != 0
+}
diff --git a/internal/refactor/inline/falcon_test.go b/internal/refactor/inline/falcon_test.go
new file mode 100644
index 00000000000..64a98f511b5
--- /dev/null
+++ b/internal/refactor/inline/falcon_test.go
@@ -0,0 +1,381 @@
+// 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 inline_test
+
+import "testing"
+
+// Testcases mostly come in pairs, of a success and a failure
+// to substitute based on specific constant argument values.
+
+func TestFalconStringIndex(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Non-negative string index.",
+ `func f(i int) byte { return s[i] }; var s string`,
+ `func _() { f(0) }`,
+ `func _() { _ = s[0] }`,
+ },
+ {
+ "Negative string index.",
+ `func f(i int) byte { return s[i] }; var s string`,
+ `func _() { f(-1) }`,
+ `func _() {
+ var i int = -1
+ _ = s[i]
+}`,
+ },
+ {
+ "String index in range.",
+ `func f(s string, i int) byte { return s[i] }`,
+ `func _() { f("-", 0) }`,
+ `func _() { _ = "-"[0] }`,
+ },
+ {
+ "String index out of range.",
+ `func f(s string, i int) byte { return s[i] }`,
+ `func _() { f("-", 1) }`,
+ `func _() {
+ var (
+ s string = "-"
+ i int = 1
+ )
+ _ = s[i]
+}`,
+ },
+ {
+ "Remove known prefix (OK)",
+ `func f(s, prefix string) string { return s[:len(prefix)] }`,
+ `func _() { f("", "") }`,
+ `func _() { _ = ""[:len("")] }`,
+ },
+ {
+ "Remove not-a-prefix (out of range)",
+ `func f(s, prefix string) string { return s[:len(prefix)] }`,
+ `func _() { f("", "pre") }`,
+ `func _() {
+ var s, prefix string = "", "pre"
+ _ = s[:len(prefix)]
+}`,
+ },
+ })
+}
+
+func TestFalconSliceIndices(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Monotonic (0<=i<=j) slice indices (len unknown).",
+ `func f(i, j int) []int { return s[i:j] }; var s []int`,
+ `func _() { f(0, 1) }`,
+ `func _() { _ = s[0:1] }`,
+ },
+ {
+ "Non-monotonic slice indices (len unknown).",
+ `func f(i, j int) []int { return s[i:j] }; var s []int`,
+ `func _() { f(1, 0) }`,
+ `func _() {
+ var i, j int = 1, 0
+ _ = s[i:j]
+}`,
+ },
+ {
+ "Negative slice index.",
+ `func f(i, j int) []int { return s[i:j] }; var s []int`,
+ `func _() { f(-1, 1) }`,
+ `func _() {
+ var i, j int = -1, 1
+ _ = s[i:j]
+}`,
+ },
+ })
+}
+
+func TestFalconMapKeys(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Unique map keys (int)",
+ `func f(x int) { _ = map[int]bool{1: true, x: true} }`,
+ `func _() { f(2) }`,
+ `func _() { _ = map[int]bool{1: true, 2: true} }`,
+ },
+ {
+ "Duplicate map keys (int)",
+ `func f(x int) { _ = map[int]bool{1: true, x: true} }`,
+ `func _() { f(1) }`,
+ `func _() {
+ var x int = 1
+ _ = map[int]bool{1: true, x: true}
+}`,
+ },
+ {
+ "Unique map keys (varied built-in types)",
+ `func f(x int16) { _ = map[any]bool{1: true, x: true} }`,
+ `func _() { f(2) }`,
+ `func _() { _ = map[any]bool{1: true, int16(2): true} }`,
+ },
+ {
+ "Duplicate map keys (varied built-in types)",
+ `func f(x int16) { _ = map[any]bool{1: true, x: true} }`,
+ `func _() { f(1) }`,
+ `func _() { _ = map[any]bool{1: true, int16(1): true} }`,
+ },
+ {
+ "Unique map keys (varied user-defined types)",
+ `func f(x myint) { _ = map[any]bool{1: true, x: true} }; type myint int`,
+ `func _() { f(2) }`,
+ `func _() { _ = map[any]bool{1: true, myint(2): true} }`,
+ },
+ {
+ "Duplicate map keys (varied user-defined types)",
+ `func f(x myint, y myint2) { _ = map[any]bool{x: true, y: true} }; type (myint int; myint2 int)`,
+ `func _() { f(1, 1) }`,
+ `func _() {
+ var (
+ x myint = 1
+ y myint2 = 1
+ )
+ _ = map[any]bool{x: true, y: true}
+}`,
+ },
+ {
+ "Duplicate map keys (user-defined alias to built-in)",
+ `func f(x myint, y int) { _ = map[any]bool{x: true, y: true} }; type myint = int`,
+ `func _() { f(1, 1) }`,
+ `func _() {
+ var (
+ x myint = 1
+ y int = 1
+ )
+ _ = map[any]bool{x: true, y: true}
+}`,
+ },
+ })
+}
+
+func TestFalconSwitchCases(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Unique switch cases (int).",
+ `func f(x int) { switch 0 { case x: case 1: } }`,
+ `func _() { f(2) }`,
+ `func _() {
+ switch 0 {
+ case 2:
+ case 1:
+ }
+}`,
+ },
+ {
+ "Duplicate switch cases (int).",
+ `func f(x int) { switch 0 { case x: case 1: } }`,
+ `func _() { f(1) }`,
+ `func _() {
+ var x int = 1
+ switch 0 {
+ case x:
+ case 1:
+ }
+}`,
+ },
+ {
+ "Unique switch cases (varied built-in types).",
+ `func f(x int) { switch any(nil) { case x: case int16(1): } }`,
+ `func _() { f(2) }`,
+ `func _() {
+ switch any(nil) {
+ case 2:
+ case int16(1):
+ }
+}`,
+ },
+ {
+ "Duplicate switch cases (varied built-in types).",
+ `func f(x int) { switch any(nil) { case x: case int16(1): } }`,
+ `func _() { f(1) }`,
+ `func _() {
+ switch any(nil) {
+ case 1:
+ case int16(1):
+ }
+}`,
+ },
+ })
+}
+
+func TestFalconDivision(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Division by two.",
+ `func f(x, y int) int { return x / y }`,
+ `func _() { f(1, 2) }`,
+ `func _() { _ = 1 / 2 }`,
+ },
+ {
+ "Division by zero.",
+ `func f(x, y int) int { return x / y }`,
+ `func _() { f(1, 0) }`,
+ `func _() {
+ var x, y int = 1, 0
+ _ = x / y
+}`,
+ },
+ {
+ "Division by two (statement).",
+ `func f(x, y int) { x /= y }`,
+ `func _() { f(1, 2) }`,
+ `func _() {
+ var x int = 1
+ x /= 2
+}`,
+ },
+ {
+ "Division by zero (statement).",
+ `func f(x, y int) { x /= y }`,
+ `func _() { f(1, 0) }`,
+ `func _() {
+ var x, y int = 1, 0
+ x /= y
+}`,
+ },
+ {
+ "Division of minint by two (ok).",
+ `func f(x, y int32) { _ = x / y }`,
+ `func _() { f(-0x80000000, 2) }`,
+ `func _() { _ = int32(-0x80000000) / int32(2) }`,
+ },
+ {
+ "Division of minint by -1 (overflow).",
+ `func f(x, y int32) { _ = x / y }`,
+ `func _() { f(-0x80000000, -1) }`,
+ `func _() {
+ var x, y int32 = -0x80000000, -1
+ _ = x / y
+}`,
+ },
+ })
+}
+
+func TestFalconMinusMinInt(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Negation of maxint.",
+ `func f(x int32) int32 { return -x }`,
+ `func _() { f(0x7fffffff) }`,
+ `func _() { _ = -int32(0x7fffffff) }`,
+ },
+ {
+ "Negation of minint.",
+ `func f(x int32) int32 { return -x }`,
+ `func _() { f(-0x80000000) }`,
+ `func _() {
+ var x int32 = -0x80000000
+ _ = -x
+}`,
+ },
+ })
+}
+
+func TestFalconArithmeticOverflow(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Addition without overflow.",
+ `func f(x, y int32) int32 { return x + y }`,
+ `func _() { f(100, 200) }`,
+ `func _() { _ = int32(100) + int32(200) }`,
+ },
+ {
+ "Addition with overflow.",
+ `func f(x, y int32) int32 { return x + y }`,
+ `func _() { f(1<<30, 1<<30) }`,
+ `func _() {
+ var x, y int32 = 1 << 30, 1 << 30
+ _ = x + y
+}`,
+ },
+ {
+ "Conversion in range.",
+ `func f(x int) int8 { return int8(x) }`,
+ `func _() { f(123) }`,
+ `func _() { _ = int8(123) }`,
+ },
+ {
+ "Conversion out of range.",
+ `func f(x int) int8 { return int8(x) }`,
+ `func _() { f(456) }`,
+ `func _() {
+ var x int = 456
+ _ = int8(x)
+}`,
+ },
+ })
+}
+
+func TestFalconComplex(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Complex arithmetic (good).",
+ `func f(re, im float64, z complex128) byte { return "x"[int(real(complex(re, im)*complex(re, -im)-z))] }`,
+ `func _() { f(1, 2, 5+0i) }`,
+ `func _() { _ = "x"[int(real(complex(float64(1), float64(2))*complex(float64(1), -float64(2))-(5+0i)))] }`,
+ },
+ {
+ "Complex arithmetic (bad).",
+ `func f(re, im float64, z complex128) byte { return "x"[int(real(complex(re, im)*complex(re, -im)-z))] }`,
+ `func _() { f(1, 3, 5+0i) }`,
+ `func _() {
+ var (
+ re, im float64 = 1, 3
+ z complex128 = 5 + 0i
+ )
+ _ = "x"[int(real(complex(re, im)*complex(re, -im)-z))]
+}`,
+ },
+ })
+}
+func TestFalconMisc(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Compound constant expression (good).",
+ `func f(x, y string, i, j int) byte { return x[i*len(y)+j] }`,
+ `func _() { f("abc", "xy", 2, -3) }`,
+ `func _() { _ = "abc"[2*len("xy")+-3] }`,
+ },
+ {
+ "Compound constant expression (index out of range).",
+ `func f(x, y string, i, j int) byte { return x[i*len(y)+j] }`,
+ `func _() { f("abc", "xy", 4, -3) }`,
+ `func _() {
+ var (
+ x, y string = "abc", "xy"
+ i, j int = 4, -3
+ )
+ _ = x[i*len(y)+j]
+}`,
+ },
+ {
+ "Constraints within nested functions (good).",
+ `func f(x int) { _ = func() { _ = [1]int{}[x] } }`,
+ `func _() { f(0) }`,
+ `func _() { _ = func() { _ = [1]int{}[0] } }`,
+ },
+ {
+ "Constraints within nested functions (bad).",
+ `func f(x int) { _ = func() { _ = [1]int{}[x] } }`,
+ `func _() { f(1) }`,
+ `func _() {
+ var x int = 1
+ _ = func() { _ = [1]int{}[x] }
+}`,
+ },
+ {
+ "Falcon violation rejects only the constant arguments (x, z).",
+ `func f(x, y, z string) string { return x[:2] + y + z[:2] }; var b string`,
+ `func _() { f("a", b, "c") }`,
+ `func _() {
+ var x, z string = "a", "c"
+ _ = x[:2] + b + z[:2]
+}`,
+ },
+ })
+}
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
index 9167498fc35..9615ab43c59 100644
--- a/internal/refactor/inline/inline.go
+++ b/internal/refactor/inline/inline.go
@@ -2,205 +2,20 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Package inline implements inlining of Go function calls.
-//
-// The client provides information about the caller and callee,
-// including the source text, syntax tree, and type information, and
-// the inliner returns the modified source file for the caller, or an
-// error if the inlining operation is invalid (for example because the
-// function body refers to names that are inaccessible to the caller).
-//
-// Although this interface demands more information from the client
-// than might seem necessary, it enables smoother integration with
-// existing batch and interactive tools that have their own ways of
-// managing the processes of reading, parsing, and type-checking
-// packages. In particular, this package does not assume that the
-// caller and callee belong to the same token.FileSet or
-// types.Importer realms.
-//
-// In general, inlining consists of modifying a function or method
-// call expression f(a1, ..., an) so that the name of the function f
-// is replaced ("literalized") by a literal copy of the function
-// declaration, with free identifiers suitably modified to use the
-// locally appropriate identifiers or perhaps constant argument
-// values.
-//
-// Inlining must not change the semantics of the call. Semantics
-// preservation is crucial for clients such as codebase maintenance
-// tools that automatically inline all calls to designated functions
-// on a large scale. Such tools must not introduce subtle behavior
-// changes. (Fully inlining a call is dynamically observable using
-// reflection over the call stack, but this exception to the rule is
-// explicitly allowed.)
-//
-// In some special cases it is possible to entirely replace ("reduce")
-// the call by a copy of the function's body in which parameters have
-// been replaced by arguments, but this is surprisingly tricky for a
-// number of reasons, some of which are listed here for illustration:
-//
-// - Any effects of the call argument expressions must be preserved,
-// even if the corresponding parameters are never referenced, or are
-// referenced multiple times, or are referenced in a different order
-// from the arguments.
-//
-// - Even an argument expression as simple as ptr.x may not be
-// referentially transparent, because another argument may have the
-// effect of changing the value of ptr.
-//
-// - Although constants are referentially transparent, as a matter of
-// style we do not wish to duplicate literals that are referenced
-// multiple times in the body because this undoes proper factoring.
-// Also, string literals may be arbitrarily large.
-//
-// - If the function body consists of statements other than just
-// "return expr", in some contexts it may be syntactically
-// impossible to replace the call expression by the body statements.
-// Consider "} else if x := f(); cond { ... }".
-// (Go has no equivalent to Lisp's progn or Rust's blocks.)
-//
-// - Similarly, without the equivalent of Rust-style blocks and
-// first-class tuples, there is no general way to reduce a call
-// to a function such as
-// > func(params)(args)(results) { stmts; return body }
-// to an expression such as
-// > { var params = args; stmts; body }
-// or even a statement such as
-// > results = { var params = args; stmts; body }
-// Consequently the declaration and scope of the result variables,
-// and the assignment and control-flow implications of the return
-// statement, must be dealt with by cases.
-//
-// - A standalone call statement that calls a function whose body is
-// "return expr" cannot be simply replaced by the body expression
-// if it is not itself a call or channel receive expression; it is
-// necessary to explicitly discard the result using "_ = expr".
-//
-// Similarly, if the body is a call expression, only calls to some
-// built-in functions with no result (such as copy or panic) are
-// permitted as statements, whereas others (such as append) return
-// a result that must be used, even if just by discarding.
-//
-// - If a parameter or result variable is updated by an assignment
-// within the function body, it cannot always be safely replaced
-// by a variable in the caller. For example, given
-// > func f(a int) int { a++; return a }
-// The call y = f(x) cannot be replaced by { x++; y = x } because
-// this would change the value of the caller's variable x.
-// Only if the caller is finished with x is this safe.
-//
-// A similar argument applies to parameter or result variables
-// that escape: by eliminating a variable, inlining would change
-// the identity of the variable that escapes.
-//
-// - If the function body uses 'defer' and the inlined call is not a
-// tail-call, inlining may delay the deferred effects.
-//
-// - Each control label that is used by both caller and callee must
-// be α-renamed.
-//
-// - Given
-// > func f() uint8 { return 0 }
-// > var x any = f()
-// reducing the call to var x any = 0 is unsound because it
-// discards the implicit conversion. We may need to make each
-// argument->parameter and return->result assignment conversion
-// implicit if the types differ. Assignments to variadic
-// parameters may need to explicitly construct a slice.
-//
-// More complex callee functions are inlinable with more elaborate and
-// invasive changes to the statements surrounding the call expression.
-//
-// TODO(adonovan): future work:
-//
-// - Handle more of the above special cases by careful analysis,
-// thoughtful factoring of the large design space, and thorough
-// test coverage.
-//
-// - Write a fuzz-like test that selects function calls at
-// random in the corpus, inlines them, and checks that the
-// result is either a sensible error or a valid transformation.
-//
-// - Eliminate parameters that are unreferenced in the callee
-// and whose argument expression is side-effect free.
-//
-// - Afford the client more control such as a limit on the total
-// increase in line count, or a refusal to inline using the
-// general approach (replacing name by function literal). This
-// could be achieved by returning metadata alongside the result
-// and having the client conditionally discard the change.
-//
-// - Is it acceptable to skip effects that are limited to runtime
-// panics? Can we avoid evaluating an argument x.f
-// or a[i] when the corresponding parameter is unused?
-//
-// - When caller syntax permits a block, replace argument-to-parameter
-// assignment by a set of local var decls, e.g. f(1, 2) would
-// become { var x, y = 1, 2; body... }.
-//
-// But even this is complicated: a single var decl initializer
-// cannot declare all the parameters and initialize them to their
-// arguments in one go if they have varied types. Instead,
-// one must use multiple specs such as:
-// > { var x int = 1; var y int32 = 2; body ...}
-// but this means that the initializer expression for y is
-// within the scope of x, so it may require α-renaming.
-//
-// It is tempting to use a short var decl { x, y := 1, 2; body ...}
-// as it permits simultaneous declaration and initialization
-// of many variables of varied type. However, one must take care
-// to convert each argument expression to the correct parameter
-// variable type, perhaps explicitly. (Consider "x := 1 << 64".)
-//
-// Also, as a matter of style, having all parameter declarations
-// and argument expressions in a single statement is potentially
-// unwieldy.
-//
-// - Support inlining of generic functions, replacing type parameters
-// by their instantiations.
-//
-// - Support inlining of calls to function literals such as:
-// > f := func(...) { ...}
-// > f()
-// including recursive ones:
-// > var f func(...)
-// > f = func(...) { ...f...}
-// > f()
-// But note that the existing algorithm makes widespread assumptions
-// that the callee is a package-level function or method.
-//
-// - Eliminate parens inserted conservatively when they are redundant.
-//
-// - Allow non-'go' build systems such as Bazel/Blaze a chance to
-// decide whether an import is accessible using logic other than
-// "/internal/" path segments. This could be achieved by returning
-// the list of added import paths.
-//
-// - Inlining a function from another module may change the
-// effective version of the Go language spec that governs it. We
-// should probably make the client responsible for rejecting
-// attempts to inline from newer callees to older callers, since
-// there's no way for this package to access module versions.
-//
-// - Use an alternative implementation of the import-organizing
-// operation that doesn't require operating on a complete file
-// (and reformatting). Then return the results in a higher-level
-// form as a set of import additions and deletions plus a single
-// diff that encloses the call expression. This interface could
-// perhaps be implemented atop imports.Process by post-processing
-// its result to obtain the abstract import changes and discarding
-// its formatted output.
package inline
import (
"bytes"
"fmt"
"go/ast"
+ "go/constant"
+ "go/format"
+ "go/parser"
"go/token"
"go/types"
- "log"
pathpkg "path"
"reflect"
- "sort"
+ "strconv"
"strings"
"golang.org/x/tools/go/ast/astutil"
@@ -218,21 +33,299 @@ type Caller struct {
Info *types.Info
File *ast.File
Call *ast.CallExpr
- Content []byte
-}
+ Content []byte // source of file containing
-func (caller *Caller) offset(pos token.Pos) int { return offsetOf(caller.Fset, pos) }
+ path []ast.Node // path from call to root of file syntax tree
+ enclosingFunc *ast.FuncDecl // top-level function/method enclosing the call, if any
+}
// Inline inlines the called function (callee) into the function call (caller)
// and returns the updated, formatted content of the caller source file.
-func Inline(caller *Caller, callee_ *Callee) ([]byte, error) {
- callee := &callee_.impl
+//
+// Inline does not mutate any public fields of Caller or Callee.
+//
+// The log records the decision-making process.
+//
+// TODO(adonovan): provide an API for clients that want structured
+// output: a list of import additions and deletions plus one or more
+// localized diffs (or even AST transformations, though ownership and
+// mutation are tricky) near the call site.
+func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte, error) {
+ logf("inline %s @ %v",
+ debugFormatNode(caller.Fset, caller.Call),
+ caller.Fset.PositionFor(caller.Call.Lparen, false))
+
+ if !consistentOffsets(caller) {
+ return nil, fmt.Errorf("internal error: caller syntax positions are inconsistent with file content (did you forget to use FileSet.PositionFor when computing the file name?)")
+ }
+
+ // TODO(adonovan): use go1.21's ast.IsGenerated.
+ // Break the string literal so we can use inlining in this file. :)
+ if bytes.Contains(caller.Content, []byte("// Code generated by "+"cmd/cgo; DO NOT EDIT.")) {
+ return nil, fmt.Errorf("cannot inline calls from files that import \"C\"")
+ }
+
+ res, err := inline(logf, caller, &callee.impl)
+ if err != nil {
+ return nil, err
+ }
+
+ // Replace the call (or some node that encloses it) by new syntax.
+ assert(res.old != nil, "old is nil")
+ assert(res.new != nil, "new is nil")
+
+ // A single return operand inlined to a unary
+ // expression context may need parens. Otherwise:
+ // func two() int { return 1+1 }
+ // print(-two()) => print(-1+1) // oops!
+ //
+ // Usually it is not necessary to insert ParenExprs
+ // as the formatter is smart enough to insert them as
+ // needed by the context. But the res.{old,new}
+ // substitution is done by formatting res.new in isolation
+ // and then splicing its text over res.old, so the
+ // formatter doesn't see the parent node and cannot do
+ // the right thing. (One solution would be to always
+ // format the enclosing node of old, but that requires
+ // non-lossy comment handling, #20744.)
+ //
+ // So, we must analyze the call's context
+ // to see whether ambiguity is possible.
+ // For example, if the context is x[y:z], then
+ // the x subtree is subject to precedence ambiguity
+ // (replacing x by p+q would give p+q[y:z] which is wrong)
+ // but the y and z subtrees are safe.
+ if needsParens(caller.path, res.old, res.new) {
+ res.new = &ast.ParenExpr{X: res.new.(ast.Expr)}
+ }
+
+ // Some reduction strategies return a new block holding the
+ // callee's statements. The block's braces may be elided when
+ // there is no conflict between names declared in the block
+ // with those declared by the parent block, and no risk of
+ // a caller's goto jumping forward across a declaration.
+ //
+ // This elision is only safe when the ExprStmt is beneath a
+ // BlockStmt, CaseClause.Body, or CommClause.Body;
+ // (see "statement theory").
+ elideBraces := false
+ if newBlock, ok := res.new.(*ast.BlockStmt); ok {
+ i := nodeIndex(caller.path, res.old)
+ parent := caller.path[i+1]
+ var body []ast.Stmt
+ switch parent := parent.(type) {
+ case *ast.BlockStmt:
+ body = parent.List
+ case *ast.CommClause:
+ body = parent.Body
+ case *ast.CaseClause:
+ body = parent.Body
+ }
+ if body != nil {
+ callerNames := declares(body)
+
+ // If BlockStmt is a function body,
+ // include its receiver, params, and results.
+ addFieldNames := func(fields *ast.FieldList) {
+ if fields != nil {
+ for _, field := range fields.List {
+ for _, id := range field.Names {
+ callerNames[id.Name] = true
+ }
+ }
+ }
+ }
+ switch f := caller.path[i+2].(type) {
+ case *ast.FuncDecl:
+ addFieldNames(f.Recv)
+ addFieldNames(f.Type.Params)
+ addFieldNames(f.Type.Results)
+ case *ast.FuncLit:
+ addFieldNames(f.Type.Params)
+ addFieldNames(f.Type.Results)
+ }
+
+ if len(callerLabels(caller.path)) > 0 {
+ // TODO(adonovan): be more precise and reject
+ // only forward gotos across the inlined block.
+ logf("keeping block braces: caller uses control labels")
+ } else if intersects(declares(newBlock.List), callerNames) {
+ logf("keeping block braces: avoids name conflict")
+ } else {
+ elideBraces = true
+ }
+ }
+ }
+
+ // Don't call replaceNode(caller.File, res.old, res.new)
+ // as it mutates the caller's syntax tree.
+ // Instead, splice the file, replacing the extent of the "old"
+ // node by a formatting of the "new" node, and re-parse.
+ // We'll fix up the imports on this new tree, and format again.
+ var f *ast.File
+ {
+ start := offsetOf(caller.Fset, res.old.Pos())
+ end := offsetOf(caller.Fset, res.old.End())
+ var out bytes.Buffer
+ out.Write(caller.Content[:start])
+ // TODO(adonovan): might it make more sense to use
+ // callee.Fset when formatting res.new?
+ // The new tree is a mix of (cloned) caller nodes for
+ // the argument expressions and callee nodes for the
+ // function body. In essence the question is: which
+ // is more likely to have comments?
+ // Usually the callee body will be larger and more
+ // statement-heavy than the the arguments, but a
+ // strategy may widen the scope of the replacement
+ // (res.old) from CallExpr to, say, its enclosing
+ // block, so the caller nodes dominate.
+ // Precise comment handling would make this a
+ // non-issue. Formatting wouldn't really need a
+ // FileSet at all.
+ mark := out.Len()
+ if err := format.Node(&out, caller.Fset, res.new); err != nil {
+ return nil, err
+ }
+ if elideBraces {
+ // Overwrite unnecessary {...} braces with spaces.
+ // TODO(adonovan): less hacky solution.
+ out.Bytes()[mark] = ' '
+ out.Bytes()[out.Len()-1] = ' '
+ }
+ out.Write(caller.Content[end:])
+ const mode = parser.ParseComments | parser.SkipObjectResolution | parser.AllErrors
+ f, err = parser.ParseFile(caller.Fset, "callee.go", &out, mode)
+ if err != nil {
+ // Something has gone very wrong.
+ logf("failed to parse <<%s>>", &out) // debugging
+ return nil, err
+ }
+ }
+
+ // Add new imports.
+ //
+ // Insert new imports after last existing import,
+ // to avoid migration of pre-import comments.
+ // The imports will be organized below.
+ if len(res.newImports) > 0 {
+ var importDecl *ast.GenDecl
+ if len(f.Imports) > 0 {
+ // Append specs to existing import decl
+ importDecl = f.Decls[0].(*ast.GenDecl)
+ } else {
+ // Insert new import decl.
+ importDecl = &ast.GenDecl{Tok: token.IMPORT}
+ f.Decls = prepend[ast.Decl](importDecl, f.Decls...)
+ }
+ for _, spec := range res.newImports {
+ // Check that the new imports are accessible.
+ path, _ := strconv.Unquote(spec.Path.Value)
+ if !canImport(caller.Types.Path(), path) {
+ return nil, fmt.Errorf("can't inline function %v as its body refers to inaccessible package %q", callee, path)
+ }
+ importDecl.Specs = append(importDecl.Specs, spec)
+ }
+ }
+
+ var out bytes.Buffer
+ if err := format.Node(&out, caller.Fset, f); err != nil {
+ return nil, err
+ }
+ newSrc := out.Bytes()
+
+ // Remove imports that are no longer referenced.
+ //
+ // It ought to be possible to compute the set of PkgNames used
+ // by the "old" code, compute the free identifiers of the
+ // "new" code using a syntax-only (no go/types) algorithm, and
+ // see if the reduction in the number of uses of any PkgName
+ // equals the number of times it appears in caller.Info.Uses,
+ // indicating that it is no longer referenced by res.new.
+ //
+ // However, the notorious ambiguity of resolving T{F: 0} makes this
+ // unreliable: without types, we can't tell whether F refers to
+ // a field of struct T, or a package-level const/var of a
+ // dot-imported (!) package.
+ //
+ // So, for now, we run imports.Process, which is
+ // unsatisfactory as it has to run the go command, and it
+ // looks at the user's module cache state--unnecessarily,
+ // since this step cannot add new imports.
+ //
+ // TODO(adonovan): replace with a simpler implementation since
+ // all the necessary imports are present but merely untidy.
+ // That will be faster, and also less prone to nondeterminism
+ // if there are bugs in our logic for import maintenance.
+ //
+ // However, golang.org/x/tools/internal/imports.ApplyFixes is
+ // too simple as it requires the caller to have figured out
+ // all the logical edits. In our case, we know all the new
+ // imports that are needed (see newImports), each of which can
+ // be specified as:
+ //
+ // &imports.ImportFix{
+ // StmtInfo: imports.ImportInfo{path, name,
+ // IdentName: name,
+ // FixType: imports.AddImport,
+ // }
+ //
+ // but we don't know which imports are made redundant by the
+ // inlining itself. For example, inlining a call to
+ // fmt.Println may make the "fmt" import redundant.
+ //
+ // Also, both imports.Process and internal/imports.ApplyFixes
+ // reformat the entire file, which is not ideal for clients
+ // such as gopls. (That said, the point of a canonical format
+ // is arguably that any tool can reformat as needed without
+ // this being inconvenient.)
+ //
+ // We could invoke imports.Process and parse its result,
+ // compare against the original AST, compute a list of import
+ // fixes, and return that too.
+
+ // Recompute imports only if there were existing ones.
+ if len(f.Imports) > 0 {
+ formatted, err := imports.Process("output", newSrc, nil)
+ if err != nil {
+ logf("cannot reformat: %v <<%s>>", err, &out)
+ return nil, err // cannot reformat (a bug?)
+ }
+ newSrc = formatted
+ }
+ return newSrc, nil
+}
+
+type result struct {
+ newImports []*ast.ImportSpec
+ old, new ast.Node // e.g. replace call expr by callee function body expression
+}
- // -- check caller --
+// inline returns a pair of an old node (the call, or something
+// enclosing it) and a new node (its replacement, which may be a
+// combination of caller, callee, and new nodes), along with the set
+// of new imports needed.
+//
+// TODO(adonovan): rethink the 'result' interface. The assumption of a
+// one-to-one replacement seems fragile. One can easily imagine the
+// transformation replacing the call and adding new variable
+// declarations, for example, or replacing a call statement by zero or
+// many statements.)
+//
+// TODO(adonovan): in earlier drafts, the transformation was expressed
+// by splicing substrings of the two source files because syntax
+// trees don't preserve comments faithfully (see #20744), but such
+// transformations don't compose. The current implementation is
+// tree-based but is very lossy wrt comments. It would make a good
+// candidate for evaluating an alternative fully self-contained tree
+// representation, such as any proposed solution to #20744, or even
+// dst or some private fork of go/ast.)
+func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*result, error) {
+ checkInfoFields(caller.Info)
// Inlining of dynamic calls is not currently supported,
- // even for local closure calls.
- if typeutil.StaticCallee(caller.Info, caller.Call) == nil {
+ // even for local closure calls. (This would be a lot of work.)
+ calleeSymbol := typeutil.StaticCallee(caller.Info, caller.Call)
+ if calleeSymbol == nil {
// e.g. interface method
return nil, fmt.Errorf("cannot inline: not a static function call")
}
@@ -247,79 +340,117 @@ func Inline(caller *Caller, callee_ *Callee) ([]byte, error) {
// -- analyze callee's free references in caller context --
- // syntax path enclosing Call, innermost first (Path[0]=Call)
- callerPath, _ := astutil.PathEnclosingInterval(caller.File, caller.Call.Pos(), caller.Call.End())
- callerLookup := func(name string, pos token.Pos) types.Object {
- for _, n := range callerPath {
- // The function body scope (containing not just params)
- // is associated with FuncDecl.Type, not FuncDecl.Body.
- if decl, ok := n.(*ast.FuncDecl); ok {
- n = decl.Type
- }
- if scope := caller.Info.Scopes[n]; scope != nil {
- if _, obj := scope.LookupParent(name, pos); obj != nil {
- return obj
- }
- }
+ // Compute syntax path enclosing Call, innermost first (Path[0]=Call),
+ // and outermost enclosing function, if any.
+ caller.path, _ = astutil.PathEnclosingInterval(caller.File, caller.Call.Pos(), caller.Call.End())
+ for _, n := range caller.path {
+ if decl, ok := n.(*ast.FuncDecl); ok {
+ caller.enclosingFunc = decl
+ break
}
- return nil
}
- // Import map, initially populated with caller imports.
+ // If call is within a function, analyze all its
+ // local vars for the "single assignment" property.
+ // (Taking the address &v counts as a potential assignment.)
+ var assign1 func(v *types.Var) bool // reports whether v a single-assignment local var
+ {
+ updatedLocals := make(map[*types.Var]bool)
+ if caller.enclosingFunc != nil {
+ escape(caller.Info, caller.enclosingFunc, func(v *types.Var, _ bool) {
+ updatedLocals[v] = true
+ })
+ logf("multiple-assignment vars: %v", updatedLocals)
+ }
+ assign1 = func(v *types.Var) bool { return !updatedLocals[v] }
+ }
+
+ // import map, initially populated with caller imports.
//
// For simplicity we ignore existing dot imports, so that a
// qualified identifier (QI) in the callee is always
// represented by a QI in the caller, allowing us to treat a
// QI like a selection on a package name.
- importMap := make(map[string]string) // maps package path to local name
+ importMap := make(map[string][]string) // maps package path to local name(s)
for _, imp := range caller.File.Imports {
- if pkgname, ok := importedPkgName(caller.Info, imp); ok && pkgname.Name() != "." {
- importMap[pkgname.Imported().Path()] = pkgname.Name()
+ if pkgname, ok := importedPkgName(caller.Info, imp); ok &&
+ pkgname.Name() != "." &&
+ pkgname.Name() != "_" {
+ path := pkgname.Imported().Path()
+ importMap[path] = append(importMap[path], pkgname.Name())
}
}
// localImportName returns the local name for a given imported package path.
- var newImports []string
- localImportName := func(path string) string {
- name, ok := importMap[path]
- if !ok {
- // import added by callee
- //
- // Choose local PkgName based on last segment of
- // package path plus, if needed, a numeric suffix to
- // ensure uniqueness.
- //
- // TODO(adonovan): preserve the PkgName used
- // in the original source, or, for a dot import,
- // use the package's declared name.
- base := pathpkg.Base(path)
- name = base
- for n := 0; callerLookup(name, caller.Call.Pos()) != nil; n++ {
- name = fmt.Sprintf("%s%d", base, n)
- }
-
- // TODO(adonovan): don't use a renaming import
- // unless the local name differs from either
- // the package name or the last segment of path.
- // This requires that we tabulate (path, declared name, local name)
- // triples for each package referenced by the callee.
- newImports = append(newImports, fmt.Sprintf("%s %q", name, path))
- importMap[path] = name
+ var newImports []*ast.ImportSpec
+ localImportName := func(path string, shadows map[string]bool) string {
+ // Does an import exist?
+ for _, name := range importMap[path] {
+ // Check that either the import preexisted,
+ // or that it was newly added (no PkgName) but is not shadowed,
+ // either in the callee (shadows) or caller (caller.lookup).
+ if !shadows[name] {
+ found := caller.lookup(name)
+ if is[*types.PkgName](found) || found == nil {
+ return name
+ }
+ }
+ }
+
+ newlyAdded := func(name string) bool {
+ for _, new := range newImports {
+ if new.Name.Name == name {
+ return true
+ }
+ }
+ return false
}
+
+ // import added by callee
+ //
+ // Choose local PkgName based on last segment of
+ // package path plus, if needed, a numeric suffix to
+ // ensure uniqueness.
+ //
+ // "init" is not a legal PkgName.
+ //
+ // TODO(adonovan): preserve the PkgName used
+ // in the original source, or, for a dot import,
+ // use the package's declared name.
+ base := pathpkg.Base(path)
+ name := base
+ for n := 0; shadows[name] || caller.lookup(name) != nil || newlyAdded(name) || name == "init"; n++ {
+ name = fmt.Sprintf("%s%d", base, n)
+ }
+
+ // TODO(adonovan): don't use a renaming import
+ // unless the local name differs from either
+ // the package name or the last segment of path.
+ // This requires that we tabulate (path, declared name, local name)
+ // triples for each package referenced by the callee.
+ logf("adding import %s %q", name, path)
+ newImports = append(newImports, &ast.ImportSpec{
+ Name: makeIdent(name),
+ Path: &ast.BasicLit{
+ Kind: token.STRING,
+ Value: strconv.Quote(path),
+ },
+ })
+ importMap[path] = append(importMap[path], name)
return name
}
// Compute the renaming of the callee's free identifiers.
- objRenames := make([]string, len(callee.FreeObjs)) // "" => no rename
+ objRenames := make([]ast.Expr, len(callee.FreeObjs)) // nil => no change
for i, obj := range callee.FreeObjs {
// obj is a free object of the callee.
//
// Possible cases are:
- // - nil or a builtin
+ // - builtin function, type, or value (e.g. nil, zero)
// => check not shadowed in caller.
// - package-level var/func/const/types
// => same package: check not shadowed in caller.
- // => otherwise: import other package form a qualified identifier.
+ // => otherwise: import other package, form a qualified identifier.
// (Unexported cross-package references were rejected already.)
// - type parameter
// => not yet supported
@@ -328,37 +459,39 @@ func Inline(caller *Caller, callee_ *Callee) ([]byte, error) {
//
// There can be no free references to labels, fields, or methods.
- var newName string
+ // Note that we must consider potential shadowing both
+ // at the caller side (caller.lookup) and, when
+ // choosing new PkgNames, within the callee (obj.shadow).
+
+ var newName ast.Expr
if obj.Kind == "pkgname" {
// Use locally appropriate import, creating as needed.
- newName = localImportName(obj.PkgPath) // imported package
+ newName = makeIdent(localImportName(obj.PkgPath, obj.Shadow)) // imported package
} else if !obj.ValidPos {
- // Built-in function, type, or nil: check not shadowed at caller.
- found := callerLookup(obj.Name, caller.Call.Pos()) // can't fail
+ // Built-in function, type, or value (e.g. nil, zero):
+ // check not shadowed at caller.
+ found := caller.lookup(obj.Name) // always finds something
if found.Pos().IsValid() {
return nil, fmt.Errorf("cannot inline because built-in %q is shadowed in caller by a %s (line %d)",
obj.Name, objectKind(found),
- caller.Fset.Position(found.Pos()).Line)
+ caller.Fset.PositionFor(found.Pos(), false).Line)
}
- newName = obj.Name
-
} else {
// Must be reference to package-level var/func/const/type,
// since type parameters are not yet supported.
- newName = obj.Name
qualify := false
if obj.PkgPath == callee.PkgPath {
// reference within callee package
if samePkg {
// Caller and callee are in same package.
// Check caller has not shadowed the decl.
- found := callerLookup(obj.Name, caller.Call.Pos()) // can't fail
+ found := caller.lookup(obj.Name) // can't fail
if !isPkgLevel(found) {
return nil, fmt.Errorf("cannot inline because %q is shadowed in caller by a %s (line %d)",
obj.Name, objectKind(found),
- caller.Fset.Position(found.Pos()).Line)
+ caller.Fset.PositionFor(found.Pos(), false).Line)
}
} else {
// Cross-package reference.
@@ -373,316 +506,2131 @@ func Inline(caller *Caller, callee_ *Callee) ([]byte, error) {
// Form a qualified identifier, pkg.Name.
if qualify {
- pkgName := localImportName(obj.PkgPath)
- newName = pkgName + "." + newName
+ pkgName := localImportName(obj.PkgPath, obj.Shadow)
+ newName = &ast.SelectorExpr{
+ X: makeIdent(pkgName),
+ Sel: makeIdent(obj.Name),
+ }
}
}
objRenames[i] = newName
}
- // Compute edits to inlined callee.
- type edit struct {
- start, end int // byte offsets wrt callee.content
- new string
- }
- var edits []edit
-
- // Give explicit blank "_" names to all method parameters
- // (including receiver) since we will make the receiver a regular
- // parameter and one cannot mix named and unnamed parameters.
- // e.g. func (T) f(int, string) -> (_ T, _ int, _ string)
- if callee.decl.Recv != nil {
- ensureNamed := func(params *ast.FieldList) {
- for _, param := range params.List {
- if param.Names == nil {
- offset := callee.offset(param.Type.Pos())
- edits = append(edits, edit{
- start: offset,
- end: offset,
- new: "_ ",
- })
- }
- }
- }
- ensureNamed(callee.decl.Recv)
- ensureNamed(callee.decl.Type.Params)
+ res := &result{
+ newImports: newImports,
+ }
+
+ // Parse callee function declaration.
+ calleeFset, calleeDecl, err := parseCompact(callee.Content)
+ if err != nil {
+ return nil, err // "can't happen"
+ }
+
+ // replaceCalleeID replaces an identifier in the callee.
+ // The replacement tree must not belong to the caller; use cloneNode as needed.
+ replaceCalleeID := func(offset int, repl ast.Expr) {
+ id := findIdent(calleeDecl, calleeDecl.Pos()+token.Pos(offset))
+ logf("- replace id %q @ #%d to %q", id.Name, offset, debugFormatNode(calleeFset, repl))
+ replaceNode(calleeDecl, id, repl)
}
// Generate replacements for each free identifier.
+ // (The same tree may be spliced in multiple times, resulting in a DAG.)
for _, ref := range callee.FreeRefs {
- if repl := objRenames[ref.Object]; repl != "" {
- edits = append(edits, edit{
- start: ref.Start,
- end: ref.End,
- new: repl,
- })
+ if repl := objRenames[ref.Object]; repl != nil {
+ replaceCalleeID(ref.Offset, repl)
}
}
- // Edits are non-overlapping but insertions and edits may be coincident.
- // Preserve original order.
- sort.SliceStable(edits, func(i, j int) bool {
- return edits[i].start < edits[j].start
- })
-
- // Check that all imports (in particular, the new ones) are accessible.
- // TODO(adonovan): allow customization of the accessibility relation (e.g. for Bazel).
- for path := range importMap {
- // TODO(adonovan): better segment hygiene.
- if i := strings.Index(path, "/internal/"); i >= 0 {
- if !strings.HasPrefix(caller.Types.Path(), path[:i]) {
- return nil, fmt.Errorf("can't inline function %v as its body refers to inaccessible package %q", callee.Name, path)
- }
- }
+ // Gather the effective call arguments, including the receiver.
+ // Later, elements will be eliminated (=> nil) by parameter substitution.
+ args, err := arguments(caller, calleeDecl, assign1)
+ if err != nil {
+ return nil, err // e.g. implicit field selection cannot be made explicit
}
- // The transformation is expressed by splicing substrings of
- // the two source files, because syntax trees don't preserve
- // comments faithfully (see #20744).
- var out bytes.Buffer
+ // Gather effective parameter tuple, including the receiver if any.
+ // Simplify variadic parameters to slices (in all cases but one).
+ var params []*parameter // including receiver; nil => parameter substituted
+ {
+ sig := calleeSymbol.Type().(*types.Signature)
+ if sig.Recv() != nil {
+ params = append(params, ¶meter{
+ obj: sig.Recv(),
+ fieldType: calleeDecl.Recv.List[0].Type,
+ info: callee.Params[0],
+ })
+ }
- // 'replace' emits to out the specified range of the callee,
- // applying all edits that fall completely within it.
- replace := func(start, end int) {
- off := start
- for _, edit := range edits {
- if start <= edit.start && edit.end <= end {
- out.Write(callee.Content[off:edit.start])
- out.WriteString(edit.new)
- off = edit.end
+ // Flatten the list of syntactic types.
+ var types []ast.Expr
+ for _, field := range calleeDecl.Type.Params.List {
+ if field.Names == nil {
+ types = append(types, field.Type)
+ } else {
+ for range field.Names {
+ types = append(types, field.Type)
+ }
}
}
- out.Write(callee.Content[off:end])
- }
- // Insert new imports after last existing import,
- // to avoid migration of pre-import comments.
- // The imports will be organized later.
- {
- offset := caller.offset(caller.File.Name.End()) // after package decl
- if len(caller.File.Imports) > 0 {
- // It's tempting to insert the new import after the last ImportSpec,
- // but that may not be at the end of the import decl.
- // Consider: import ( "a"; "b" ‸ )
- for _, decl := range caller.File.Decls {
- if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT {
- offset = caller.offset(decl.End()) // after import decl
+ for i := 0; i < sig.Params().Len(); i++ {
+ params = append(params, ¶meter{
+ obj: sig.Params().At(i),
+ fieldType: types[i],
+ info: callee.Params[len(params)],
+ })
+ }
+
+ // Variadic function?
+ //
+ // There are three possible types of call:
+ // - ordinary f(a1, ..., aN)
+ // - ellipsis f(a1, ..., slice...)
+ // - spread f(recv?, g()) where g() is a tuple.
+ // The first two are desugared to non-variadic calls
+ // with an ordinary slice parameter;
+ // the third is tricky and cannot be reduced, and (if
+ // a receiver is present) cannot even be literalized.
+ // Fortunately it is vanishingly rare.
+ //
+ // TODO(adonovan): extract this to a function.
+ if sig.Variadic() {
+ lastParam := last(params)
+ if len(args) > 0 && last(args).spread {
+ // spread call to variadic: tricky
+ lastParam.variadic = true
+ } else {
+ // ordinary/ellipsis call to variadic
+
+ // simplify decl: func(T...) -> func([]T)
+ lastParamField := last(calleeDecl.Type.Params.List)
+ lastParamField.Type = &ast.ArrayType{
+ Elt: lastParamField.Type.(*ast.Ellipsis).Elt,
+ }
+
+ if caller.Call.Ellipsis.IsValid() {
+ // ellipsis call: f(slice...) -> f(slice)
+ // nop
+ } else {
+ // ordinary call: f(a1, ... aN) -> f([]T{a1, ..., aN})
+ n := len(params) - 1
+ ordinary, extra := args[:n], args[n:]
+ var elts []ast.Expr
+ pure, effects := true, false
+ for _, arg := range extra {
+ elts = append(elts, arg.expr)
+ pure = pure && arg.pure
+ effects = effects || arg.effects
+ }
+ args = append(ordinary, &argument{
+ expr: &ast.CompositeLit{
+ Type: lastParamField.Type,
+ Elts: elts,
+ },
+ typ: lastParam.obj.Type(),
+ constant: nil,
+ pure: pure,
+ effects: effects,
+ duplicable: false,
+ freevars: nil, // not needed
+ })
}
}
}
- out.Write(caller.Content[:offset])
- out.WriteString("\n")
- for _, imp := range newImports {
- fmt.Fprintf(&out, "import %s\n", imp)
+ }
+
+ // Log effective arguments.
+ for i, arg := range args {
+ logf("arg #%d: %s pure=%t effects=%t duplicable=%t free=%v type=%v",
+ i, debugFormatNode(caller.Fset, arg.expr),
+ arg.pure, arg.effects, arg.duplicable, arg.freevars, arg.typ)
+ }
+
+ // Note: computation below should be expressed in terms of
+ // the args and params slices, not the raw material.
+
+ // Perform parameter substitution.
+ // May eliminate some elements of params/args.
+ substitute(logf, caller, params, args, callee.Effects, callee.Falcon, replaceCalleeID)
+
+ // Update the callee's signature syntax.
+ updateCalleeParams(calleeDecl, params)
+
+ // Create a var (param = arg; ...) decl for use by some strategies.
+ bindingDeclStmt := createBindingDecl(logf, caller, args, calleeDecl, callee.Results)
+
+ var remainingArgs []ast.Expr
+ for _, arg := range args {
+ if arg != nil {
+ remainingArgs = append(remainingArgs, arg.expr)
}
- out.Write(caller.Content[offset:caller.offset(caller.Call.Pos())])
}
- // Special case: a call to a function whose body consists only
- // of "return expr" may be replaced by the expression, so long as:
+ // -- let the inlining strategies begin --
//
- // (a) There are no receiver or parameter argument expressions
- // whose side effects must be considered.
- // (b) There are no named parameter or named result variables
- // that could potentially escape.
+ // When we commit to a strategy, we log a message of the form:
//
- // TODO(adonovan): expand this special case to cover more scenarios.
- // Consider each parameter in turn. If:
- // - the parameter does not escape and is never assigned;
- // - its argument is pure (no effects or panics--basically just idents and literals)
- // and referentially transparent (not new(T) or &T{...}) or referenced at most once; and
- // - the argument and parameter have the same type
- // then the parameter can be eliminated and each reference
- // to it replaced by the argument.
- // If:
- // - all parameters can be so replaced;
- // - and the body is just "return expr";
- // - and the result vars are unnamed or never referenced (and thus cannot escape);
- // then the call expression can be replaced by its body expression.
- if callee.BodyIsReturnExpr &&
- callee.decl.Recv == nil && // no receiver arg effects to consider
- len(caller.Call.Args) == 0 && // no argument effects to consider
- !hasNamedVars(callee.decl.Type.Params) && // no param vars escape
- !hasNamedVars(callee.decl.Type.Results) { // no result vars escape
-
- // A single return operand inlined to an expression
- // context may need parens. Otherwise:
- // func two() int { return 1+1 }
- // print(-two()) => print(-1+1) // oops!
- parens := callee.NumResults == 1
-
- // If the call is a standalone statement, but the
- // callee body is not suitable as a standalone statement
- // (f() or <-ch), explicitly discard the results:
- // _, _ = expr
- if isCallStmt(callerPath) {
- parens = false
-
- if !callee.ValidForCallStmt {
- for i := 0; i < callee.NumResults; i++ {
- if i > 0 {
- out.WriteString(", ")
+ // "strategy: reduce expr-context call to { return expr }"
+ //
+ // This is a terse way of saying:
+ //
+ // we plan to reduce a call
+ // that appears in expression context
+ // to a function whose body is of the form { return expr }
+
+ // TODO(adonovan): split this huge function into a sequence of
+ // function calls with an error sentinel that means "try the
+ // next strategy", and make sure each strategy writes to the
+ // log the reason it didn't match.
+
+ // Special case: eliminate a call to a function whose body is empty.
+ // (=> callee has no results and caller is a statement.)
+ //
+ // func f(params) {}
+ // f(args)
+ // => _, _ = args
+ //
+ if len(calleeDecl.Body.List) == 0 {
+ logf("strategy: reduce call to empty body")
+
+ // Evaluate the arguments for effects and delete the call entirely.
+ stmt := callStmt(caller.path, false) // cannot fail
+ res.old = stmt
+ if nargs := len(remainingArgs); nargs > 0 {
+ // Emit "_, _ = args" to discard results.
+
+ // TODO(adonovan): if args is the []T{a1, ..., an}
+ // literal synthesized during variadic simplification,
+ // consider unwrapping it to its (pure) elements.
+ // Perhaps there's no harm doing this for any slice literal.
+
+ // Make correction for spread calls
+ // f(g()) or recv.f(g()) where g() is a tuple.
+ if last := last(args); last != nil && last.spread {
+ nspread := last.typ.(*types.Tuple).Len()
+ if len(args) > 1 { // [recv, g()]
+ // A single AssignStmt cannot discard both, so use a 2-spec var decl.
+ res.new = &ast.GenDecl{
+ Tok: token.VAR,
+ Specs: []ast.Spec{
+ &ast.ValueSpec{
+ Names: []*ast.Ident{makeIdent("_")},
+ Values: []ast.Expr{args[0].expr},
+ },
+ &ast.ValueSpec{
+ Names: blanks[*ast.Ident](nspread),
+ Values: []ast.Expr{args[1].expr},
+ },
+ },
}
- out.WriteString("_")
+ return res, nil
}
- out.WriteString(" = ")
- }
- }
- // Emit the body expression(s).
- for i, res := range callee.decl.Body.List[0].(*ast.ReturnStmt).Results {
- if i > 0 {
- out.WriteString(", ")
+ // Sole argument is spread call.
+ nargs = nspread
}
- if parens {
- out.WriteString("(")
- }
- replace(callee.offset(res.Pos()), callee.offset(res.End()))
- if parens {
- out.WriteString(")")
+
+ res.new = &ast.AssignStmt{
+ Lhs: blanks[ast.Expr](nargs),
+ Tok: token.ASSIGN,
+ Rhs: remainingArgs,
}
- }
- goto rest
- }
- // Emit a function literal in place of the callee name,
- // with appropriate replacements.
- out.WriteString("func (")
- if recv := callee.decl.Recv; recv != nil {
- // Move method receiver to head of ordinary parameters.
- replace(callee.offset(recv.Opening+1), callee.offset(recv.Closing))
- if len(callee.decl.Type.Params.List) > 0 {
- out.WriteString(", ")
+ } else {
+ // No remaining arguments: delete call statement entirely
+ res.new = &ast.EmptyStmt{}
}
+ return res, nil
}
- replace(callee.offset(callee.decl.Type.Params.Opening+1),
- callee.offset(callee.decl.End()))
- // Emit call arguments.
- out.WriteString("(")
- if callee.decl.Recv != nil {
- // Move receiver argument x.f(...) to argument list f(x, ...).
- recv := astutil.Unparen(caller.Call.Fun).(*ast.SelectorExpr).X
+ // If all parameters have been substituted and no result
+ // variable is referenced, we don't need a binding decl.
+ // This may enable better reduction strategies.
+ allResultsUnreferenced := forall(callee.Results, func(i int, r *paramInfo) bool { return len(r.Refs) == 0 })
+ needBindingDecl := !allResultsUnreferenced ||
+ exists(params, func(i int, p *parameter) bool { return p != nil })
+
+ // Special case: call to { return exprs }.
+ //
+ // Reduces to:
+ // { var (bindings); _, _ = exprs }
+ // or _, _ = exprs
+ // or expr
+ //
+ // If:
+ // - the body is just "return expr" with trivial implicit conversions,
+ // or the caller's return type matches the callee's,
+ // - all parameters and result vars can be eliminated
+ // or replaced by a binding decl,
+ // then the call expression can be replaced by the
+ // callee's body expression, suitably substituted.
+ if len(calleeDecl.Body.List) == 1 &&
+ is[*ast.ReturnStmt](calleeDecl.Body.List[0]) &&
+ len(calleeDecl.Body.List[0].(*ast.ReturnStmt).Results) > 0 && // not a bare return
+ safeReturn(caller, calleeSymbol, callee) {
+ results := calleeDecl.Body.List[0].(*ast.ReturnStmt).Results
+
+ context := callContext(caller.path)
- // If the receiver argument and parameter have
- // different pointerness, make the "&" or "*" explicit.
- argPtr := is[*types.Pointer](typeparams.CoreType(caller.Info.TypeOf(recv)))
- paramPtr := is[*ast.StarExpr](callee.decl.Recv.List[0].Type)
- if !argPtr && paramPtr {
- out.WriteString("&")
- } else if argPtr && !paramPtr {
- out.WriteString("*")
+ // statement context
+ if stmt, ok := context.(*ast.ExprStmt); ok &&
+ (!needBindingDecl || bindingDeclStmt != nil) {
+ logf("strategy: reduce stmt-context call to { return exprs }")
+ clearPositions(calleeDecl.Body)
+
+ if callee.ValidForCallStmt {
+ logf("callee body is valid as statement")
+ // Inv: len(results) == 1
+ if !needBindingDecl {
+ // Reduces to: expr
+ res.old = caller.Call
+ res.new = results[0]
+ } else {
+ // Reduces to: { var (bindings); expr }
+ res.old = stmt
+ res.new = &ast.BlockStmt{
+ List: []ast.Stmt{
+ bindingDeclStmt,
+ &ast.ExprStmt{X: results[0]},
+ },
+ }
+ }
+ } else {
+ logf("callee body is not valid as statement")
+ // The call is a standalone statement, but the
+ // callee body is not suitable as a standalone statement
+ // (f() or <-ch), explicitly discard the results:
+ // Reduces to: _, _ = exprs
+ discard := &ast.AssignStmt{
+ Lhs: blanks[ast.Expr](callee.NumResults),
+ Tok: token.ASSIGN,
+ Rhs: results,
+ }
+ res.old = stmt
+ if !needBindingDecl {
+ // Reduces to: _, _ = exprs
+ res.new = discard
+ } else {
+ // Reduces to: { var (bindings); _, _ = exprs }
+ res.new = &ast.BlockStmt{
+ List: []ast.Stmt{
+ bindingDeclStmt,
+ discard,
+ },
+ }
+ }
+ }
+ return res, nil
}
- out.Write(caller.Content[caller.offset(recv.Pos()):caller.offset(recv.End())])
+ // expression context
+ if !needBindingDecl {
+ clearPositions(calleeDecl.Body)
+
+ if callee.NumResults == 1 {
+ logf("strategy: reduce expr-context call to { return expr }")
+
+ res.old = caller.Call
+ res.new = results[0]
+ } else {
+ logf("strategy: reduce spread-context call to { return expr }")
- if len(caller.Call.Args) > 0 {
- out.WriteString(", ")
+ // The call returns multiple results but is
+ // not a standalone call statement. It must
+ // be the RHS of a spread assignment:
+ // var x, y = f()
+ // x, y := f()
+ // x, y = f()
+ // or the sole argument to a spread call:
+ // printf(f())
+ res.old = context
+ switch context := context.(type) {
+ case *ast.AssignStmt:
+ // Inv: the call is in Rhs[0], not Lhs.
+ assign := shallowCopy(context)
+ assign.Rhs = results
+ res.new = assign
+ case *ast.ValueSpec:
+ // Inv: the call is in Values[0], not Names.
+ spec := shallowCopy(context)
+ spec.Values = results
+ res.new = spec
+ case *ast.CallExpr:
+ // Inv: the Call is Args[0], not Fun.
+ call := shallowCopy(context)
+ call.Args = results
+ res.new = call
+ default:
+ return nil, fmt.Errorf("internal error: unexpected context %T for spread call", context)
+ }
+ }
+ return res, nil
}
}
- // Append ordinary args, sans initial "(".
- out.Write(caller.Content[caller.offset(caller.Call.Lparen+1):caller.offset(caller.Call.End())])
-
- // Append rest of caller file.
-rest:
- out.Write(caller.Content[caller.offset(caller.Call.End()):])
- // Reformat, and organize imports.
- //
- // TODO(adonovan): this looks at the user's cache state.
- // Replace with a simpler implementation since
- // all the necessary imports are present but merely untidy.
- // That will be faster, and also less prone to nondeterminism
- // if there are bugs in our logic for import maintenance.
+ // Special case: tail-call.
//
- // However, golang.org/x/tools/internal/imports.ApplyFixes is
- // too simple as it requires the caller to have figured out
- // all the logical edits. In our case, we know all the new
- // imports that are needed (see newImports), each of which can
- // be specified as:
+ // Inlining:
+ // return f(args)
+ // where:
+ // func f(params) (results) { body }
+ // reduces to:
+ // { var (bindings); body }
+ // { body }
+ // so long as:
+ // - all parameters can be eliminated or replaced by a binding decl,
+ // - call is a tail-call;
+ // - all returns in body have trivial result conversions,
+ // or the caller's return type matches the callee's,
+ // - there is no label conflict;
+ // - no result variable is referenced by name,
+ // or implicitly by a bare return.
//
- // &imports.ImportFix{
- // StmtInfo: imports.ImportInfo{path, name,
- // IdentName: name,
- // FixType: imports.AddImport,
- // }
+ // The body may use defer, arbitrary control flow, and
+ // multiple returns.
//
- // but we don't know which imports are made redundant by the
- // inlining itself. For example, inlining a call to
- // fmt.Println may make the "fmt" import redundant.
+ // TODO(adonovan): omit the braces if the sets of
+ // names in the two blocks are disjoint.
//
- // Also, both imports.Process and internal/imports.ApplyFixes
- // reformat the entire file, which is not ideal for clients
- // such as gopls. (That said, the point of a canonical format
- // is arguably that any tool can reformat as needed without
- // this being inconvenient.)
- res, err := imports.Process("output", out.Bytes(), nil)
- if err != nil {
- if false { // debugging
- log.Printf("cannot reformat: %v <<%s>>", err, &out)
+ // TODO(adonovan): add a strategy for a 'void tail
+ // call', i.e. a call statement prior to an (explicit
+ // or implicit) return.
+ if ret, ok := callContext(caller.path).(*ast.ReturnStmt); ok &&
+ len(ret.Results) == 1 &&
+ safeReturn(caller, calleeSymbol, callee) &&
+ !callee.HasBareReturn &&
+ (!needBindingDecl || bindingDeclStmt != nil) &&
+ !hasLabelConflict(caller.path, callee.Labels) &&
+ allResultsUnreferenced {
+ logf("strategy: reduce tail-call")
+ body := calleeDecl.Body
+ clearPositions(body)
+ if needBindingDecl {
+ body.List = prepend(bindingDeclStmt, body.List...)
}
- return nil, err // cannot reformat (a bug?)
+ res.old = ret
+ res.new = body
+ return res, nil
}
- return res, nil
-}
-
-// -- helpers --
-
-func is[T any](x any) bool {
- _, ok := x.(T)
- return ok
-}
-func within(pos token.Pos, n ast.Node) bool {
- return n.Pos() <= pos && pos <= n.End()
-}
+ // Special case: call to void function
+ //
+ // Inlining:
+ // f(args)
+ // where:
+ // func f(params) { stmts }
+ // reduces to:
+ // { var (bindings); stmts }
+ // { stmts }
+ // so long as:
+ // - callee is a void function (no returns)
+ // - callee does not use defer
+ // - there is no label conflict between caller and callee
+ // - all parameters and result vars can be eliminated
+ // or replaced by a binding decl,
+ // - caller ExprStmt is in unrestricted statement context.
+ //
+ // If there is only a single statement, the braces are omitted.
+ if stmt := callStmt(caller.path, true); stmt != nil &&
+ (!needBindingDecl || bindingDeclStmt != nil) &&
+ !callee.HasDefer &&
+ !hasLabelConflict(caller.path, callee.Labels) &&
+ callee.TotalReturns == 0 {
+ logf("strategy: reduce stmt-context call to { stmts }")
+ body := calleeDecl.Body
+ var repl ast.Stmt = body
+ clearPositions(repl)
+ if needBindingDecl {
+ body.List = prepend(bindingDeclStmt, body.List...)
+ }
+ if len(body.List) == 1 { // FIXME do this opt later
+ repl = body.List[0] // singleton: omit braces
+ }
+ res.old = stmt
+ res.new = repl
+ return res, nil
+ }
-func offsetOf(fset *token.FileSet, pos token.Pos) int {
- return fset.PositionFor(pos, false).Offset
-}
+ // TODO(adonovan): parameterless call to { stmts; return expr }
+ // from one of these contexts:
+ // x, y = f()
+ // x, y := f()
+ // var x, y = f()
+ // =>
+ // var (x T1, y T2); { stmts; x, y = expr }
+ //
+ // Because the params are no longer declared simultaneously
+ // we need to check that (for example) x ∉ freevars(T2),
+ // in addition to the usual checks for arg/result conversions,
+ // complex control, etc.
+ // Also test cases where expr is an n-ary call (spread returns).
-// importedPkgName returns the PkgName object declared by an ImportSpec.
-// TODO(adonovan): make this a method of types.Info (#62037).
-func importedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) {
- var obj types.Object
- if imp.Name != nil {
- obj = info.Defs[imp.Name]
- } else {
- obj = info.Implicits[imp]
+ // Literalization isn't quite infallible.
+ // Consider a spread call to a method in which
+ // no parameters are eliminated, e.g.
+ // new(T).f(g())
+ // where
+ // func (recv *T) f(x, y int) { body }
+ // func g() (int, int)
+ // This would be literalized to:
+ // func (recv *T, x, y int) { body }(new(T), g()),
+ // which is not a valid argument list because g() must appear alone.
+ // Reject this case for now.
+ if len(args) == 2 && args[0] != nil && args[1] != nil && is[*types.Tuple](args[1].typ) {
+ return nil, fmt.Errorf("can't yet inline spread call to method")
}
- pkgname, ok := obj.(*types.PkgName)
- return pkgname, ok
-}
-func isPkgLevel(obj types.Object) bool {
- return obj.Pkg().Scope().Lookup(obj.Name()) == obj
-}
+ // Infallible general case: literalization.
+ logf("strategy: literalization")
-// objectKind returns an object's kind (e.g. var, func, const, typename).
-func objectKind(obj types.Object) string {
- return strings.TrimPrefix(strings.ToLower(reflect.TypeOf(obj).String()), "*types.")
+ // Emit a new call to a function literal in place of
+ // the callee name, with appropriate replacements.
+ newCall := &ast.CallExpr{
+ Fun: &ast.FuncLit{
+ Type: calleeDecl.Type,
+ Body: calleeDecl.Body,
+ },
+ Ellipsis: token.NoPos, // f(slice...) is always simplified
+ Args: remainingArgs,
+ }
+ clearPositions(newCall.Fun)
+ res.old = caller.Call
+ res.new = newCall
+ return res, nil
}
-// isCallStmt reports whether the function call (specified
-// as a PathEnclosingInterval) appears within an ExprStmt.
-func isCallStmt(callPath []ast.Node) bool {
- _ = callPath[0].(*ast.CallExpr)
- for _, n := range callPath[1:] {
- switch n.(type) {
- case *ast.ParenExpr:
- continue
- case *ast.ExprStmt:
- return true
- }
- break
- }
- return false
+type argument struct {
+ expr ast.Expr
+ typ types.Type // may be tuple for sole non-receiver arg in spread call
+ constant constant.Value // value of argument if constant
+ spread bool // final arg is call() assigned to multiple params
+ pure bool // expr is pure (doesn't read variables)
+ effects bool // expr has effects (updates variables)
+ duplicable bool // expr may be duplicated
+ freevars map[string]bool // free names of expr
+ substitutable bool // is candidate for substitution
}
-// hasNamedVars reports whether a function parameter tuple uses named variables.
+// arguments returns the effective arguments of the call.
+//
+// If the receiver argument and parameter have
+// different pointerness, make the "&" or "*" explicit.
//
-// TODO(adonovan): this is a placeholder for a more complex analysis to detect
-// whether inlining might cause named param/result variables to escape.
-func hasNamedVars(tuple *ast.FieldList) bool {
- return tuple != nil && len(tuple.List) > 0 && tuple.List[0].Names != nil
+// Also, if x.f() is shorthand for promoted method x.y.f(),
+// make the .y explicit in T.f(x.y, ...).
+//
+// Beware that:
+//
+// - a method can only be called through a selection, but only
+// the first of these two forms needs special treatment:
+//
+// expr.f(args) -> ([&*]expr, args) MethodVal
+// T.f(recv, args) -> ( expr, args) MethodExpr
+//
+// - the presence of a value in receiver-position in the call
+// is a property of the caller, not the callee. A method
+// (calleeDecl.Recv != nil) may be called like an ordinary
+// function.
+//
+// - the types.Signatures seen by the caller (from
+// StaticCallee) and by the callee (from decl type)
+// differ in this case.
+//
+// In a spread call f(g()), the sole ordinary argument g(),
+// always last in args, has a tuple type.
+//
+// We compute type-based predicates like pure, duplicable,
+// freevars, etc, now, before we start modifying syntax.
+func arguments(caller *Caller, calleeDecl *ast.FuncDecl, assign1 func(*types.Var) bool) ([]*argument, error) {
+ var args []*argument
+
+ callArgs := caller.Call.Args
+ if calleeDecl.Recv != nil {
+ sel := astutil.Unparen(caller.Call.Fun).(*ast.SelectorExpr)
+ seln := caller.Info.Selections[sel]
+ var recvArg ast.Expr
+ switch seln.Kind() {
+ case types.MethodVal: // recv.f(callArgs)
+ recvArg = sel.X
+ case types.MethodExpr: // T.f(recv, callArgs)
+ recvArg = callArgs[0]
+ callArgs = callArgs[1:]
+ }
+ if recvArg != nil {
+ // Compute all the type-based predicates now,
+ // before we start meddling with the syntax;
+ // the meddling will update them.
+ arg := &argument{
+ expr: recvArg,
+ typ: caller.Info.TypeOf(recvArg),
+ constant: caller.Info.Types[recvArg].Value,
+ pure: pure(caller.Info, assign1, recvArg),
+ effects: effects(caller.Info, recvArg),
+ duplicable: duplicable(caller.Info, recvArg),
+ freevars: freeVars(caller.Info, recvArg),
+ }
+ recvArg = nil // prevent accidental use
+
+ // Move receiver argument recv.f(args) to argument list f(&recv, args).
+ args = append(args, arg)
+
+ // Make field selections explicit (recv.f -> recv.y.f),
+ // updating arg.{expr,typ}.
+ indices := seln.Index()
+ for _, index := range indices[:len(indices)-1] {
+ t := deref(arg.typ)
+ fld := typeparams.CoreType(t).(*types.Struct).Field(index)
+ if fld.Pkg() != caller.Types && !fld.Exported() {
+ return nil, fmt.Errorf("in %s, implicit reference to unexported field .%s cannot be made explicit",
+ debugFormatNode(caller.Fset, caller.Call.Fun),
+ fld.Name())
+ }
+ if is[*types.Pointer](arg.typ.Underlying()) {
+ arg.pure = false // implicit *ptr operation => impure
+ }
+ arg.expr = &ast.SelectorExpr{
+ X: arg.expr,
+ Sel: makeIdent(fld.Name()),
+ }
+ arg.typ = fld.Type()
+ arg.duplicable = false
+ }
+
+ // Make * or & explicit.
+ argIsPtr := arg.typ != deref(arg.typ)
+ paramIsPtr := is[*types.Pointer](seln.Obj().Type().(*types.Signature).Recv().Type())
+ if !argIsPtr && paramIsPtr {
+ // &recv
+ arg.expr = &ast.UnaryExpr{Op: token.AND, X: arg.expr}
+ arg.typ = types.NewPointer(arg.typ)
+ } else if argIsPtr && !paramIsPtr {
+ // *recv
+ arg.expr = &ast.StarExpr{X: arg.expr}
+ arg.typ = deref(arg.typ)
+ arg.duplicable = false
+ arg.pure = false
+ }
+ }
+ }
+ for _, expr := range callArgs {
+ tv := caller.Info.Types[expr]
+ args = append(args, &argument{
+ expr: expr,
+ typ: tv.Type,
+ constant: tv.Value,
+ spread: is[*types.Tuple](tv.Type), // => last
+ pure: pure(caller.Info, assign1, expr),
+ effects: effects(caller.Info, expr),
+ duplicable: duplicable(caller.Info, expr),
+ freevars: freeVars(caller.Info, expr),
+ })
+ }
+
+ // Re-typecheck each constant argument expression in a neutral context.
+ //
+ // In a call such as func(int16){}(1), the type checker infers
+ // the type "int16", not "untyped int", for the argument 1,
+ // because it has incorporated information from the left-hand
+ // side of the assignment implicit in parameter passing, but
+ // of course in a different context, the expression 1 may have
+ // a different type.
+ //
+ // So, we must use CheckExpr to recompute the type of the
+ // argument in a neutral context to find its inherent type.
+ // (This is arguably a bug in go/types, but I'm pretty certain
+ // I requested it be this way long ago... -adonovan)
+ //
+ // This is only needed for constants. Other implicit
+ // assignment conversions, such as unnamed-to-named struct or
+ // chan to <-chan, do not result in the type-checker imposing
+ // the LHS type on the RHS value.
+ for _, arg := range args {
+ if arg.constant == nil {
+ continue
+ }
+ info := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
+ if err := types.CheckExpr(caller.Fset, caller.Types, caller.Call.Pos(), arg.expr, info); err != nil {
+ return nil, err
+ }
+ arg.typ = info.TypeOf(arg.expr)
+ }
+
+ return args, nil
+}
+
+type parameter struct {
+ obj *types.Var // parameter var from caller's signature
+ fieldType ast.Expr // syntax of type, from calleeDecl.Type.{Recv,Params}
+ info *paramInfo // information from AnalyzeCallee
+ variadic bool // (final) parameter is unsimplified ...T
+}
+
+// substitute implements parameter elimination by substitution.
+//
+// It considers each parameter and its corresponding argument in turn
+// and evaluate these conditions:
+//
+// - the parameter is neither address-taken nor assigned;
+// - the argument is pure;
+// - if the parameter refcount is zero, the argument must
+// not contain the last use of a local var;
+// - if the parameter refcount is > 1, the argument must be duplicable;
+// - the argument (or types.Default(argument) if it's untyped) has
+// the same type as the parameter.
+//
+// If all conditions are met then the parameter can be substituted and
+// each reference to it replaced by the argument. In that case, the
+// replaceCalleeID function is called for each reference to the
+// parameter, and is provided with its relative offset and replacement
+// expression (argument), and the corresponding elements of params and
+// args are replaced by nil.
+func substitute(logf func(string, ...any), caller *Caller, params []*parameter, args []*argument, effects []int, falcon falconResult, replaceCalleeID func(offset int, repl ast.Expr)) {
+ // Inv:
+ // in calls to variadic, len(args) >= len(params)-1
+ // in spread calls to non-variadic, len(args) < len(params)
+ // in spread calls to variadic, len(args) <= len(params)
+ // (In spread calls len(args) = 1, or 2 if call has receiver.)
+ // Non-spread variadics have been simplified away already,
+ // so the args[i] lookup is safe if we stop after the spread arg.
+next:
+ for i, param := range params {
+ arg := args[i]
+ // Check argument against parameter.
+ //
+ // Beware: don't use types.Info on arg since
+ // the syntax may be synthetic (not created by parser)
+ // and thus lacking positions and types;
+ // do it earlier (see pure/duplicable/freevars).
+
+ if arg.spread {
+ // spread => last argument, but not always last parameter
+ logf("keeping param %q and following ones: argument %s is spread",
+ param.info.Name, debugFormatNode(caller.Fset, arg.expr))
+ return // give up
+ }
+ assert(!param.variadic, "unsimplified variadic parameter")
+ if param.info.Escapes {
+ logf("keeping param %q: escapes from callee", param.info.Name)
+ continue
+ }
+ if param.info.Assigned {
+ logf("keeping param %q: assigned by callee", param.info.Name)
+ continue // callee needs the parameter variable
+ }
+ if len(param.info.Refs) > 1 && !arg.duplicable {
+ logf("keeping param %q: argument is not duplicable", param.info.Name)
+ continue // incorrect or poor style to duplicate an expression
+ }
+ if len(param.info.Refs) == 0 {
+ if arg.effects {
+ logf("keeping param %q: though unreferenced, it has effects", param.info.Name)
+ continue
+ }
+
+ // If the caller is within a function body,
+ // eliminating an unreferenced parameter might
+ // remove the last reference to a caller local var.
+ if caller.enclosingFunc != nil {
+ for free := range arg.freevars {
+ if v, ok := caller.lookup(free).(*types.Var); ok && within(v.Pos(), caller.enclosingFunc.Body) {
+ // TODO(adonovan): be more precise and check that v
+ // is indeed referenced only by call arguments.
+ // Better: proceed, but blank out its declaration as needed.
+ logf("keeping param %q: arg contains perhaps the last reference to possible caller local %v @ %v",
+ param.info.Name, v, caller.Fset.PositionFor(v.Pos(), false))
+ continue next
+ }
+ }
+ }
+ }
+
+ // Check for shadowing.
+ //
+ // Consider inlining a call f(z, 1) to
+ // func f(x, y int) int { z := y; return x + y + z }:
+ // we can't replace x in the body by z (or any
+ // expression that has z as a free identifier)
+ // because there's an intervening declaration of z
+ // that would shadow the caller's one.
+ for free := range arg.freevars {
+ if param.info.Shadow[free] {
+ logf("keeping param %q: cannot replace with argument as it has free ref to %s that is shadowed", param.info.Name, free)
+ continue next // shadowing conflict
+ }
+ }
+
+ arg.substitutable = true // may be substituted, if effects permit
+ }
+
+ // Reject constant arguments as substitution candidates
+ // if they cause violation of falcon constraints.
+ checkFalconConstraints(logf, params, args, falcon)
+
+ // As a final step, introduce bindings to resolve any
+ // evaluation order hazards. This must be done last, as
+ // additional subsequent bindings could introduce new hazards.
+ resolveEffects(logf, args, effects)
+
+ // The remaining candidates are safe to substitute.
+ for i, param := range params {
+ if arg := args[i]; arg.substitutable {
+
+ // Wrap the argument in an explicit conversion if
+ // substitution might materially change its type.
+ // (We already did the necessary shadowing check
+ // on the parameter type syntax.)
+ //
+ // This is only needed for substituted arguments. All
+ // other arguments are given explicit types in either
+ // a binding decl or when using the literalization
+ // strategy.
+ if len(param.info.Refs) > 0 && !trivialConversion(args[i].typ, params[i].obj) {
+ arg.expr = convert(params[i].fieldType, arg.expr)
+ logf("param %q: adding explicit %s -> %s conversion around argument",
+ param.info.Name, args[i].typ, params[i].obj.Type())
+ }
+
+ // It is safe to substitute param and replace it with arg.
+ // The formatter introduces parens as needed for precedence.
+ //
+ // Because arg.expr belongs to the caller,
+ // we clone it before splicing it into the callee tree.
+ logf("replacing parameter %q by argument %q",
+ param.info.Name, debugFormatNode(caller.Fset, arg.expr))
+ for _, ref := range param.info.Refs {
+ replaceCalleeID(ref, cloneNode(arg.expr).(ast.Expr))
+ }
+ params[i] = nil // substituted
+ args[i] = nil // substituted
+ }
+ }
+}
+
+// checkFalconConstraints checks whether constant arguments
+// are safe to substitute (e.g. s[i] -> ""[0] is not safe.)
+//
+// Any failed constraint causes us to reject all constant arguments as
+// substitution candidates (by clearing args[i].substitution=false).
+//
+// TODO(adonovan): we could obtain a finer result rejecting only the
+// freevars of each failed constraint, and processing constraints in
+// order of increasing arity, but failures are quite rare.
+func checkFalconConstraints(logf func(string, ...any), params []*parameter, args []*argument, falcon falconResult) {
+ // Create a dummy package, as this is the only
+ // way to create an environment for CheckExpr.
+ pkg := types.NewPackage("falcon", "falcon")
+
+ // Declare types used by constraints.
+ for _, typ := range falcon.Types {
+ logf("falcon env: type %s %s", typ.Name, types.Typ[typ.Kind])
+ pkg.Scope().Insert(types.NewTypeName(token.NoPos, pkg, typ.Name, types.Typ[typ.Kind]))
+ }
+
+ // Declared constants and variables for for parameters.
+ nconst := 0
+ for i, param := range params {
+ name := param.info.Name
+ if name == "" {
+ continue // unreferenced
+ }
+ arg := args[i]
+ if arg.constant != nil && arg.substitutable && param.info.FalconType != "" {
+ t := pkg.Scope().Lookup(param.info.FalconType).Type()
+ pkg.Scope().Insert(types.NewConst(token.NoPos, pkg, name, t, arg.constant))
+ logf("falcon env: const %s %s = %v", name, param.info.FalconType, arg.constant)
+ nconst++
+ } else {
+ pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, name, arg.typ))
+ logf("falcon env: var %s %s", name, arg.typ)
+ }
+ }
+ if nconst == 0 {
+ return // nothing to do
+ }
+
+ // Parse and evaluate the constraints in the environment.
+ fset := token.NewFileSet()
+ for _, falcon := range falcon.Constraints {
+ expr, err := parser.ParseExprFrom(fset, "falcon", falcon, 0)
+ if err != nil {
+ panic(fmt.Sprintf("failed to parse falcon constraint %s: %v", falcon, err))
+ }
+ if err := types.CheckExpr(fset, pkg, token.NoPos, expr, nil); err != nil {
+ logf("falcon: constraint %s violated: %v", falcon, err)
+ for j, arg := range args {
+ if arg.constant != nil && arg.substitutable {
+ logf("keeping param %q due falcon violation", params[j].info.Name)
+ arg.substitutable = false
+ }
+ }
+ break
+ }
+ logf("falcon: constraint %s satisfied", falcon)
+ }
+}
+
+// resolveEffects marks arguments as non-substitutable to resolve
+// hazards resulting from the callee evaluation order described by the
+// effects list.
+//
+// To do this, each argument is categorized as a read (R), write (W),
+// or pure. A hazard occurs when the order of evaluation of a W
+// changes with respect to any R or W. Pure arguments can be
+// effectively ignored, as they can be safely evaluated in any order.
+//
+// The callee effects list contains the index of each parameter in the
+// order it is first evaluated during execution of the callee. In
+// addition, the two special values R∞ and W∞ indicate the relative
+// position of the callee's first non-parameter read and its first
+// effects (or other unknown behavior).
+// For example, the list [0 2 1 R∞ 3 W∞] for func(a, b, c, d)
+// indicates that the callee referenced parameters a, c, and b,
+// followed by an arbitrary read, then parameter d, and finally
+// unknown behavior.
+//
+// When an argument is marked as not substitutable, we say that it is
+// 'bound', in the sense that its evaluation occurs in a binding decl
+// or literalized call. Such bindings always occur in the original
+// callee parameter order.
+//
+// In this context, "resolving hazards" means binding arguments so
+// that they are evaluated in a valid, hazard-free order. A trivial
+// solution to this problem would be to bind all arguments, but of
+// course that's not useful. The goal is to bind as few arguments as
+// possible.
+//
+// The algorithm proceeds by inspecting arguments in reverse parameter
+// order (right to left), preserving the invariant that every
+// higher-ordered argument is either already substituted or does not
+// need to be substituted. At each iteration, if there is an
+// evaluation hazard in the callee effects relative to the current
+// argument, the argument must be bound. Subsequently, if the argument
+// is bound for any reason, each lower-ordered argument must also be
+// bound if either the argument or lower-order argument is a
+// W---otherwise the binding itself would introduce a hazard.
+//
+// Thus, after each iteration, there are no hazards relative to the
+// current argument. Subsequent iterations cannot introduce hazards
+// with that argument because they can result only in additional
+// binding of lower-ordered arguments.
+func resolveEffects(logf func(string, ...any), args []*argument, effects []int) {
+ effectStr := func(effects bool, idx int) string {
+ i := fmt.Sprint(idx)
+ if idx == len(args) {
+ i = "∞"
+ }
+ return string("RW"[btoi(effects)]) + i
+ }
+ for i := len(args) - 1; i >= 0; i-- {
+ argi := args[i]
+ if argi.substitutable && !argi.pure {
+ // i is not bound: check whether it must be bound due to hazards.
+ idx := index(effects, i)
+ if idx >= 0 {
+ for _, j := range effects[:idx] {
+ var (
+ ji int // effective param index
+ jw bool // j is a write
+ )
+ if j == winf || j == rinf {
+ jw = j == winf
+ ji = len(args)
+ } else {
+ jw = args[j].effects
+ ji = j
+ }
+ if ji > i && (jw || argi.effects) { // out of order evaluation
+ logf("binding argument %s: preceded by %s",
+ effectStr(argi.effects, i), effectStr(jw, ji))
+ argi.substitutable = false
+ break
+ }
+ }
+ }
+ }
+ if !argi.substitutable {
+ for j := 0; j < i; j++ {
+ argj := args[j]
+ if argj.pure {
+ continue
+ }
+ if (argi.effects || argj.effects) && argj.substitutable {
+ logf("binding argument %s: %s is bound",
+ effectStr(argj.effects, j), effectStr(argi.effects, i))
+ argj.substitutable = false
+ }
+ }
+ }
+ }
+}
+
+// updateCalleeParams updates the calleeDecl syntax to remove
+// substituted parameters and move the receiver (if any) to the head
+// of the ordinary parameters.
+func updateCalleeParams(calleeDecl *ast.FuncDecl, params []*parameter) {
+ // The logic is fiddly because of the three forms of ast.Field:
+ //
+ // func(int), func(x int), func(x, y int)
+ //
+ // Also, ensure that all remaining parameters are named
+ // to avoid a mix of named/unnamed when joining (recv, params...).
+ // func (T) f(int, bool) -> (_ T, _ int, _ bool)
+ // (Strictly, we need do this only for methods and only when
+ // the namednesses of Recv and Params differ; that might be tidier.)
+
+ paramIdx := 0 // index in original parameter list (incl. receiver)
+ var newParams []*ast.Field
+ filterParams := func(field *ast.Field) {
+ var names []*ast.Ident
+ if field.Names == nil {
+ // Unnamed parameter field (e.g. func f(int)
+ if params[paramIdx] != nil {
+ // Give it an explicit name "_" since we will
+ // make the receiver (if any) a regular parameter
+ // and one cannot mix named and unnamed parameters.
+ names = append(names, makeIdent("_"))
+ }
+ paramIdx++
+ } else {
+ // Named parameter field e.g. func f(x, y int)
+ // Remove substituted parameters in place.
+ // If all were substituted, delete field.
+ for _, id := range field.Names {
+ if pinfo := params[paramIdx]; pinfo != nil {
+ // Rename unreferenced parameters with "_".
+ // This is crucial for binding decls, since
+ // unlike parameters, they are subject to
+ // "unreferenced var" checks.
+ if len(pinfo.info.Refs) == 0 {
+ id = makeIdent("_")
+ }
+ names = append(names, id)
+ }
+ paramIdx++
+ }
+ }
+ if names != nil {
+ newParams = append(newParams, &ast.Field{
+ Names: names,
+ Type: field.Type,
+ })
+ }
+ }
+ if calleeDecl.Recv != nil {
+ filterParams(calleeDecl.Recv.List[0])
+ calleeDecl.Recv = nil
+ }
+ for _, field := range calleeDecl.Type.Params.List {
+ filterParams(field)
+ }
+ calleeDecl.Type.Params.List = newParams
+}
+
+// createBindingDecl constructs a "binding decl" that implements
+// parameter assignment and declares any named result variables
+// referenced by the callee.
+//
+// It may not always be possible to create the decl (e.g. due to
+// shadowing), in which case it returns nil; but if it succeeds, the
+// declaration may be used by reduction strategies to relax the
+// requirement that all parameters have been substituted.
+//
+// For example, a call:
+//
+// f(a0, a1, a2)
+//
+// where:
+//
+// func f(p0, p1 T0, p2 T1) { body }
+//
+// reduces to:
+//
+// {
+// var (
+// p0, p1 T0 = a0, a1
+// p2 T1 = a2
+// )
+// body
+// }
+//
+// so long as p0, p1 ∉ freevars(T1) or freevars(a2), and so on,
+// because each spec is statically resolved in sequence and
+// dynamically assigned in sequence. By contrast, all
+// parameters are resolved simultaneously and assigned
+// simultaneously.
+//
+// The pX names should already be blank ("_") if the parameter
+// is unreferenced; this avoids "unreferenced local var" checks.
+//
+// Strategies may impose additional checks on return
+// conversions, labels, defer, etc.
+func createBindingDecl(logf func(string, ...any), caller *Caller, args []*argument, calleeDecl *ast.FuncDecl, results []*paramInfo) ast.Stmt {
+ // Spread calls are tricky as they may not align with the
+ // parameters' field groupings nor types.
+ // For example, given
+ // func g() (int, string)
+ // the call
+ // f(g())
+ // is legal with these decls of f:
+ // func f(int, string)
+ // func f(x, y any)
+ // func f(x, y ...any)
+ // TODO(adonovan): support binding decls for spread calls by
+ // splitting parameter groupings as needed.
+ if lastArg := last(args); lastArg != nil && lastArg.spread {
+ logf("binding decls not yet supported for spread calls")
+ return nil
+ }
+
+ var (
+ specs []ast.Spec
+ shadowed = make(map[string]bool) // names defined by previous specs
+ )
+ // shadow reports whether any name referenced by spec is
+ // shadowed by a name declared by a previous spec (since,
+ // unlike parameters, each spec of a var decl is within the
+ // scope of the previous specs).
+ shadow := func(spec *ast.ValueSpec) bool {
+ // Compute union of free names of type and values
+ // and detect shadowing. Values is the arguments
+ // (caller syntax), so we can use type info.
+ // But Type is the untyped callee syntax,
+ // so we have to use a syntax-only algorithm.
+ free := make(map[string]bool)
+ for _, value := range spec.Values {
+ for name := range freeVars(caller.Info, value) {
+ free[name] = true
+ }
+ }
+ freeishNames(free, spec.Type)
+ for name := range free {
+ if shadowed[name] {
+ logf("binding decl would shadow free name %q", name)
+ return true
+ }
+ }
+ for _, id := range spec.Names {
+ if id.Name != "_" {
+ shadowed[id.Name] = true
+ }
+ }
+ return false
+ }
+
+ // parameters
+ //
+ // Bind parameters that were not eliminated through
+ // substitution. (Non-nil arguments correspond to the
+ // remaining parameters in calleeDecl.)
+ var values []ast.Expr
+ for _, arg := range args {
+ if arg != nil {
+ values = append(values, arg.expr)
+ }
+ }
+ for _, field := range calleeDecl.Type.Params.List {
+ // Each field (param group) becomes a ValueSpec.
+ spec := &ast.ValueSpec{
+ Names: field.Names,
+ Type: field.Type,
+ Values: values[:len(field.Names)],
+ }
+ values = values[len(field.Names):]
+ if shadow(spec) {
+ return nil
+ }
+ specs = append(specs, spec)
+ }
+ assert(len(values) == 0, "args/params mismatch")
+
+ // results
+ //
+ // Add specs to declare any named result
+ // variables that are referenced by the body.
+ if calleeDecl.Type.Results != nil {
+ resultIdx := 0
+ for _, field := range calleeDecl.Type.Results.List {
+ if field.Names == nil {
+ resultIdx++
+ continue // unnamed field
+ }
+ var names []*ast.Ident
+ for _, id := range field.Names {
+ if len(results[resultIdx].Refs) > 0 {
+ names = append(names, id)
+ }
+ resultIdx++
+ }
+ if len(names) > 0 {
+ spec := &ast.ValueSpec{
+ Names: names,
+ Type: field.Type,
+ }
+ if shadow(spec) {
+ return nil
+ }
+ specs = append(specs, spec)
+ }
+ }
+ }
+
+ decl := &ast.DeclStmt{
+ Decl: &ast.GenDecl{
+ Tok: token.VAR,
+ Specs: specs,
+ },
+ }
+ logf("binding decl: %s", debugFormatNode(caller.Fset, decl))
+ return decl
+}
+
+// lookup does a symbol lookup in the lexical environment of the caller.
+func (caller *Caller) lookup(name string) types.Object {
+ pos := caller.Call.Pos()
+ for _, n := range caller.path {
+ if scope := scopeFor(caller.Info, n); scope != nil {
+ if _, obj := scope.LookupParent(name, pos); obj != nil {
+ return obj
+ }
+ }
+ }
+ return nil
+}
+
+func scopeFor(info *types.Info, n ast.Node) *types.Scope {
+ // The function body scope (containing not just params)
+ // is associated with the function's type, not body.
+ switch fn := n.(type) {
+ case *ast.FuncDecl:
+ n = fn.Type
+ case *ast.FuncLit:
+ n = fn.Type
+ }
+ return info.Scopes[n]
+}
+
+// -- predicates over expressions --
+
+// freeVars returns the names of all free identifiers of e:
+// those lexically referenced by it but not defined within it.
+// (Fields and methods are not included.)
+func freeVars(info *types.Info, e ast.Expr) map[string]bool {
+ free := make(map[string]bool)
+ ast.Inspect(e, func(n ast.Node) bool {
+ if id, ok := n.(*ast.Ident); ok {
+ // The isField check is so that we don't treat T{f: 0} as a ref to f.
+ if obj, ok := info.Uses[id]; ok && !within(obj.Pos(), e) && !isField(obj) {
+ free[obj.Name()] = true
+ }
+ }
+ return true
+ })
+ return free
+}
+
+// freeishNames computes an over-approximation to the free names
+// of the type syntax t, inserting values into the map.
+//
+// Because we don't have go/types annotations, we can't give an exact
+// result in all cases. In particular, an array type [n]T might have a
+// size such as unsafe.Sizeof(func() int{stmts...}()) and now the
+// precise answer depends upon all the statement syntax too. But that
+// never happens in practice.
+func freeishNames(free map[string]bool, t ast.Expr) {
+ var visit func(n ast.Node) bool
+ visit = func(n ast.Node) bool {
+ switch n := n.(type) {
+ case *ast.Ident:
+ free[n.Name] = true
+
+ case *ast.SelectorExpr:
+ ast.Inspect(n.X, visit)
+ return false // don't visit .Sel
+
+ case *ast.Field:
+ ast.Inspect(n.Type, visit)
+ // Don't visit .Names:
+ // FuncType parameters, interface methods, struct fields
+ return false
+ }
+ return true
+ }
+ ast.Inspect(t, visit)
+}
+
+// effects reports whether an expression might change the state of the
+// program (through function calls and channel receives) and affect
+// the evaluation of subsequent expressions.
+func effects(info *types.Info, expr ast.Expr) bool {
+ effects := false
+ ast.Inspect(expr, func(n ast.Node) bool {
+ switch n := n.(type) {
+ case *ast.FuncLit:
+ return false // prune descent
+
+ case *ast.CallExpr:
+ if info.Types[n.Fun].IsType() {
+ // A conversion T(x) has only the effect of its operand.
+ } else if !callsPureBuiltin(info, n) {
+ // A handful of built-ins have no effect
+ // beyond those of their arguments.
+ // All other calls (including append, copy, recover)
+ // have unknown effects.
+ //
+ // As with 'pure', there is room for
+ // improvement by inspecting the callee.
+ effects = true
+ }
+
+ case *ast.UnaryExpr:
+ if n.Op == token.ARROW { // <-ch
+ effects = true
+ }
+ }
+ return true
+ })
+ return effects
+}
+
+// pure reports whether an expression has the same result no matter
+// when it is executed relative to other expressions, so it can be
+// commuted with any other expression or statement without changing
+// its meaning.
+//
+// An expression is considered impure if it reads the contents of any
+// variable, with the exception of "single assignment" local variables
+// (as classified by the provided callback), which are never updated
+// after their initialization.
+//
+// Pure does not imply duplicable: for example, new(T) and T{} are
+// pure expressions but both return a different value each time they
+// are evaluated, so they are not safe to duplicate.
+//
+// Purity does not imply freedom from run-time panics. We assume that
+// target programs do not encounter run-time panics nor depend on them
+// for correct operation.
+//
+// TODO(adonovan): add unit tests of this function.
+func pure(info *types.Info, assign1 func(*types.Var) bool, e ast.Expr) bool {
+ var pure func(e ast.Expr) bool
+ pure = func(e ast.Expr) bool {
+ switch e := e.(type) {
+ case *ast.ParenExpr:
+ return pure(e.X)
+
+ case *ast.Ident:
+ if v, ok := info.Uses[e].(*types.Var); ok {
+ // In general variables are impure
+ // as they may be updated, but
+ // single-assignment local variables
+ // never change value.
+ //
+ // We assume all package-level variables
+ // may be updated, but for non-exported
+ // ones we could do better by analyzing
+ // the complete package.
+ return !isPkgLevel(v) && assign1(v)
+ }
+
+ // All other kinds of reference are pure.
+ return true
+
+ case *ast.FuncLit:
+ // A function literal may allocate a closure that
+ // references mutable variables, but mutation
+ // cannot be observed without calling the function,
+ // and calls are considered impure.
+ return true
+
+ case *ast.BasicLit:
+ return true
+
+ case *ast.UnaryExpr: // + - ! ^ & but not <-
+ return e.Op != token.ARROW && pure(e.X)
+
+ case *ast.BinaryExpr: // arithmetic, shifts, comparisons, &&/||
+ return pure(e.X) && pure(e.Y)
+
+ case *ast.CallExpr:
+ // A conversion is as pure as its operand.
+ if info.Types[e.Fun].IsType() {
+ return pure(e.Args[0])
+ }
+
+ // Calls to some built-ins are as pure as their arguments.
+ if callsPureBuiltin(info, e) {
+ for _, arg := range e.Args {
+ if !pure(arg) {
+ return false
+ }
+ }
+ return true
+ }
+
+ // All other calls are impure, so we can
+ // reject them without even looking at e.Fun.
+ //
+ // More sophisticated analysis could infer purity in
+ // commonly used functions such as strings.Contains;
+ // perhaps we could offer the client a hook so that
+ // go/analysis-based implementation could exploit the
+ // results of a purity analysis. But that would make
+ // the inliner's choices harder to explain.
+ return false
+
+ case *ast.CompositeLit:
+ // T{...} is as pure as its elements.
+ for _, elt := range e.Elts {
+ if kv, ok := elt.(*ast.KeyValueExpr); ok {
+ if !pure(kv.Value) {
+ return false
+ }
+ if id, ok := kv.Key.(*ast.Ident); ok {
+ if v, ok := info.Uses[id].(*types.Var); ok && v.IsField() {
+ continue // struct {field: value}
+ }
+ }
+ // map/slice/array {key: value}
+ if !pure(kv.Key) {
+ return false
+ }
+
+ } else if !pure(elt) {
+ return false
+ }
+ }
+ return true
+
+ case *ast.SelectorExpr:
+ if sel, ok := info.Selections[e]; ok {
+ switch sel.Kind() {
+ case types.MethodExpr:
+ // A method expression T.f acts like a
+ // reference to a func decl, so it is pure.
+ return true
+
+ case types.MethodVal:
+ // A method value x.f acts like a
+ // closure around a T.f(x, ...) call,
+ // so it is as pure as x.
+ return pure(e.X)
+
+ case types.FieldVal:
+ // A field selection x.f is pure if
+ // x is pure and the selection does
+ // not indirect a pointer.
+ return !sel.Indirect() && pure(e.X)
+
+ default:
+ panic(sel)
+ }
+ } else {
+ // A qualified identifier is
+ // treated like an unqualified one.
+ return pure(e.Sel)
+ }
+
+ case *ast.StarExpr:
+ return false // *ptr depends on the state of the heap
+
+ default:
+ return false
+ }
+ }
+ return pure(e)
+}
+
+// callsPureBuiltin reports whether call is a call of a built-in
+// function that is a pure computation over its operands (analogous to
+// a + operator). Because it does not depend on program state, it may
+// be evaluated at any point--though not necessarily at multiple
+// points (consider new, make).
+func callsPureBuiltin(info *types.Info, call *ast.CallExpr) bool {
+ if id, ok := astutil.Unparen(call.Fun).(*ast.Ident); ok {
+ if b, ok := info.ObjectOf(id).(*types.Builtin); ok {
+ switch b.Name() {
+ case "len", "cap", "complex", "imag", "real", "make", "new", "max", "min":
+ return true
+ }
+ // Not: append clear close copy delete panic print println recover
+ }
+ }
+ return false
+}
+
+// duplicable reports whether it is appropriate for the expression to
+// be freely duplicated.
+//
+// Given the declaration
+//
+// func f(x T) T { return x + g() + x }
+//
+// an argument y is considered duplicable if we would wish to see a
+// call f(y) simplified to y+g()+y. This is true for identifiers,
+// integer literals, unary negation, and selectors x.f where x is not
+// a pointer. But we would not wish to duplicate expressions that:
+// - have side effects (e.g. nearly all calls),
+// - are not referentially transparent (e.g. &T{}, ptr.field), or
+// - are long (e.g. "huge string literal").
+func duplicable(info *types.Info, e ast.Expr) bool {
+ switch e := e.(type) {
+ case *ast.ParenExpr:
+ return duplicable(info, e.X)
+
+ case *ast.Ident:
+ return true
+
+ case *ast.BasicLit:
+ v := info.Types[e].Value
+ switch e.Kind {
+ case token.INT:
+ return true // any int
+ case token.STRING:
+ return consteq(v, kZeroString) // only ""
+ case token.FLOAT:
+ return consteq(v, kZeroFloat) || consteq(v, kOneFloat) // only 0.0 or 1.0
+ }
+
+ case *ast.UnaryExpr: // e.g. +1, -1
+ return (e.Op == token.ADD || e.Op == token.SUB) && duplicable(info, e.X)
+
+ case *ast.CallExpr:
+ // Don't treat a conversion T(x) as duplicable even
+ // if x is duplicable because it could duplicate
+ // allocations. There may be cases to tease apart here.
+ return false
+
+ case *ast.SelectorExpr:
+ if sel, ok := info.Selections[e]; ok {
+ // A field or method selection x.f is referentially
+ // transparent if it does not indirect a pointer.
+ return !sel.Indirect()
+ }
+ // A qualified identifier pkg.Name is referentially transparent.
+ return true
+ }
+ return false
+}
+
+func consteq(x, y constant.Value) bool {
+ return constant.Compare(x, token.EQL, y)
+}
+
+var (
+ kZeroInt = constant.MakeInt64(0)
+ kZeroString = constant.MakeString("")
+ kZeroFloat = constant.MakeFloat64(0.0)
+ kOneFloat = constant.MakeFloat64(1.0)
+)
+
+// -- inline helpers --
+
+func assert(cond bool, msg string) {
+ if !cond {
+ panic(msg)
+ }
+}
+
+// blanks returns a slice of n > 0 blank identifiers.
+func blanks[E ast.Expr](n int) []E {
+ if n == 0 {
+ panic("blanks(0)")
+ }
+ res := make([]E, n)
+ for i := range res {
+ res[i] = ast.Expr(makeIdent("_")).(E) // ugh
+ }
+ return res
+}
+
+func makeIdent(name string) *ast.Ident {
+ return &ast.Ident{Name: name}
+}
+
+// importedPkgName returns the PkgName object declared by an ImportSpec.
+// TODO(adonovan): make this a method of types.Info (#62037).
+func importedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) {
+ var obj types.Object
+ if imp.Name != nil {
+ obj = info.Defs[imp.Name]
+ } else {
+ obj = info.Implicits[imp]
+ }
+ pkgname, ok := obj.(*types.PkgName)
+ return pkgname, ok
+}
+
+func isPkgLevel(obj types.Object) bool {
+ // TODO(adonovan): consider using the simpler obj.Parent() ==
+ // obj.Pkg().Scope() instead. But be sure to test carefully
+ // with instantiations of generics.
+ return obj.Pkg().Scope().Lookup(obj.Name()) == obj
+}
+
+// callContext returns the node immediately enclosing the call
+// (specified as a PathEnclosingInterval), ignoring parens.
+func callContext(callPath []ast.Node) ast.Node {
+ _ = callPath[0].(*ast.CallExpr) // sanity check
+ for _, n := range callPath[1:] {
+ if !is[*ast.ParenExpr](n) {
+ return n
+ }
+ }
+ return nil
+}
+
+// hasLabelConflict reports whether the set of labels of the function
+// enclosing the call (specified as a PathEnclosingInterval)
+// intersects with the set of callee labels.
+func hasLabelConflict(callPath []ast.Node, calleeLabels []string) bool {
+ labels := callerLabels(callPath)
+ for _, label := range calleeLabels {
+ if labels[label] {
+ return true // conflict
+ }
+ }
+ return false
+}
+
+// callerLabels returns the set of control labels in the function (if
+// any) enclosing the call (specified as a PathEnclosingInterval).
+func callerLabels(callPath []ast.Node) map[string]bool {
+ var callerBody *ast.BlockStmt
+ switch f := callerFunc(callPath).(type) {
+ case *ast.FuncDecl:
+ callerBody = f.Body
+ case *ast.FuncLit:
+ callerBody = f.Body
+ }
+ var labels map[string]bool
+ if callerBody != nil {
+ ast.Inspect(callerBody, func(n ast.Node) bool {
+ switch n := n.(type) {
+ case *ast.FuncLit:
+ return false // prune traversal
+ case *ast.LabeledStmt:
+ if labels == nil {
+ labels = make(map[string]bool)
+ }
+ labels[n.Label.Name] = true
+ }
+ return true
+ })
+ }
+ return labels
+}
+
+// callerFunc returns the innermost Func{Decl,Lit} node enclosing the
+// call (specified as a PathEnclosingInterval).
+func callerFunc(callPath []ast.Node) ast.Node {
+ _ = callPath[0].(*ast.CallExpr) // sanity check
+ for _, n := range callPath[1:] {
+ if is[*ast.FuncDecl](n) || is[*ast.FuncLit](n) {
+ return n
+ }
+ }
+ return nil
+}
+
+// callStmt reports whether the function call (specified
+// as a PathEnclosingInterval) appears within an ExprStmt,
+// and returns it if so.
+//
+// If unrestricted, callStmt returns nil if the ExprStmt f() appears
+// in a restricted context (such as "if f(); cond {") where it cannot
+// be replaced by an arbitrary statement. (See "statement theory".)
+func callStmt(callPath []ast.Node, unrestricted bool) *ast.ExprStmt {
+ stmt, ok := callContext(callPath).(*ast.ExprStmt)
+ if ok && unrestricted {
+ switch callPath[nodeIndex(callPath, stmt)+1].(type) {
+ case *ast.LabeledStmt,
+ *ast.BlockStmt,
+ *ast.CaseClause,
+ *ast.CommClause:
+ // unrestricted
+ default:
+ // TODO(adonovan): handle restricted
+ // XYZStmt.Init contexts (but not ForStmt.Post)
+ // by creating a block around the if/for/switch:
+ // "if f(); cond {" -> "{ stmts; if cond {"
+
+ return nil // restricted
+ }
+ }
+ return stmt
+}
+
+// Statement theory
+//
+// These are all the places a statement may appear in the AST:
+//
+// LabeledStmt.Stmt Stmt -- any
+// BlockStmt.List []Stmt -- any (but see switch/select)
+// IfStmt.Init Stmt? -- simple
+// IfStmt.Body BlockStmt
+// IfStmt.Else Stmt? -- IfStmt or BlockStmt
+// CaseClause.Body []Stmt -- any
+// SwitchStmt.Init Stmt? -- simple
+// SwitchStmt.Body BlockStmt -- CaseClauses only
+// TypeSwitchStmt.Init Stmt? -- simple
+// TypeSwitchStmt.Assign Stmt -- AssignStmt(TypeAssertExpr) or ExprStmt(TypeAssertExpr)
+// TypeSwitchStmt.Body BlockStmt -- CaseClauses only
+// CommClause.Comm Stmt? -- SendStmt or ExprStmt(UnaryExpr) or AssignStmt(UnaryExpr)
+// CommClause.Body []Stmt -- any
+// SelectStmt.Body BlockStmt -- CommClauses only
+// ForStmt.Init Stmt? -- simple
+// ForStmt.Post Stmt? -- simple
+// ForStmt.Body BlockStmt
+// RangeStmt.Body BlockStmt
+//
+// simple = AssignStmt | SendStmt | IncDecStmt | ExprStmt.
+//
+// A BlockStmt cannot replace an ExprStmt in
+// {If,Switch,TypeSwitch}Stmt.Init or ForStmt.Post.
+// That is allowed only within:
+// LabeledStmt.Stmt Stmt
+// BlockStmt.List []Stmt
+// CaseClause.Body []Stmt
+// CommClause.Body []Stmt
+
+// replaceNode performs a destructive update of the tree rooted at
+// root, replacing each occurrence of "from" with "to". If to is nil and
+// the element is within a slice, the slice element is removed.
+//
+// The root itself cannot be replaced; an attempt will panic.
+//
+// This function must not be called on the caller's syntax tree.
+//
+// TODO(adonovan): polish this up and move it to astutil package.
+// TODO(adonovan): needs a unit test.
+func replaceNode(root ast.Node, from, to ast.Node) {
+ if from == nil {
+ panic("from == nil")
+ }
+ if reflect.ValueOf(from).IsNil() {
+ panic(fmt.Sprintf("from == (%T)(nil)", from))
+ }
+ if from == root {
+ panic("from == root")
+ }
+ found := false
+ var parent reflect.Value // parent variable of interface type, containing a pointer
+ var visit func(reflect.Value)
+ visit = func(v reflect.Value) {
+ switch v.Kind() {
+ case reflect.Ptr:
+ if v.Interface() == from {
+ found = true
+
+ // If v is a struct field or array element
+ // (e.g. Field.Comment or Field.Names[i])
+ // then it is addressable (a pointer variable).
+ //
+ // But if it was the value an interface
+ // (e.g. *ast.Ident within ast.Node)
+ // then it is non-addressable, and we need
+ // to set the enclosing interface (parent).
+ if !v.CanAddr() {
+ v = parent
+ }
+
+ // to=nil => use zero value
+ var toV reflect.Value
+ if to != nil {
+ toV = reflect.ValueOf(to)
+ } else {
+ toV = reflect.Zero(v.Type()) // e.g. ast.Expr(nil)
+ }
+ v.Set(toV)
+
+ } else if !v.IsNil() {
+ switch v.Interface().(type) {
+ case *ast.Object, *ast.Scope:
+ // Skip fields of types potentially involved in cycles.
+ default:
+ visit(v.Elem())
+ }
+ }
+
+ case reflect.Struct:
+ for i := 0; i < v.Type().NumField(); i++ {
+ visit(v.Field(i))
+ }
+
+ case reflect.Slice:
+ compact := false
+ for i := 0; i < v.Len(); i++ {
+ visit(v.Index(i))
+ if v.Index(i).IsNil() {
+ compact = true
+ }
+ }
+ if compact {
+ // Elements were deleted. Eliminate nils.
+ // (Do this is a second pass to avoid
+ // unnecessary writes in the common case.)
+ j := 0
+ for i := 0; i < v.Len(); i++ {
+ if !v.Index(i).IsNil() {
+ v.Index(j).Set(v.Index(i))
+ j++
+ }
+ }
+ v.SetLen(j)
+ }
+ case reflect.Interface:
+ parent = v
+ visit(v.Elem())
+
+ case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer:
+ panic(v) // unreachable in AST
+ default:
+ // bool, string, number: nop
+ }
+ parent = reflect.Value{}
+ }
+ visit(reflect.ValueOf(root))
+ if !found {
+ panic(fmt.Sprintf("%T not found", from))
+ }
+}
+
+// cloneNode returns a deep copy of a Node.
+// It omits pointers to ast.{Scope,Object} variables.
+func cloneNode(n ast.Node) ast.Node {
+ var clone func(x reflect.Value) reflect.Value
+ set := func(dst, src reflect.Value) {
+ src = clone(src)
+ if src.IsValid() {
+ dst.Set(src)
+ }
+ }
+ clone = func(x reflect.Value) reflect.Value {
+ switch x.Kind() {
+ case reflect.Ptr:
+ if x.IsNil() {
+ return x
+ }
+ // Skip fields of types potentially involved in cycles.
+ switch x.Interface().(type) {
+ case *ast.Object, *ast.Scope:
+ return reflect.Zero(x.Type())
+ }
+ y := reflect.New(x.Type().Elem())
+ set(y.Elem(), x.Elem())
+ return y
+
+ case reflect.Struct:
+ y := reflect.New(x.Type()).Elem()
+ for i := 0; i < x.Type().NumField(); i++ {
+ set(y.Field(i), x.Field(i))
+ }
+ return y
+
+ case reflect.Slice:
+ y := reflect.MakeSlice(x.Type(), x.Len(), x.Cap())
+ for i := 0; i < x.Len(); i++ {
+ set(y.Index(i), x.Index(i))
+ }
+ return y
+
+ case reflect.Interface:
+ y := reflect.New(x.Type()).Elem()
+ set(y, x.Elem())
+ return y
+
+ case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer:
+ panic(x) // unreachable in AST
+
+ default:
+ return x // bool, string, number
+ }
+ }
+ return clone(reflect.ValueOf(n)).Interface().(ast.Node)
+}
+
+// clearPositions destroys token.Pos information within the tree rooted at root,
+// as positions in callee trees may cause caller comments to be emitted prematurely.
+//
+// In general it isn't safe to clear a valid Pos because some of them
+// (e.g. CallExpr.Ellipsis, TypeSpec.Assign) are significant to
+// go/printer, so this function sets each non-zero Pos to 1, which
+// suffices to avoid advancing the printer's comment cursor.
+//
+// This function mutates its argument; do not invoke on caller syntax.
+//
+// TODO(adonovan): remove this horrendous workaround when #20744 is finally fixed.
+func clearPositions(root ast.Node) {
+ posType := reflect.TypeOf(token.NoPos)
+ ast.Inspect(root, func(n ast.Node) bool {
+ if n != nil {
+ v := reflect.ValueOf(n).Elem() // deref the pointer to struct
+ fields := v.Type().NumField()
+ for i := 0; i < fields; i++ {
+ f := v.Field(i)
+ if f.Type() == posType {
+ // Clearing Pos arbitrarily is destructive,
+ // as its presence may be semantically significant
+ // (e.g. CallExpr.Ellipsis, TypeSpec.Assign)
+ // or affect formatting preferences (e.g. GenDecl.Lparen).
+ if f.Interface() != token.NoPos {
+ f.Set(reflect.ValueOf(token.Pos(1)))
+ }
+ }
+ }
+ }
+ return true
+ })
+}
+
+// findIdent returns the Ident beneath root that has the given pos.
+func findIdent(root ast.Node, pos token.Pos) *ast.Ident {
+ // TODO(adonovan): opt: skip subtrees that don't contain pos.
+ var found *ast.Ident
+ ast.Inspect(root, func(n ast.Node) bool {
+ if found != nil {
+ return false
+ }
+ if id, ok := n.(*ast.Ident); ok {
+ if id.Pos() == pos {
+ found = id
+ }
+ }
+ return true
+ })
+ if found == nil {
+ panic(fmt.Sprintf("findIdent %d not found in %s",
+ pos, debugFormatNode(token.NewFileSet(), root)))
+ }
+ return found
+}
+
+func prepend[T any](elem T, slice ...T) []T {
+ return append([]T{elem}, slice...)
+}
+
+// debugFormatNode formats a node or returns a formatting error.
+// Its sloppy treatment of errors is appropriate only for logging.
+func debugFormatNode(fset *token.FileSet, n ast.Node) string {
+ var out strings.Builder
+ if err := format.Node(&out, fset, n); err != nil {
+ out.WriteString(err.Error())
+ }
+ return out.String()
+}
+
+func shallowCopy[T any](ptr *T) *T {
+ copy := *ptr
+ return ©
+}
+
+// ∀
+func forall[T any](list []T, f func(i int, x T) bool) bool {
+ for i, x := range list {
+ if !f(i, x) {
+ return false
+ }
+ }
+ return true
+}
+
+// ∃
+func exists[T any](list []T, f func(i int, x T) bool) bool {
+ for i, x := range list {
+ if f(i, x) {
+ return true
+ }
+ }
+ return false
+}
+
+// last returns the last element of a slice, or zero if empty.
+func last[T any](slice []T) T {
+ n := len(slice)
+ if n > 0 {
+ return slice[n-1]
+ }
+ return *new(T)
+}
+
+// canImport reports whether one package is allowed to import another.
+//
+// TODO(adonovan): allow customization of the accessibility relation
+// (e.g. for Bazel).
+func canImport(from, to string) bool {
+ // TODO(adonovan): better segment hygiene.
+ if strings.HasPrefix(to, "internal/") {
+ // Special case: only std packages may import internal/...
+ // We can't reliably know whether we're in std, so we
+ // use a heuristic on the first segment.
+ first, _, _ := strings.Cut(from, "/")
+ if strings.Contains(first, ".") {
+ return false // example.com/foo ∉ std
+ }
+ if first == "testdata" {
+ return false // testdata/foo ∉ std
+ }
+ }
+ if i := strings.LastIndex(to, "/internal/"); i >= 0 {
+ return strings.HasPrefix(from, to[:i])
+ }
+ return true
+}
+
+// consistentOffsets reports whether the portion of caller.Content
+// that corresponds to caller.Call can be parsed as a call expression.
+// If not, the client has provided inconsistent information, possibly
+// because they forgot to ignore line directives when computing the
+// filename enclosing the call.
+// This is just a heuristic.
+func consistentOffsets(caller *Caller) bool {
+ start := offsetOf(caller.Fset, caller.Call.Pos())
+ end := offsetOf(caller.Fset, caller.Call.End())
+ if !(0 < start && start < end && end <= len(caller.Content)) {
+ return false
+ }
+ expr, err := parser.ParseExpr(string(caller.Content[start:end]))
+ if err != nil {
+ return false
+ }
+ return is[*ast.CallExpr](expr)
+}
+
+// needsParens reports whether parens are required to avoid ambiguity
+// around the new node replacing the specified old node (which is some
+// ancestor of the CallExpr identified by its PathEnclosingInterval).
+func needsParens(callPath []ast.Node, old, new ast.Node) bool {
+ // Find enclosing old node and its parent.
+ i := nodeIndex(callPath, old)
+ if i == -1 {
+ panic("not found")
+ }
+
+ // There is no precedence ambiguity when replacing
+ // (e.g.) a statement enclosing the call.
+ if !is[ast.Expr](old) {
+ return false
+ }
+
+ // An expression beneath a non-expression
+ // has no precedence ambiguity.
+ parent, ok := callPath[i+1].(ast.Expr)
+ if !ok {
+ return false
+ }
+
+ precedence := func(n ast.Node) int {
+ switch n := n.(type) {
+ case *ast.UnaryExpr, *ast.StarExpr:
+ return token.UnaryPrec
+ case *ast.BinaryExpr:
+ return n.Op.Precedence()
+ }
+ return -1
+ }
+
+ // Parens are not required if the new node
+ // is not unary or binary.
+ newprec := precedence(new)
+ if newprec < 0 {
+ return false
+ }
+
+ // Parens are required if parent and child are both
+ // unary or binary and the parent has higher precedence.
+ if precedence(parent) > newprec {
+ return true
+ }
+
+ // Was the old node the operand of a postfix operator?
+ // f().sel
+ // f()[i:j]
+ // f()[i]
+ // f().(T)
+ // f()(x)
+ switch parent := parent.(type) {
+ case *ast.SelectorExpr:
+ return parent.X == old
+ case *ast.IndexExpr:
+ return parent.X == old
+ case *ast.SliceExpr:
+ return parent.X == old
+ case *ast.TypeAssertExpr:
+ return parent.X == old
+ case *ast.CallExpr:
+ return parent.Fun == old
+ }
+ return false
+}
+
+func nodeIndex(nodes []ast.Node, n ast.Node) int {
+ // TODO(adonovan): Use index[ast.Node]() in go1.20.
+ for i, node := range nodes {
+ if node == n {
+ return i
+ }
+ }
+ return -1
+}
+
+// declares returns the set of lexical names declared by a
+// sequence of statements from the same block, excluding sub-blocks.
+// (Lexical names do not include control labels.)
+func declares(stmts []ast.Stmt) map[string]bool {
+ names := make(map[string]bool)
+ for _, stmt := range stmts {
+ switch stmt := stmt.(type) {
+ case *ast.DeclStmt:
+ for _, spec := range stmt.Decl.(*ast.GenDecl).Specs {
+ switch spec := spec.(type) {
+ case *ast.ValueSpec:
+ for _, id := range spec.Names {
+ names[id.Name] = true
+ }
+ case *ast.TypeSpec:
+ names[spec.Name.Name] = true
+ }
+ }
+
+ case *ast.AssignStmt:
+ if stmt.Tok == token.DEFINE {
+ for _, lhs := range stmt.Lhs {
+ names[lhs.(*ast.Ident).Name] = true
+ }
+ }
+ }
+ }
+ return names
+}
+
+// safeReturn reports whether the callee's return statements may be safely
+// used to return from the function enclosing the caller (which must exist).
+func safeReturn(caller *Caller, calleeSymbol *types.Func, callee *gobCallee) bool {
+ // It is safe if all callee returns involve only trivial conversions.
+ if callee.TrivialReturns == callee.TotalReturns {
+ return true
+ }
+
+ var callerType types.Type
+ // Find type of innermost function enclosing call.
+ // (Beware: Caller.enclosingFunc is the outermost.)
+loop:
+ for _, n := range caller.path {
+ switch f := n.(type) {
+ case *ast.FuncDecl:
+ callerType = caller.Info.ObjectOf(f.Name).Type()
+ break loop
+ case *ast.FuncLit:
+ callerType = caller.Info.TypeOf(f)
+ break loop
+ }
+ }
+
+ // Non-trivial return conversions in the callee are permitted
+ // if the same non-trivial conversion would occur after inlining,
+ // i.e. if the caller and callee results tuples are identical.
+ callerResults := callerType.(*types.Signature).Results()
+ calleeResults := calleeSymbol.Type().(*types.Signature).Results()
+ return types.Identical(callerResults, calleeResults)
}
diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go
index f77d2851f17..8362e445b66 100644
--- a/internal/refactor/inline/inline_test.go
+++ b/internal/refactor/inline/inline_test.go
@@ -6,14 +6,21 @@ package inline_test
import (
"bytes"
+ "crypto/sha256"
+ "encoding/binary"
"encoding/gob"
"fmt"
"go/ast"
+ "go/parser"
"go/token"
+ "go/types"
"os"
"path/filepath"
+ "reflect"
"regexp"
+ "strings"
"testing"
+ "unsafe"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/expect"
@@ -25,8 +32,8 @@ import (
"golang.org/x/tools/txtar"
)
-// Test executes test scenarios specified by files in testdata/*.txtar.
-func Test(t *testing.T) {
+// TestData executes test scenarios specified by files in testdata/*.txtar.
+func TestData(t *testing.T) {
testenv.NeedsGoPackages(t)
files, err := filepath.Glob("testdata/*.txtar")
@@ -38,6 +45,12 @@ func Test(t *testing.T) {
t.Run(filepath.Base(file), func(t *testing.T) {
t.Parallel()
+ // The few tests that use cgo should be in
+ // files whose name includes "cgo".
+ if strings.Contains(t.Name(), "cgo") {
+ testenv.NeedsTool(t, "cgo")
+ }
+
// Extract archive to temporary tree.
ar, err := txtar.ParseFile(file)
if err != nil {
@@ -86,7 +99,7 @@ func Test(t *testing.T) {
continue
}
for _, note := range notes {
- posn := pkg.Fset.Position(note.Pos)
+ posn := pkg.Fset.PositionFor(note.Pos, false)
if note.Name != "inline" {
t.Errorf("%s: invalid marker @%s", posn, note.Name)
continue
@@ -121,8 +134,7 @@ func Test(t *testing.T) {
t.Errorf("%s: @inline(rx, want): want file name (to assert success) or error message regexp (to assert failure)", posn)
continue
}
- t.Log("doInlineNote", posn)
- if err := doInlineNote(pkg, file, content, pattern, posn, want); err != nil {
+ if err := doInlineNote(t.Logf, pkg, file, content, pattern, posn, want); err != nil {
t.Errorf("%s: @inline(%v, %v): %v", posn, note.Args[0], note.Args[1], err)
continue
}
@@ -141,7 +153,7 @@ func Test(t *testing.T) {
// Finally it checks that, on success, the transformed file is equal
// to want (a []byte), or on failure that the error message matches
// want (a *Regexp).
-func doInlineNote(pkg *packages.Package, file *ast.File, content []byte, pattern *regexp.Regexp, posn token.Position, want any) error {
+func doInlineNote(logf func(string, ...any), pkg *packages.Package, file *ast.File, content []byte, pattern *regexp.Regexp, posn token.Position, want any) error {
// Find extent of pattern match within commented line.
var startPos, endPos token.Pos
{
@@ -197,18 +209,11 @@ func doInlineNote(pkg *packages.Package, file *ast.File, content []byte, pattern
}
// Find callee function.
- var (
- calleePkg *packages.Package
- calleeDecl *ast.FuncDecl
- )
+ var calleePkg *packages.Package
{
- var same func(*ast.FuncDecl) bool
// Is the call within the package?
if fn.Pkg() == caller.Types {
calleePkg = pkg // same as caller
- same = func(decl *ast.FuncDecl) bool {
- return decl.Name.Pos() == fn.Pos()
- }
} else {
// Different package. Load it now.
// (The primary load loaded all dependencies,
@@ -229,31 +234,12 @@ func doInlineNote(pkg *packages.Package, file *ast.File, content []byte, pattern
return fmt.Errorf("callee package had errors") // (see log)
}
calleePkg = roots[0]
- posn := caller.Fset.Position(fn.Pos()) // callee posn wrt caller package
- same = func(decl *ast.FuncDecl) bool {
- // We can't rely on columns in export data:
- // some variants replace it with 1.
- // We can't expect file names to have the same prefix.
- // export data for go1.20 std packages have $GOROOT written in
- // them, so how are we supposed to find the source? Yuck!
- // Ugh. need to samefile? Nope $GOROOT just won't work
- // This is highly client specific anyway.
- posn2 := calleePkg.Fset.Position(decl.Name.Pos())
- return posn.Filename == posn2.Filename &&
- posn.Line == posn2.Line
- }
}
+ }
- for _, file := range calleePkg.Syntax {
- for _, decl := range file.Decls {
- if decl, ok := decl.(*ast.FuncDecl); ok && same(decl) {
- calleeDecl = decl
- goto found
- }
- }
- }
- return fmt.Errorf("can't find FuncDecl for callee") // can't happen?
- found:
+ calleeDecl, err := findFuncByPosition(calleePkg, caller.Fset.PositionFor(fn.Pos(), false))
+ if err != nil {
+ return err
}
// Do the inlining. For the purposes of the test,
@@ -265,6 +251,7 @@ func doInlineNote(pkg *packages.Package, file *ast.File, content []byte, pattern
return nil, err
}
callee, err := inline.AnalyzeCallee(
+ logf,
calleePkg.Fset,
calleePkg.Types,
calleePkg.TypesInfo,
@@ -274,17 +261,13 @@ func doInlineNote(pkg *packages.Package, file *ast.File, content []byte, pattern
return nil, err
}
- // Perform Gob transcoding so that it is exercised by the test.
- var enc bytes.Buffer
- if err := gob.NewEncoder(&enc).Encode(callee); err != nil {
- return nil, fmt.Errorf("internal error: gob encoding failed: %v", err)
- }
- *callee = inline.Callee{}
- if err := gob.NewDecoder(&enc).Decode(callee); err != nil {
- return nil, fmt.Errorf("internal error: gob decoding failed: %v", err)
+ if err := checkTranscode(callee); err != nil {
+ return nil, err
}
- return inline.Inline(caller, callee)
+ check := checkNoMutation(caller.File)
+ defer check()
+ return inline.Inline(logf, caller, callee)
}()
if err != nil {
if wantRE, ok := want.(*regexp.Regexp); ok {
@@ -310,6 +293,929 @@ func doInlineNote(pkg *packages.Package, file *ast.File, content []byte, pattern
}
+// findFuncByPosition returns the FuncDecl at the specified (package-agnostic) position.
+func findFuncByPosition(pkg *packages.Package, posn token.Position) (*ast.FuncDecl, error) {
+ same := func(decl *ast.FuncDecl) bool {
+ // We can't rely on columns in export data:
+ // some variants replace it with 1.
+ // We can't expect file names to have the same prefix.
+ // export data for go1.20 std packages have $GOROOT written in
+ // them, so how are we supposed to find the source? Yuck!
+ // Ugh. need to samefile? Nope $GOROOT just won't work
+ // This is highly client specific anyway.
+ posn2 := pkg.Fset.PositionFor(decl.Name.Pos(), false)
+ return posn.Filename == posn2.Filename &&
+ posn.Line == posn2.Line
+ }
+ for _, file := range pkg.Syntax {
+ for _, decl := range file.Decls {
+ if decl, ok := decl.(*ast.FuncDecl); ok && same(decl) {
+ return decl, nil
+ }
+ }
+ }
+ return nil, fmt.Errorf("can't find FuncDecl at %v in package %q", posn, pkg.PkgPath)
+}
+
+// Each callee must declare a function or method named f,
+// and each caller must call it.
+const funcName = "f"
+
+// A testcase is an item in a table-driven test.
+//
+// The table-driven tests are less flexible, but enable more compact
+// expression of single-package test cases than is possible with the
+// txtar notation.
+//
+// TODO(adonovan): improve coverage of the cross product of each
+// strategy with the checklist of concerns enumerated in the package
+// doc comment.
+type testcase struct {
+ descr string
+ callee, caller string // Go source files (sans package decl) of caller, callee
+ want string // expected new portion of caller file, or "error: regexp"
+}
+
+func TestErrors(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Generic functions are not yet supported.",
+ `func f[T any](x T) T { return x }`,
+ `var _ = f(0)`,
+ `error: type parameters are not yet supported`,
+ },
+ {
+ "Methods on generic types are not yet supported.",
+ `type G[T any] struct{}; func (G[T]) f(x T) T { return x }`,
+ `var _ = G[int]{}.f(0)`,
+ `error: type parameters are not yet supported`,
+ },
+ })
+}
+
+func TestBasics(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Basic",
+ `func f(x int) int { return x }`,
+ `var _ = f(0)`,
+ `var _ = 0`,
+ },
+ {
+ "Empty body, no arg effects.",
+ `func f(x, y int) {}`,
+ `func _() { f(1, 2) }`,
+ `func _() {}`,
+ },
+ {
+ "Empty body, some arg effects.",
+ `func f(x, y, z int) {}`,
+ `func _() { f(1, recover().(int), 3) }`,
+ `func _() { _ = recover().(int) }`,
+ },
+ {
+ "Non-duplicable arguments are not substituted even if pure.",
+ `func f(s string, i int) { print(s, s, i, i) }`,
+ `func _() { f("hi", 0) }`,
+ `func _() {
+ var s string = "hi"
+ print(s, s, 0, 0)
+}`,
+ },
+ {
+ "Workaround for T(x) misformatting (#63362).",
+ `func f(ch <-chan int) { <-ch }`,
+ `func _(ch chan int) { f(ch) }`,
+ `func _(ch chan int) { <-(<-chan int)(ch) }`,
+ },
+ })
+}
+
+func TestExprStmtReduction(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "A call in an unrestricted ExprStmt may be replaced by the body stmts.",
+ `func f() { var _ = len("") }`,
+ `func _() { f() }`,
+ `func _() { var _ = len("") }`,
+ },
+ {
+ "ExprStmts in the body of a switch case are unrestricted.",
+ `func f() { x := 1; print(x) }`,
+ `func _() { switch { case true: f() } }`,
+ `func _() {
+ switch {
+ case true:
+ x := 1
+ print(x)
+ }
+}`,
+ },
+ {
+ "ExprStmts in the body of a select case are unrestricted.",
+ `func f() { x := 1; print(x) }`,
+ `func _() { select { default: f() } }`,
+ `func _() {
+ select {
+ default:
+ x := 1
+ print(x)
+ }
+}`,
+ },
+ {
+ "Some ExprStmt contexts are restricted to simple statements.",
+ `func f() { var _ = len("") }`,
+ `func _(cond bool) { if f(); cond {} }`,
+ `func _(cond bool) {
+ if func() { var _ = len("") }(); cond {
+ }
+}`,
+ },
+ {
+ "Braces must be preserved to avoid a name conflict (decl before).",
+ `func f() { x := 1; print(x) }`,
+ `func _() { x := 2; print(x); f() }`,
+ `func _() {
+ x := 2
+ print(x)
+ {
+ x := 1
+ print(x)
+ }
+}`,
+ },
+ {
+ "Braces must be preserved to avoid a name conflict (decl after).",
+ `func f() { x := 1; print(x) }`,
+ `func _() { f(); x := 2; print(x) }`,
+ `func _() {
+ {
+ x := 1
+ print(x)
+ }
+ x := 2
+ print(x)
+}`,
+ },
+ {
+ "Braces must be preserved to avoid a forward jump across a decl.",
+ `func f() { x := 1; print(x) }`,
+ `func _() { goto label; f(); label: }`,
+ `func _() {
+ goto label
+ {
+ x := 1
+ print(x)
+ }
+label:
+}`,
+ },
+ })
+}
+
+func TestPrecedenceParens(t *testing.T) {
+ // Ensure that parens are inserted when (and only when) necessary
+ // around the replacement for the call expression. (This is a special
+ // case in the way the inliner uses a combination of AST formatting
+ // for the call and text splicing for the rest of the file.)
+ runTests(t, []testcase{
+ {
+ "Multiplication in addition context (no parens).",
+ `func f(x, y int) int { return x * y }`,
+ `func _() { _ = 1 + f(2, 3) }`,
+ `func _() { _ = 1 + 2*3 }`,
+ },
+ {
+ "Addition in multiplication context (parens).",
+ `func f(x, y int) int { return x + y }`,
+ `func _() { _ = 1 * f(2, 3) }`,
+ `func _() { _ = 1 * (2 + 3) }`,
+ },
+ {
+ "Addition in negation context (parens).",
+ `func f(x, y int) int { return x + y }`,
+ `func _() { _ = -f(1, 2) }`,
+ `func _() { _ = -(1 + 2) }`,
+ },
+ {
+ "Addition in call context (no parens).",
+ `func f(x, y int) int { return x + y }`,
+ `func _() { println(f(1, 2)) }`,
+ `func _() { println(1 + 2) }`,
+ },
+ {
+ "Addition in slice operand context (parens).",
+ `func f(x, y string) string { return x + y }`,
+ `func _() { _ = f("x", "y")[1:2] }`,
+ `func _() { _ = ("x" + "y")[1:2] }`,
+ },
+ {
+ "String literal in slice operand context (no parens).",
+ `func f(x string) string { return x }`,
+ `func _() { _ = f("xy")[1:2] }`,
+ `func _() { _ = "xy"[1:2] }`,
+ },
+ })
+}
+
+func TestSubstitution(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Arg to unref'd param can be eliminated if has no effects.",
+ `func f(x, y int) {}; var global int`,
+ `func _() { f(0, global) }`,
+ `func _() {}`,
+ },
+ {
+ "But not if it may contain last reference to a caller local var.",
+ `func f(int) {}`,
+ `func _() { var local int; f(local) }`,
+ `func _() { var local int; _ = local }`,
+ },
+ {
+ "Regression test for detection of shadowing in nested functions.",
+ `func f(x int) { _ = func() { y := 1; print(y); print(x) } }`,
+ `func _(y int) { f(y) } `,
+ `func _(y int) {
+ var x int = y
+ _ = func() { y := 1; print(y); print(x) }
+}`,
+ },
+ })
+}
+
+func TestTailCallStrategy(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Tail call.",
+ `func f() int { return 1 }`,
+ `func _() int { return f() }`,
+ `func _() int { return 1 }`,
+ },
+ {
+ "Void tail call.",
+ `func f() { println() }`,
+ `func _() { f() }`,
+ `func _() { println() }`,
+ },
+ {
+ "Void tail call with defer.", // => literalized
+ `func f() { defer f(); println() }`,
+ `func _() { f() }`,
+ `func _() { func() { defer f(); println() }() }`,
+ },
+ // Tests for issue #63336:
+ {
+ "Tail call with non-trivial return conversion (caller.sig = callee.sig).",
+ `func f() error { if true { return nil } else { return e } }; var e struct{error}`,
+ `func _() error { return f() }`,
+ `func _() error {
+ if true {
+ return nil
+ } else {
+ return e
+ }
+}`,
+ },
+ {
+ "Tail call with non-trivial return conversion (caller.sig != callee.sig).",
+ `func f() error { return E{} }; type E struct{error}`,
+ `func _() any { return f() }`,
+ `func _() any { return func() error { return E{} }() }`,
+ },
+ })
+}
+
+func TestSpreadCalls(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Edge case: cannot literalize spread method call.",
+ `type I int
+ func g() (I, I)
+ func (r I) f(x, y I) I {
+ defer g() // force literalization
+ return x + y + r
+ }`,
+ `func _() I { return recover().(I).f(g()) }`,
+ `error: can't yet inline spread call to method`,
+ },
+ {
+ "Spread argument evaluated for effect.",
+ `func f(int, int) {}; func g() (int, int)`,
+ `func _() { f(g()) }`,
+ `func _() { _, _ = g() }`,
+ },
+ {
+ "Edge case: receiver and spread argument, both evaluated for effect.",
+ `type T int; func (T) f(int, int) {}; func g() (int, int)`,
+ `func _() { T(0).f(g()) }`,
+ `func _() {
+ var (
+ _ = T(0)
+ _, _ = g()
+ )
+}`,
+ },
+ })
+}
+
+func TestVariadic(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Variadic cancellation (basic).",
+ `func f(args ...any) { defer f(&args); println(args) }`,
+ `func _(slice []any) { f(slice...) }`,
+ `func _(slice []any) { func(args []any) { defer f(&args); println(args) }(slice) }`,
+ },
+ {
+ "Variadic cancellation (literalization with parameter elimination).",
+ `func f(args ...any) { defer f(); println(args) }`,
+ `func _(slice []any) { f(slice...) }`,
+ `func _(slice []any) { func() { defer f(); println(slice) }() }`,
+ },
+ {
+ "Variadic cancellation (reduction).",
+ `func f(args ...any) { println(args) }`,
+ `func _(slice []any) { f(slice...) }`,
+ `func _(slice []any) { println(slice) }`,
+ },
+ {
+ "Variadic elimination (literalization).",
+ `func f(x any, rest ...any) { defer println(x, rest) }`, // defer => literalization
+ `func _() { f(1, 2, 3) }`,
+ `func _() { func() { defer println(any(1), []any{2, 3}) }() }`,
+ },
+ {
+ "Variadic elimination (reduction).",
+ `func f(x int, rest ...int) { println(x, rest) }`,
+ `func _() { f(1, 2, 3) }`,
+ `func _() { println(1, []int{2, 3}) }`,
+ },
+ {
+ "Spread call to variadic (1 arg, 1 param).",
+ `func f(rest ...int) { println(rest) }; func g() (a, b int)`,
+ `func _() { f(g()) }`,
+ `func _() { func(rest ...int) { println(rest) }(g()) }`,
+ },
+ {
+ "Spread call to variadic (1 arg, 2 params).",
+ `func f(x int, rest ...int) { println(x, rest) }; func g() (a, b int)`,
+ `func _() { f(g()) }`,
+ `func _() { func(x int, rest ...int) { println(x, rest) }(g()) }`,
+ },
+ {
+ "Spread call to variadic (1 arg, 3 params).",
+ `func f(x, y int, rest ...int) { println(x, y, rest) }; func g() (a, b, c int)`,
+ `func _() { f(g()) }`,
+ `func _() { func(x, y int, rest ...int) { println(x, y, rest) }(g()) }`,
+ },
+ })
+}
+
+func TestParameterBindingDecl(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "IncDec counts as assignment.",
+ `func f(x int) { x++ }`,
+ `func _() { f(1) }`,
+ `func _() {
+ var x int = 1
+ x++
+}`,
+ },
+ {
+ "Binding declaration (x, y, z eliminated).",
+ `func f(w, x, y any, z int) { println(w, y, z) }; func g(int) int`,
+ `func _() { f(g(0), g(1), g(2), g(3)) }`,
+ `func _() {
+ var w, _ any = g(0), g(1)
+ println(w, any(g(2)), g(3))
+}`,
+ },
+ {
+ "Reduction of stmt-context call to { return exprs }, with substitution",
+ `func f(ch chan int) int { return <-ch }; func g() chan int`,
+ `func _() { f(g()) }`,
+ `func _() { <-g() }`,
+ },
+ {
+ // Same again, with callee effects:
+ "Binding decl in reduction of stmt-context call to { return exprs }",
+ `func f(x int) int { return <-h(g(2), x) }; func g(int) int; func h(int, int) chan int`,
+ `func _() { f(g(1)) }`,
+ `func _() {
+ var x int = g(1)
+ <-h(g(2), x)
+}`,
+ },
+ {
+ "No binding decl due to shadowing of int",
+ `func f(int, y any, z int) { defer g(0); println(int, y, z) }; func g(int) int`,
+ `func _() { f(g(1), g(2), g(3)) }`,
+ `func _() { func(int, y any, z int) { defer g(0); println(int, y, z) }(g(1), g(2), g(3)) }`,
+ },
+ })
+}
+
+func TestEmbeddedFields(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Embedded fields in x.f method selection (direct).",
+ `type T int; func (t T) f() { print(t) }; type U struct{ T }`,
+ `func _(u U) { u.f() }`,
+ `func _(u U) { print(u.T) }`,
+ },
+ {
+ "Embedded fields in x.f method selection (implicit *).",
+ `type ( T int; U struct{*T}; V struct {U} ); func (t T) f() { print(t) }`,
+ `func _(v V) { v.f() }`,
+ `func _(v V) { print(*v.U.T) }`,
+ },
+ {
+ "Embedded fields in x.f method selection (implicit &).",
+ `type ( T int; U struct{T}; V struct {U} ); func (t *T) f() { print(t) }`,
+ `func _(v V) { v.f() }`,
+ `func _(v V) { print(&v.U.T) }`,
+ },
+ // Now the same tests again with T.f(recv).
+ {
+ "Embedded fields in T.f method selection.",
+ `type T int; func (t T) f() { print(t) }; type U struct{ T }`,
+ `func _(u U) { U.f(u) }`,
+ `func _(u U) { print(u.T) }`,
+ },
+ {
+ "Embedded fields in T.f method selection (implicit *).",
+ `type ( T int; U struct{*T}; V struct {U} ); func (t T) f() { print(t) }`,
+ `func _(v V) { V.f(v) }`,
+ `func _(v V) { print(*v.U.T) }`,
+ },
+ {
+ "Embedded fields in (*T).f method selection.",
+ `type ( T int; U struct{T}; V struct {U} ); func (t *T) f() { print(t) }`,
+ `func _(v V) { (*V).f(&v) }`,
+ `func _(v V) { print(&(&v).U.T) }`,
+ },
+ {
+ // x is a single-assign var, and x.f does not load through a pointer
+ // (despite types.Selection.Indirect=true), so x is pure.
+ "No binding decl is required for recv in method-to-method calls.",
+ `type T struct{}; func (x *T) f() { g(); print(*x) }; func g()`,
+ `func (x *T) _() { x.f() }`,
+ `func (x *T) _() {
+ g()
+ print(*x)
+}`,
+ },
+ {
+ "Same, with implicit &recv.",
+ `type T struct{}; func (x *T) f() { g(); print(*x) }; func g()`,
+ `func (x T) _() { x.f() }`,
+ `func (x T) _() {
+ {
+ var x *T = &x
+ g()
+ print(*x)
+ }
+}`,
+ },
+ })
+}
+
+func TestSubstitutionPreservesArgumentEffectOrder(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Arguments have effects, but parameters are evaluated in order.",
+ `func f(a, b, c int) { print(a, b, c) }; func g(int) int`,
+ `func _() { f(g(1), g(2), g(3)) }`,
+ `func _() { print(g(1), g(2), g(3)) }`,
+ },
+ {
+ "Arguments have effects, and parameters are evaluated out of order.",
+ `func f(a, b, c int) { print(a, c, b) }; func g(int) int`,
+ `func _() { f(g(1), g(2), g(3)) }`,
+ `func _() {
+ var a, b int = g(1), g(2)
+ print(a, g(3), b)
+}`,
+ },
+ {
+ "Pure arguments may commute with argument that have effects.",
+ `func f(a, b, c int) { print(a, c, b) }; func g(int) int`,
+ `func _() { f(g(1), 2, g(3)) }`,
+ `func _() { print(g(1), g(3), 2) }`,
+ },
+ {
+ "Impure arguments may commute with each other.",
+ `func f(a, b, c, d int) { print(a, c, b, d) }; func g(int) int; var x, y int`,
+ `func _() { f(g(1), x, y, g(2)) }`,
+ `func _() { print(g(1), y, x, g(2)) }`,
+ },
+ {
+ "Impure arguments do not commute with arguments that have effects (1)",
+ `func f(a, b, c, d int) { print(a, c, b, d) }; func g(int) int; var x, y int`,
+ `func _() { f(g(1), g(2), y, g(3)) }`,
+ `func _() {
+ var a, b int = g(1), g(2)
+ print(a, y, b, g(3))
+}`,
+ },
+ {
+ "Impure arguments do not commute with those that have effects (2).",
+ `func f(a, b, c, d int) { print(a, c, b, d) }; func g(int) int; var x, y int`,
+ `func _() { f(g(1), y, g(2), g(3)) }`,
+ `func _() {
+ var a, b int = g(1), y
+ print(a, g(2), b, g(3))
+}`,
+ },
+ {
+ "Callee effects commute with pure arguments.",
+ `func f(a, b, c int) { print(a, c, recover().(int), b) }; func g(int) int`,
+ `func _() { f(g(1), 2, g(3)) }`,
+ `func _() { print(g(1), g(3), recover().(int), 2) }`,
+ },
+ {
+ "Callee reads may commute with impure arguments.",
+ `func f(a, b int) { print(a, x, b) }; func g(int) int; var x, y int`,
+ `func _() { f(g(1), y) }`,
+ `func _() { print(g(1), x, y) }`,
+ },
+ {
+ "All impure parameters preceding a read hazard must be kept.",
+ `func f(a, b, c int) { print(a, b, recover().(int), c) }; var x, y, z int`,
+ `func _() { f(x, y, z) }`,
+ `func _() {
+ var c int = z
+ print(x, y, recover().(int), c)
+}`,
+ },
+ {
+ "All parameters preceding a write hazard must be kept.",
+ `func f(a, b, c int) { print(a, b, recover().(int), c) }; func g(int) int; var x, y, z int`,
+ `func _() { f(x, y, g(0)) }`,
+ `func _() {
+ var a, b, c int = x, y, g(0)
+ print(a, b, recover().(int), c)
+}`,
+ },
+ {
+ "[W1 R0 W2 W4 R3] -- test case for second iteration of effect loop",
+ `func f(a, b, c, d, e int) { print(b, a, c, e, d) }; func g(int) int; var x, y int`,
+ `func _() { f(x, g(1), g(2), y, g(3)) }`,
+ `func _() {
+ var a, b, c, d int = x, g(1), g(2), y
+ print(b, a, c, g(3), d)
+}`,
+ },
+ {
+ // In this example, the set() call is rejected as a substitution
+ // candidate due to a shadowing conflict (x). This must entail that the
+ // selection x.y (R) is also rejected, because it is lower numbered.
+ //
+ // Incidentally this program (which panics when executed) illustrates
+ // that although effects occur left-to-right, read operations such
+ // as x.y are not ordered wrt writes, depending on the compiler.
+ // Changing x.y to identity(x).y forces the ordering and avoids the panic.
+ "Hazards with args already rejected (e.g. due to shadowing) are detected too.",
+ `func f(x, y int) int { return x + y }; func set[T any](ptr *T, old, new T) int { println(old); *ptr = new; return 0; }`,
+ `func _() { x := new(struct{ y int }); f(x.y, set(&x, x, nil)) }`,
+ `func _() {
+ x := new(struct{ y int })
+ {
+ var x, y int = x.y, set(&x, x, nil)
+ _ = x + y
+ }
+}`,
+ },
+ {
+ // Rejection of a later parameter for reasons other than callee
+ // effects (e.g. escape) may create hazards with lower-numbered
+ // parameters that require them to be rejected too.
+ "Hazards with already eliminated parameters (variant)",
+ `func f(x, y int) { _ = &y }; func g(int) int`,
+ `func _() { f(g(1), g(2)) }`,
+ `func _() {
+ var _, y int = g(1), g(2)
+ _ = &y
+}`,
+ },
+ {
+ // In this case g(2) is rejected for substitution because it is
+ // unreferenced but has effects, so parameter x must also be rejected
+ // so that its argument v can be evaluated earlier in the binding decl.
+ "Hazards with already eliminated parameters (unreferenced fx variant)",
+ `func f(x, y int) { _ = x }; func g(int) int; var v int`,
+ `func _() { f(v, g(2)) }`,
+ `func _() {
+ var x, _ int = v, g(2)
+ _ = x
+}`,
+ },
+ {
+ "Defer f() evaluates f() before unknown effects",
+ `func f(int, y any, z int) { defer println(int, y, z) }; func g(int) int`,
+ `func _() { f(g(1), g(2), g(3)) }`,
+ `func _() { func() { defer println(any(g(1)), any(g(2)), g(3)) }() }`,
+ },
+ })
+}
+
+func TestNamedResultVars(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Stmt-context call to {return g()} that mentions named result.",
+ `func f() (x int) { return g(x) }; func g(int) int`,
+ `func _() { f() }`,
+ `func _() {
+ var x int
+ g(x)
+}`,
+ },
+ {
+ "Ditto, with binding decl again.",
+ `func f(y string) (x int) { return x+x+len(y+y) }`,
+ `func _() { f(".") }`,
+ `func _() {
+ var (
+ y string = "."
+ x int
+ )
+ _ = x + x + len(y+y)
+}`,
+ },
+
+ {
+ "Ditto, with binding decl (due to repeated y refs).",
+ `func f(y string) (x string) { return x+y+y }`,
+ `func _() { f(".") }`,
+ `func _() {
+ var (
+ y string = "."
+ x string
+ )
+ _ = x + y + y
+}`,
+ },
+ {
+ "Stmt-context call to {return binary} that mentions named result.",
+ `func f() (x int) { return x+x }`,
+ `func _() { f() }`,
+ `func _() {
+ var x int
+ _ = x + x
+}`,
+ },
+ {
+ "Tail call to {return expr} that mentions named result.",
+ `func f() (x int) { return x }`,
+ `func _() int { return f() }`,
+ `func _() int { return func() (x int) { return x }() }`,
+ },
+ {
+ "Tail call to {return} that implicitly reads named result.",
+ `func f() (x int) { return }`,
+ `func _() int { return f() }`,
+ `func _() int { return func() (x int) { return }() }`,
+ },
+ {
+ "Spread-context call to {return expr} that mentions named result.",
+ `func f() (x, y int) { return x, y }`,
+ `func _() { var _, _ = f() }`,
+ `func _() { var _, _ = func() (x, y int) { return x, y }() }`,
+ },
+ {
+ "Shadowing in binding decl for named results => literalization.",
+ `func f(y string) (x y) { return x+x+len(y+y) }; type y = int`,
+ `func _() { f(".") }`,
+ `func _() { func(y string) (x y) { return x + x + len(y+y) }(".") }`,
+ },
+ })
+}
+
+func TestSubstitutionPreservesParameterType(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Substitution preserves argument type (#63193).",
+ `func f(x int16) { y := x; _ = (*int16)(&y) }`,
+ `func _() { f(1) }`,
+ `func _() {
+ y := int16(1)
+ _ = (*int16)(&y)
+}`,
+ },
+ {
+ "Same, with non-constant (unnamed to named struct) conversion.",
+ `func f(x T) { y := x; _ = (*T)(&y) }; type T struct{}`,
+ `func _() { f(struct{}{}) }`,
+ `func _() {
+ y := T(struct{}{})
+ _ = (*T)(&y)
+}`,
+ },
+ {
+ "Same, with non-constant (chan to <-chan) conversion.",
+ `func f(x T) { y := x; _ = (*T)(&y) }; type T = <-chan int; var ch chan int`,
+ `func _() { f(ch) }`,
+ `func _() {
+ y := T(ch)
+ _ = (*T)(&y)
+}`,
+ },
+ {
+ "Same, with untyped nil to typed nil conversion.",
+ `func f(x *int) { y := x; _ = (**int)(&y) }`,
+ `func _() { f(nil) }`,
+ `func _() {
+ y := (*int)(nil)
+ _ = (**int)(&y)
+}`,
+ },
+ {
+ "Conversion of untyped int to named type is made explicit.",
+ `type T int; func (x T) f() { x.g() }; func (T) g() {}`,
+ `func _() { T.f(1) }`,
+ `func _() { T(1).g() }`,
+ },
+ {
+ "Check for shadowing error on type used in the conversion.",
+ `func f(x T) { _ = &x == (*T)(nil) }; type T int16`,
+ `func _() { type T bool; f(1) }`,
+ `error: T.*shadowed.*by.*type`,
+ },
+ })
+}
+
+func runTests(t *testing.T, tests []testcase) {
+ for _, test := range tests {
+ test := test
+ t.Run(test.descr, func(t *testing.T) {
+ fset := token.NewFileSet()
+ mustParse := func(filename string, content any) *ast.File {
+ f, err := parser.ParseFile(fset, filename, content, parser.ParseComments|parser.SkipObjectResolution)
+ if err != nil {
+ t.Fatalf("ParseFile: %v", err)
+ }
+ return f
+ }
+
+ // Parse callee file and find first func decl named f.
+ calleeContent := "package p\n" + test.callee
+ calleeFile := mustParse("callee.go", calleeContent)
+ var decl *ast.FuncDecl
+ for _, d := range calleeFile.Decls {
+ if d, ok := d.(*ast.FuncDecl); ok && d.Name.Name == funcName {
+ decl = d
+ break
+ }
+ }
+ if decl == nil {
+ t.Fatalf("declaration of func %s not found: %s", funcName, test.callee)
+ }
+
+ // Parse caller file and find first call to f().
+ callerContent := "package p\n" + test.caller
+ callerFile := mustParse("caller.go", callerContent)
+ var call *ast.CallExpr
+ ast.Inspect(callerFile, func(n ast.Node) bool {
+ if n, ok := n.(*ast.CallExpr); ok {
+ switch fun := n.Fun.(type) {
+ case *ast.SelectorExpr:
+ if fun.Sel.Name == funcName {
+ call = n
+ }
+ case *ast.Ident:
+ if fun.Name == funcName {
+ call = n
+ }
+ }
+ }
+ return call == nil
+ })
+ if call == nil {
+ t.Fatalf("call to %s not found: %s", funcName, test.caller)
+ }
+
+ // Type check both files as one package.
+ info := &types.Info{
+ Defs: make(map[*ast.Ident]types.Object),
+ Uses: make(map[*ast.Ident]types.Object),
+ Types: make(map[ast.Expr]types.TypeAndValue),
+ Implicits: make(map[ast.Node]types.Object),
+ Selections: make(map[*ast.SelectorExpr]*types.Selection),
+ Scopes: make(map[ast.Node]*types.Scope),
+ }
+ conf := &types.Config{Error: func(err error) { t.Error(err) }}
+ pkg, err := conf.Check("p", fset, []*ast.File{callerFile, calleeFile}, info)
+ if err != nil {
+ t.Fatal("transformation introduced type errors")
+ }
+
+ // Analyze callee and inline call.
+ doIt := func() ([]byte, error) {
+ callee, err := inline.AnalyzeCallee(t.Logf, fset, pkg, info, decl, []byte(calleeContent))
+ if err != nil {
+ return nil, err
+ }
+ if err := checkTranscode(callee); err != nil {
+ t.Fatal(err)
+ }
+
+ caller := &inline.Caller{
+ Fset: fset,
+ Types: pkg,
+ Info: info,
+ File: callerFile,
+ Call: call,
+ Content: []byte(callerContent),
+ }
+ check := checkNoMutation(caller.File)
+ defer check()
+ return inline.Inline(t.Logf, caller, callee)
+ }
+ gotContent, err := doIt()
+
+ // Want error?
+ if rest := strings.TrimPrefix(test.want, "error: "); rest != test.want {
+ if err == nil {
+ t.Fatalf("unexpected sucess: want error matching %q", rest)
+ }
+ msg := err.Error()
+ if ok, err := regexp.MatchString(rest, msg); err != nil {
+ t.Fatalf("invalid regexp: %v", err)
+ } else if !ok {
+ t.Fatalf("wrong error: %s (want match for %q)", msg, rest)
+ }
+ return
+ }
+
+ // Want success.
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Compute a single-hunk line-based diff.
+ srcLines := strings.Split(callerContent, "\n")
+ gotLines := strings.Split(string(gotContent), "\n")
+ for len(srcLines) > 0 && len(gotLines) > 0 &&
+ srcLines[0] == gotLines[0] {
+ srcLines = srcLines[1:]
+ gotLines = gotLines[1:]
+ }
+ for len(srcLines) > 0 && len(gotLines) > 0 &&
+ srcLines[len(srcLines)-1] == gotLines[len(gotLines)-1] {
+ srcLines = srcLines[:len(srcLines)-1]
+ gotLines = gotLines[:len(gotLines)-1]
+ }
+ got := strings.Join(gotLines, "\n")
+
+ if strings.TrimSpace(got) != strings.TrimSpace(test.want) {
+ t.Fatalf("\nInlining this call:\t%s\nof this callee: \t%s\nproduced:\n%s\nWant:\n\n%s",
+ test.caller,
+ test.callee,
+ got,
+ test.want)
+ }
+
+ // Check that resulting code type-checks.
+ newCallerFile := mustParse("newcaller.go", gotContent)
+ if _, err := conf.Check("p", fset, []*ast.File{newCallerFile, calleeFile}, nil); err != nil {
+ t.Fatalf("modified source failed to typecheck: <<%s>>", gotContent)
+ }
+ })
+ }
+}
+
+// -- helpers --
+
+// checkNoMutation returns a function that, when called,
+// asserts that file was not modified since the checkNoMutation call.
+func checkNoMutation(file *ast.File) func() {
+ pre := deepHash(file)
+ return func() {
+ post := deepHash(file)
+ if pre != post {
+ panic("Inline mutated caller.File")
+ }
+ }
+}
+
+// checkTranscode replaces *callee by the results of gob-encoding and
+// then decoding it, to test that these operations are lossless.
+func checkTranscode(callee *inline.Callee) error {
+ // Perform Gob transcoding so that it is exercised by the test.
+ var enc bytes.Buffer
+ if err := gob.NewEncoder(&enc).Encode(callee); err != nil {
+ return fmt.Errorf("internal error: gob encoding failed: %v", err)
+ }
+ *callee = inline.Callee{}
+ if err := gob.NewDecoder(&enc).Decode(callee); err != nil {
+ return fmt.Errorf("internal error: gob decoding failed: %v", err)
+ }
+ return nil
+}
+
// TODO(adonovan): publish this a helper (#61386).
func extractTxtar(ar *txtar.Archive, dir string) error {
for _, file := range ar.Files {
@@ -323,3 +1229,83 @@ func extractTxtar(ar *txtar.Archive, dir string) error {
}
return nil
}
+
+// deepHash computes a cryptographic hash of an ast.Node so that
+// if the data structure is mutated, the hash changes.
+// It assumes Go variables do not change address.
+//
+// TODO(adonovan): consider publishing this in the astutil package.
+//
+// TODO(adonovan): consider a variant that reports where in the tree
+// the mutation occurred (obviously at a cost in space).
+func deepHash(n ast.Node) any {
+ seen := make(map[unsafe.Pointer]bool) // to break cycles
+
+ hasher := sha256.New()
+ le := binary.LittleEndian
+ writeUint64 := func(v uint64) {
+ var bs [8]byte
+ le.PutUint64(bs[:], v)
+ hasher.Write(bs[:])
+ }
+
+ var visit func(reflect.Value)
+ visit = func(v reflect.Value) {
+ switch v.Kind() {
+ case reflect.Ptr:
+ ptr := v.UnsafePointer()
+ writeUint64(uint64(uintptr(ptr)))
+ if !v.IsNil() {
+ if !seen[ptr] {
+ seen[ptr] = true
+ // Skip types we don't handle yet, but don't care about.
+ switch v.Interface().(type) {
+ case *ast.Scope:
+ return // involves a map
+ }
+
+ visit(v.Elem())
+ }
+ }
+
+ case reflect.Struct:
+ for i := 0; i < v.Type().NumField(); i++ {
+ visit(v.Field(i))
+ }
+
+ case reflect.Slice:
+ ptr := v.UnsafePointer()
+ // We may encounter different slices at the same address,
+ // so don't mark ptr as "seen".
+ writeUint64(uint64(uintptr(ptr)))
+ writeUint64(uint64(v.Len()))
+ writeUint64(uint64(v.Cap()))
+ for i := 0; i < v.Len(); i++ {
+ visit(v.Index(i))
+ }
+
+ case reflect.Interface:
+ if v.IsNil() {
+ writeUint64(0)
+ } else {
+ rtype := reflect.ValueOf(v.Type()).UnsafePointer()
+ writeUint64(uint64(uintptr(rtype)))
+ visit(v.Elem())
+ }
+
+ case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer:
+ panic(v) // unreachable in AST
+
+ default: // bool, string, number
+ if v.Kind() == reflect.String { // proper framing
+ writeUint64(uint64(v.Len()))
+ }
+ binary.Write(hasher, le, v.Interface())
+ }
+ }
+ visit(reflect.ValueOf(n))
+
+ var hash [sha256.Size]byte
+ hasher.Sum(hash[:0])
+ return hash
+}
diff --git a/internal/refactor/inline/testdata/basic-err.txtar b/internal/refactor/inline/testdata/basic-err.txtar
index 18e0eb7adb3..4868b2cbfb1 100644
--- a/internal/refactor/inline/testdata/basic-err.txtar
+++ b/internal/refactor/inline/testdata/basic-err.txtar
@@ -19,6 +19,6 @@ package a
import "io"
-var _ = func(err error) string { return err.Error() }(io.EOF) //@ inline(re"getError", getError)
+var _ = io.EOF.Error() //@ inline(re"getError", getError)
func getError(err error) string { return err.Error() }
diff --git a/internal/refactor/inline/testdata/basic-literal.txtar b/internal/refactor/inline/testdata/basic-literal.txtar
index 50bac33456a..a74fbda42de 100644
--- a/internal/refactor/inline/testdata/basic-literal.txtar
+++ b/internal/refactor/inline/testdata/basic-literal.txtar
@@ -1,19 +1,29 @@
-Most basic test of inlining by literalization.
+Basic tests of inlining by literalization.
+
+The use of defer forces literalization.
+
+recover() is an example of a function with effects,
+defeating elimination of parameter x; but parameter
+y is eliminated by substitution.
-- go.mod --
module testdata
go 1.12
--- a/a.go --
+-- a/a1.go --
package a
-var _ = add(1, 2) //@ inline(re"add", add)
+func _() {
+ add(recover().(int), 2) //@ inline(re"add", add1)
+}
-func add(x, y int) int { return x + y }
+func add(x, y int) int { defer print(); return x + y }
--- add --
+-- add1 --
package a
-var _ = func(x, y int) int { return x + y }(1, 2) //@ inline(re"add", add)
+func _() {
+ func(x int) int { defer print(); return x + 2 }(recover().(int)) //@ inline(re"add", add1)
+}
-func add(x, y int) int { return x + y }
+func add(x, y int) int { defer print(); return x + y }
diff --git a/internal/refactor/inline/testdata/basic-reduce.txtar b/internal/refactor/inline/testdata/basic-reduce.txtar
index 9eedbc05f1e..10aca5284ef 100644
--- a/internal/refactor/inline/testdata/basic-reduce.txtar
+++ b/internal/refactor/inline/testdata/basic-reduce.txtar
@@ -4,7 +4,7 @@ Most basic test of inlining by reduction.
module testdata
go 1.12
--- a/a.go --
+-- a/a0.go --
package a
var _ = zero() //@ inline(re"zero", zero)
@@ -14,6 +14,37 @@ func zero() int { return 0 }
-- zero --
package a
-var _ = (0) //@ inline(re"zero", zero)
+var _ = 0 //@ inline(re"zero", zero)
func zero() int { return 0 }
+
+-- a/a1.go --
+package a
+
+func _() {
+ one := 1
+ add(one, 2) //@ inline(re"add", add1)
+}
+
+func add(x, y int) int { return x + y }
+
+-- add1 --
+package a
+
+func _() {
+ one := 1
+ _ = one + 2 //@ inline(re"add", add1)
+}
+
+func add(x, y int) int { return x + y }
+
+-- a/a2.go --
+package a
+
+var _ = add(len(""), 2) //@ inline(re"add", add2)
+
+-- add2 --
+package a
+
+var _ = len("") + 2 //@ inline(re"add", add2)
+
diff --git a/internal/refactor/inline/testdata/cgo.txtar b/internal/refactor/inline/testdata/cgo.txtar
new file mode 100644
index 00000000000..41567ed7cbb
--- /dev/null
+++ b/internal/refactor/inline/testdata/cgo.txtar
@@ -0,0 +1,45 @@
+Test that attempts to inline with caller or callee in a cgo-generated
+file are rejected.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+/*
+static void f() {}
+*/
+import "C"
+
+func a() {
+ C.f() //@ inline(re"f", re"cannot inline cgo-generated functions")
+ g() //@ inline(re"g", re`cannot inline calls from files that import "C"`)
+}
+
+func g() {
+ println()
+}
+
+-- a/a2.go --
+package a
+
+func b() {
+ a() //@ inline(re"a", re"cannot inline cgo-generated functions")
+}
+
+func c() {
+ b() //@ inline(re"b", result)
+}
+
+-- result --
+package a
+
+func b() {
+ a() //@ inline(re"a", re"cannot inline cgo-generated functions")
+}
+
+func c() {
+ a() //@ inline(re"b", result)
+}
diff --git a/internal/refactor/inline/testdata/comments.txtar b/internal/refactor/inline/testdata/comments.txtar
index 0482e919a48..76f64926b13 100644
--- a/internal/refactor/inline/testdata/comments.txtar
+++ b/internal/refactor/inline/testdata/comments.txtar
@@ -1,5 +1,11 @@
-Inlining, whether by literalization or reduction,
-preserves comments in the callee.
+Test of (lack of) comment preservation by inlining,
+whether by literalization or reduction.
+
+Comment handling was better in an earlier implementation
+based on byte-oriented file surgery; switching to AST
+manipulation (though better in all other respects) was
+a regression. The underlying problem of AST comment fidelity
+is Go issue #20744.
-- go.mod --
module testdata
@@ -22,12 +28,7 @@ func f() {
package a
func _() {
- func() {
- // a
- /* b */
- g() /* c */
- // d
- }() //@ inline(re"f", f)
+ g() //@ inline(re"f", f)
}
func f() {
@@ -50,7 +51,7 @@ func g() int { return 1 /*hello*/ + /*there*/ 1 }
package a
func _() {
- println((1 /*hello*/ + /*there*/ 1)) //@ inline(re"g", g)
+ println(1 + 1) //@ inline(re"g", g)
}
func g() int { return 1 /*hello*/ + /*there*/ 1 }
diff --git a/internal/refactor/inline/testdata/crosspkg-selfref.txtar b/internal/refactor/inline/testdata/crosspkg-selfref.txtar
new file mode 100644
index 00000000000..0c45be87d92
--- /dev/null
+++ b/internal/refactor/inline/testdata/crosspkg-selfref.txtar
@@ -0,0 +1,32 @@
+A self-reference counts as a free reference,
+so that it gets properly package-qualified as needed.
+(Regression test for a bug.)
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+import "testdata/b"
+
+func _() {
+ b.F(1) //@ inline(re"F", output)
+}
+
+-- b/b.go --
+package b
+
+func F(x int) {
+ F(x + 2)
+}
+
+-- output --
+package a
+
+import "testdata/b"
+
+func _() {
+ b.F(1 + 2) //@ inline(re"F", output)
+}
diff --git a/internal/refactor/inline/testdata/crosspkg.txtar b/internal/refactor/inline/testdata/crosspkg.txtar
index 43dc63f32ea..7c0704be819 100644
--- a/internal/refactor/inline/testdata/crosspkg.txtar
+++ b/internal/refactor/inline/testdata/crosspkg.txtar
@@ -46,7 +46,6 @@ package a
import (
"fmt"
"testdata/b"
-
c "testdata/c"
)
@@ -54,8 +53,8 @@ import (
func A() {
fmt.Println()
- func() { c.C() }() //@ inline(re"B1", b1result)
- b.B2() //@ inline(re"B2", b2result)
+ c.C() //@ inline(re"B1", b1result)
+ b.B2() //@ inline(re"B2", b2result)
}
-- b2result --
@@ -72,6 +71,6 @@ import (
func A() {
fmt.Println()
- b.B1() //@ inline(re"B1", b1result)
- func() { fmt.Println() }() //@ inline(re"B2", b2result)
+ b.B1() //@ inline(re"B1", b1result)
+ fmt.Println() //@ inline(re"B2", b2result)
}
diff --git a/internal/refactor/inline/testdata/dotimport.txtar b/internal/refactor/inline/testdata/dotimport.txtar
index 7e886afdb94..8ca5f05cda7 100644
--- a/internal/refactor/inline/testdata/dotimport.txtar
+++ b/internal/refactor/inline/testdata/dotimport.txtar
@@ -28,8 +28,10 @@ func _() {
-- result --
package c
-import a "testdata/a"
+import (
+ a "testdata/a"
+)
func _() {
- func() { a.A() }() //@ inline(re"B", result)
+ a.A() //@ inline(re"B", result)
}
diff --git a/internal/refactor/inline/testdata/embed.txtar b/internal/refactor/inline/testdata/embed.txtar
new file mode 100644
index 00000000000..ab52f5a5a00
--- /dev/null
+++ b/internal/refactor/inline/testdata/embed.txtar
@@ -0,0 +1,28 @@
+Test of implicit field selections in method calls.
+
+The two level wrapping T -> unexported -> U is required
+to exercise the implicit selections exportedness check;
+with only a single level, the receiver declaration in
+"func (unexported) F()" would fail the earlier
+unexportedness check.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+import "testdata/b"
+
+func _(x b.T) {
+ x.F() //@ inline(re"F", re"in x.F, implicit reference to unexported field .unexported cannot be made explicit")
+}
+
+-- b/b.go --
+package b
+
+type T struct { unexported }
+type unexported struct { U }
+type U struct{}
+func (U) F() {}
diff --git a/internal/refactor/inline/testdata/empty-body.txtar b/internal/refactor/inline/testdata/empty-body.txtar
new file mode 100644
index 00000000000..8983fda8c6e
--- /dev/null
+++ b/internal/refactor/inline/testdata/empty-body.txtar
@@ -0,0 +1,103 @@
+Test of elimination of calls to functions with completely empty bodies.
+The arguments must still be evaluated and their results discarded.
+The number of discard blanks must match the type, not the syntax (see 2-ary f).
+If there are no arguments, the entire call is eliminated.
+
+We cannot eliminate some pure argument expressions because they
+may contain the last reference to a local variable.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a0.go --
+package a
+
+func _() {
+ empty() //@ inline(re"empty", empty0)
+}
+
+func empty(...any) {}
+
+-- empty0 --
+package a
+
+func _() {
+ //@ inline(re"empty", empty0)
+}
+
+func empty(...any) {}
+
+-- a/a1.go --
+package a
+
+func _(ch chan int) {
+ empty(f()) //@ inline(re"empty", empty1)
+}
+
+func f() (int, int)
+
+-- empty1 --
+package a
+
+func _(ch chan int) {
+ _, _ = f() //@ inline(re"empty", empty1)
+}
+
+func f() (int, int)
+
+-- a/a2.go --
+package a
+
+func _(ch chan int) {
+ empty(-1, ch, len(""), g(), <-ch) //@ inline(re"empty", empty2)
+}
+
+func g() int
+
+-- empty2 --
+package a
+
+func _(ch chan int) {
+ _ = []any{-1, ch, len(""), g(), <-ch} //@ inline(re"empty", empty2)
+}
+
+func g() int
+
+-- a/a3.go --
+package a
+
+func _() {
+ new(T).empty() //@ inline(re"empty", empty3)
+}
+
+type T int
+
+func (T) empty() int {}
+
+-- empty3 --
+package a
+
+func _() {
+ //@ inline(re"empty", empty3)
+}
+
+type T int
+
+func (T) empty() int {}
+
+-- a/a4.go --
+package a
+
+func _() {
+ var x T
+ x.empty() //@ inline(re"empty", empty4)
+}
+
+-- empty4 --
+package a
+
+func _() {
+ var x T
+ _ = x //@ inline(re"empty", empty4)
+}
diff --git a/internal/refactor/inline/testdata/import-shadow.txtar b/internal/refactor/inline/testdata/import-shadow.txtar
index 913c9cbe01a..4188a52375d 100644
--- a/internal/refactor/inline/testdata/import-shadow.txtar
+++ b/internal/refactor/inline/testdata/import-shadow.txtar
@@ -1,5 +1,11 @@
-Test of heuristic for generating a fresh import PkgName.
-The names c and c0 are taken, so it uses c1.
+Just because a package (e.g. log) is imported by the caller,
+and the name log is in scope, doesn't mean the name in scope
+refers to the package: it could be locally shadowed.
+
+In all three scenarios below, renaming import with a fresh name is
+added because the usual name is locally shadowed: in cases 1, 2 an
+existing import is shadowed by (respectively) a local constant,
+parameter; in case 3 there is no existing import.
-- go.mod --
module testdata
@@ -9,33 +15,111 @@ go 1.12
package a
import "testdata/b"
+import "log"
func A() {
- const c = 1
- type c0 int
- b.B() //@ inline(re"B", result)
+ const log = "shadow"
+ b.B() //@ inline(re"B", bresult)
}
+var _ log.Logger
+
-- b/b.go --
package b
-import "testdata/c"
+import "log"
-func B() { c.C() }
+func B() {
+ log.Printf("")
+}
--- c/c.go --
-package c
+-- bresult --
+package a
-func C() {}
+import (
+ "log"
+ log0 "log"
+)
--- result --
+func A() {
+ const log = "shadow"
+ log0.Printf("") //@ inline(re"B", bresult)
+}
+
+var _ log.Logger
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
package a
-import c1 "testdata/c"
+import "testdata/b"
-func A() {
- const c = 1
- type c0 int
- func() { c1.C() }() //@ inline(re"B", result)
+var x b.T
+
+func A(b int) {
+ x.F() //@ inline(re"F", fresult)
}
+-- b/b.go --
+package b
+
+type T struct{}
+
+func (T) F() {
+ One()
+ Two()
+}
+
+func One() {}
+func Two() {}
+
+-- fresult --
+package a
+
+import (
+ "testdata/b"
+ b0 "testdata/b"
+)
+
+var x b.T
+
+func A(b int) {
+
+ b0.One()
+ b0.Two()
+ //@ inline(re"F", fresult)
+}
+
+-- d/d.go --
+package d
+
+import "testdata/e"
+
+func D() {
+ const log = "shadow"
+ e.E() //@ inline(re"E", eresult)
+}
+
+-- e/e.go --
+package e
+
+import "log"
+
+func E() {
+ log.Printf("")
+}
+
+-- eresult --
+package d
+
+import (
+ log0 "log"
+)
+
+func D() {
+ const log = "shadow"
+ log0.Printf("") //@ inline(re"E", eresult)
+}
diff --git a/internal/refactor/inline/testdata/issue62667.txtar b/internal/refactor/inline/testdata/issue62667.txtar
new file mode 100644
index 00000000000..21420e21df4
--- /dev/null
+++ b/internal/refactor/inline/testdata/issue62667.txtar
@@ -0,0 +1,44 @@
+Regression test for #62667: the callee's reference to Split
+was blindly qualified to path.Split even though the imported
+PkgName path is shadowed by the parameter of the same name.
+
+The defer is to defeat reduction of the call and
+substitution of the path parameter by g().
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+import "testdata/path"
+
+func A() {
+ path.Dir(g()) //@ inline(re"Dir", result)
+}
+
+func g() string
+
+-- path/path.go --
+package path
+
+func Dir(path string) {
+ defer func(){}()
+ Split(path)
+}
+
+func Split(string) {}
+
+-- result --
+package a
+
+import (
+ path0 "testdata/path"
+)
+
+func A() {
+ func(path string) { defer func() {}(); path0.Split(path) }(g()) //@ inline(re"Dir", result)
+}
+
+func g() string
\ No newline at end of file
diff --git a/internal/refactor/inline/testdata/issue63298.txtar b/internal/refactor/inline/testdata/issue63298.txtar
new file mode 100644
index 00000000000..e355e8e64d9
--- /dev/null
+++ b/internal/refactor/inline/testdata/issue63298.txtar
@@ -0,0 +1,52 @@
+Regression test for #63298: inlining a function that
+depends on two packages with the same name leads
+to duplicate PkgNames.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+func _() {
+ a2() //@ inline(re"a2", result)
+}
+
+-- a/a2.go --
+package a
+
+import "testdata/b"
+import anotherb "testdata/another/b"
+
+func a2() {
+ b.B()
+ anotherb.B()
+}
+
+-- b/b.go --
+package b
+
+func B() {}
+
+-- b/another/b.go --
+package b
+
+func B() {}
+
+-- result --
+package a
+
+import (
+ b "testdata/b"
+ b0 "testdata/another/b"
+
+ //@ inline(re"a2", result)
+)
+
+func _() {
+
+ b.B()
+ b0.B()
+
+}
\ No newline at end of file
diff --git a/internal/refactor/inline/testdata/line-directives.txtar b/internal/refactor/inline/testdata/line-directives.txtar
new file mode 100644
index 00000000000..66ae9ede335
--- /dev/null
+++ b/internal/refactor/inline/testdata/line-directives.txtar
@@ -0,0 +1,35 @@
+Test of line directives in caller and caller.
+Neither should have any effect on inlining.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+import "testdata/b"
+
+func A() {
+//line b2.go:3:3
+ b.F() //@ inline(re"F", result)
+}
+
+-- b/b.go --
+package b
+
+//line b2.go:1:1
+func F() { println("hi") }
+
+-- b/b2.go --
+package b
+
+func NotWhatYouWereLookingFor() {}
+
+-- result --
+package a
+
+func A() {
+//line b2.go:3:3
+ println("hi") //@ inline(re"F", result)
+}
diff --git a/internal/refactor/inline/testdata/method.txtar b/internal/refactor/inline/testdata/method.txtar
index a4e02d575ca..b141b09d707 100644
--- a/internal/refactor/inline/testdata/method.txtar
+++ b/internal/refactor/inline/testdata/method.txtar
@@ -1,6 +1,7 @@
Test of inlining a method call.
-The call to (*T).g0 implicitly takes the address &x.
+The call to (*T).g0 implicitly takes the address &x, and
+the call to T.h implictly dereferences the argument *ptr.
The f1/g1 methods have parameters, exercising the
splicing of the receiver into the parameter list.
@@ -14,7 +15,8 @@ go 1.12
package a
type T int
-func (T) f0() {}
+
+func (recv T) f0() { println(recv) }
func _(x T) {
x.f0() //@ inline(re"f0", f0)
@@ -25,16 +27,16 @@ package a
type T int
-func (T) f0() {}
+func (recv T) f0() { println(recv) }
func _(x T) {
- func(_ T) {}(x) //@ inline(re"f0", f0)
+ println(x) //@ inline(re"f0", f0)
}
-- a/g0.go --
package a
-func (recv *T) g0() {}
+func (recv *T) g0() { println(recv) }
func _(x T) {
x.g0() //@ inline(re"g0", g0)
@@ -43,16 +45,16 @@ func _(x T) {
-- g0 --
package a
-func (recv *T) g0() {}
+func (recv *T) g0() { println(recv) }
func _(x T) {
- func(recv *T) {}(&x) //@ inline(re"g0", g0)
+ println(&x) //@ inline(re"g0", g0)
}
-- a/f1.go --
package a
-func (T) f1(int, int) {}
+func (recv T) f1(int, int) { println(recv) }
func _(x T) {
x.f1(1, 2) //@ inline(re"f1", f1)
@@ -61,16 +63,16 @@ func _(x T) {
-- f1 --
package a
-func (T) f1(int, int) {}
+func (recv T) f1(int, int) { println(recv) }
func _(x T) {
- func(_ T, _ int, _ int) {}(x, 1, 2) //@ inline(re"f1", f1)
+ println(x) //@ inline(re"f1", f1)
}
-- a/g1.go --
package a
-func (recv *T) g1(int, int) {}
+func (recv *T) g1(int, int) { println(recv) }
func _(x T) {
x.g1(1, 2) //@ inline(re"g1", g1)
@@ -79,10 +81,10 @@ func _(x T) {
-- g1 --
package a
-func (recv *T) g1(int, int) {}
+func (recv *T) g1(int, int) { println(recv) }
func _(x T) {
- func(recv *T, _ int, _ int) {}(&x, 1, 2) //@ inline(re"g1", g1)
+ println(&x) //@ inline(re"g1", g1)
}
-- a/h.go --
@@ -91,7 +93,8 @@ package a
func (T) h() int { return 1 }
func _() {
- new(T).h() //@ inline(re"h", h)
+ var ptr *T
+ ptr.h() //@ inline(re"h", h)
}
-- h --
@@ -100,5 +103,27 @@ package a
func (T) h() int { return 1 }
func _() {
- func(_ T) int { return 1 }(*new(T)) //@ inline(re"h", h)
+ var ptr *T
+
+ var _ T = *ptr
+ _ = 1
+ //@ inline(re"h", h)
+}
+
+-- a/i.go --
+package a
+
+func (T) i() int { return 1 }
+
+func _() {
+ (*T).i(nil) //@ inline(re"i", i)
+}
+
+-- i --
+package a
+
+func (T) i() int { return 1 }
+
+func _() {
+ _ = 1 //@ inline(re"i", i)
}
diff --git a/internal/refactor/inline/testdata/multistmt-body.txtar b/internal/refactor/inline/testdata/multistmt-body.txtar
new file mode 100644
index 00000000000..6bd0108e1fe
--- /dev/null
+++ b/internal/refactor/inline/testdata/multistmt-body.txtar
@@ -0,0 +1,87 @@
+Tests of reduction of calls to multi-statement bodies.
+
+a1: reduced to a block with a parameter binding decl.
+ (Parameter x can't be substituted by z without a shadowing conflict.)
+
+a2: reduced with parameter substitution (no shadowing).
+
+a3: literalized, because of the return statement.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a1.go --
+package a
+
+func _() {
+ z := 1
+ f(z, 2) //@ inline(re"f", out1)
+}
+
+func f(x, y int) {
+ z := 1
+ print(x + y + z)
+}
+
+-- out1 --
+package a
+
+func _() {
+ z := 1
+ {
+ var x int = z
+ z := 1
+ print(x + 2 + z)
+ } //@ inline(re"f", out1)
+}
+
+func f(x, y int) {
+ z := 1
+ print(x + y + z)
+}
+
+-- a/a2.go --
+package a
+
+func _() {
+ a := 1
+ f(a, 2) //@ inline(re"f", out2)
+}
+
+-- out2 --
+package a
+
+func _() {
+ a := 1
+
+ z := 1
+ print(a + 2 + z)
+ //@ inline(re"f", out2)
+}
+
+-- a/a3.go --
+package a
+
+func _() {
+ a := 1
+ g(a, 2) //@ inline(re"g", out3)
+}
+
+func g(x, y int) int {
+ z := 1
+ return x + y + z
+}
+
+-- out3 --
+package a
+
+func _() {
+ a := 1
+ func() int { z := 1; return a + 2 + z }() //@ inline(re"g", out3)
+}
+
+func g(x, y int) int {
+ z := 1
+ return x + y + z
+}
diff --git a/internal/refactor/inline/testdata/param-subst.txtar b/internal/refactor/inline/testdata/param-subst.txtar
new file mode 100644
index 00000000000..b6e462d7e71
--- /dev/null
+++ b/internal/refactor/inline/testdata/param-subst.txtar
@@ -0,0 +1,19 @@
+Test of parameter substitution.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a0.go --
+package a
+
+var _ = add(2, 1+1) //@ inline(re"add", add)
+
+func add(x, y int) int { return x + 2*y }
+
+-- add --
+package a
+
+var _ = 2 + 2*(1+1) //@ inline(re"add", add)
+
+func add(x, y int) int { return x + 2*y }
\ No newline at end of file
diff --git a/internal/refactor/inline/testdata/revdotimport.txtar b/internal/refactor/inline/testdata/revdotimport.txtar
index f8b895e9218..3838793754d 100644
--- a/internal/refactor/inline/testdata/revdotimport.txtar
+++ b/internal/refactor/inline/testdata/revdotimport.txtar
@@ -33,11 +33,10 @@ package c
import (
. "testdata/a"
-
a "testdata/a"
)
func _() {
A()
- func() { a.A() }() //@ inline(re"B", result)
+ a.A() //@ inline(re"B", result)
}
diff --git a/internal/refactor/inline/testdata/std-internal.txtar b/internal/refactor/inline/testdata/std-internal.txtar
new file mode 100644
index 00000000000..460cdacd604
--- /dev/null
+++ b/internal/refactor/inline/testdata/std-internal.txtar
@@ -0,0 +1,15 @@
+
+std packages are a special case of the internal package check.
+
+This test assumes that strings.Index refers to internal/bytealg.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+import "strings"
+
+var _ = strings.Index("", "") //@ inline(re"Index", re`inaccessible package "internal/bytealg"`)
diff --git a/internal/refactor/inline/testdata/tailcall.txtar b/internal/refactor/inline/testdata/tailcall.txtar
new file mode 100644
index 00000000000..53b6de367dd
--- /dev/null
+++ b/internal/refactor/inline/testdata/tailcall.txtar
@@ -0,0 +1,122 @@
+Reduction of parameterless tail-call to functions.
+
+1. a0 (sum) is reduced, despite the complexity of the callee.
+
+2. a1 (conflict) is not reduced, because the caller and callee have
+ intersecting sets of labels.
+
+3. a2 (usesResult) is not reduced, because it refers to a result variable.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a0.go --
+package a
+
+func _() int {
+ return sum(1, 2) //@ inline(re"sum", sum)
+}
+
+func sum(lo, hi int) int {
+ total := 0
+start:
+ for i := lo; i <= hi; i++ {
+ total += i
+ if i == 6 {
+ goto start
+ } else if i == 7 {
+ return -1
+ }
+ }
+ return total
+}
+
+-- sum --
+package a
+
+func _() int {
+
+ total := 0
+start:
+ for i := 1; i <= 2; i++ {
+ total += i
+ if i == 6 {
+ goto start
+ } else if i == 7 {
+ return -1
+ }
+ }
+ return total
+ //@ inline(re"sum", sum)
+}
+
+func sum(lo, hi int) int {
+ total := 0
+start:
+ for i := lo; i <= hi; i++ {
+ total += i
+ if i == 6 {
+ goto start
+ } else if i == 7 {
+ return -1
+ }
+ }
+ return total
+}
+
+-- a/a1.go --
+package a
+
+func _() int {
+ hello:
+ return conflict(1, 2) //@ inline(re"conflict", conflict)
+ goto hello
+}
+
+func conflict(lo, hi int) int {
+hello:
+ return lo + hi
+}
+
+-- conflict --
+package a
+
+func _() int {
+hello:
+ return func() int {
+ hello:
+ return 1 + 2
+ }() //@ inline(re"conflict", conflict)
+ goto hello
+}
+
+func conflict(lo, hi int) int {
+hello:
+ return lo + hi
+}
+
+-- a/a2.go --
+package a
+
+func _() int {
+ return usesResult(1, 2) //@ inline(re"usesResult", usesResult)
+}
+
+func usesResult(lo, hi int) (z int) {
+ z = y + x
+ return
+}
+
+-- usesResult --
+package a
+
+func _() int {
+ return func() (z int) { z = y + x; return }() //@ inline(re"usesResult", usesResult)
+}
+
+func usesResult(lo, hi int) (z int) {
+ z = y + x
+ return
+}
+
diff --git a/internal/refactor/inline/util.go b/internal/refactor/inline/util.go
new file mode 100644
index 00000000000..98d654eeb51
--- /dev/null
+++ b/internal/refactor/inline/util.go
@@ -0,0 +1,119 @@
+// 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 inline
+
+// This file defines various common helpers.
+
+import (
+ "go/ast"
+ "go/token"
+ "go/types"
+ "reflect"
+ "strings"
+)
+
+func is[T any](x any) bool {
+ _, ok := x.(T)
+ return ok
+}
+
+// TODO(adonovan): use go1.21's slices.Clone.
+func clone[T any](slice []T) []T { return append([]T{}, slice...) }
+
+// TODO(adonovan): use go1.21's slices.Index.
+func index[T comparable](slice []T, x T) int {
+ for i, elem := range slice {
+ if elem == x {
+ return i
+ }
+ }
+ return -1
+}
+
+func btoi(b bool) int {
+ if b {
+ return 1
+ } else {
+ return 0
+ }
+}
+
+func offsetOf(fset *token.FileSet, pos token.Pos) int {
+ return fset.PositionFor(pos, false).Offset
+}
+
+// objectKind returns an object's kind (e.g. var, func, const, typename).
+func objectKind(obj types.Object) string {
+ return strings.TrimPrefix(strings.ToLower(reflect.TypeOf(obj).String()), "*types.")
+}
+
+// within reports whether pos is within the half-open interval [n.Pos, n.End).
+func within(pos token.Pos, n ast.Node) bool {
+ return n.Pos() <= pos && pos < n.End()
+}
+
+// trivialConversion reports whether it is safe to omit the implicit
+// value-to-variable conversion that occurs in argument passing or
+// result return. The only case currently allowed is converting from
+// untyped constant to its default type (e.g. 0 to int).
+//
+// The reason for this check is that converting from A to B to C may
+// yield a different result than converting A directly to C: consider
+// 0 to int32 to any.
+func trivialConversion(val types.Type, obj *types.Var) bool {
+ return types.Identical(types.Default(val), obj.Type())
+}
+
+func checkInfoFields(info *types.Info) {
+ assert(info.Defs != nil, "types.Info.Defs is nil")
+ assert(info.Implicits != nil, "types.Info.Implicits is nil")
+ assert(info.Scopes != nil, "types.Info.Scopes is nil")
+ assert(info.Selections != nil, "types.Info.Selections is nil")
+ assert(info.Types != nil, "types.Info.Types is nil")
+ assert(info.Uses != nil, "types.Info.Uses is nil")
+}
+
+func funcHasTypeParams(decl *ast.FuncDecl) bool {
+ // generic function?
+ if decl.Type.TypeParams != nil {
+ return true
+ }
+ // method on generic type?
+ if decl.Recv != nil {
+ t := decl.Recv.List[0].Type
+ if u, ok := t.(*ast.StarExpr); ok {
+ t = u.X
+ }
+ return is[*ast.IndexExpr](t) || is[*ast.IndexListExpr](t)
+ }
+ return false
+}
+
+// intersects reports whether the maps' key sets intersect.
+func intersects[K comparable, T1, T2 any](x map[K]T1, y map[K]T2) bool {
+ if len(x) > len(y) {
+ return intersects(y, x)
+ }
+ for k := range x {
+ if _, ok := y[k]; ok {
+ return true
+ }
+ }
+ return false
+}
+
+// convert returns syntax for the conversion T(x).
+func convert(T, x ast.Expr) *ast.CallExpr {
+ // The formatter generally adds parens as needed,
+ // but before go1.22 it had a bug (#63362) for
+ // channel types that requires this workaround.
+ if ch, ok := T.(*ast.ChanType); ok && ch.Dir == ast.RECV {
+ T = &ast.ParenExpr{X: T}
+ }
+ return &ast.CallExpr{
+ Fun: T,
+ Args: []ast.Expr{x},
+ }
+}
diff --git a/internal/robustio/copyfiles.go b/internal/robustio/copyfiles.go
index 6e9f4b3875f..8c93fcd7163 100644
--- a/internal/robustio/copyfiles.go
+++ b/internal/robustio/copyfiles.go
@@ -53,7 +53,7 @@ func main() {
content = bytes.Replace(content, []byte("windows.ERROR_SHARING_VIOLATION"), []byte("ERROR_SHARING_VIOLATION"), -1)
}
- // Replace os.ReadFile with ioutil.ReadFile (for 1.15 and older). We
+ // Replace os.ReadFile with os.ReadFile (for 1.15 and older). We
// attempt to match calls (via the '('), to avoid matching mentions of
// os.ReadFile in comments.
//
@@ -61,7 +61,7 @@ func main() {
// this and break the build.
if bytes.Contains(content, []byte("os.ReadFile(")) {
content = bytes.Replace(content, []byte("\"os\""), []byte("\"io/ioutil\"\n\t\"os\""), 1)
- content = bytes.Replace(content, []byte("os.ReadFile("), []byte("ioutil.ReadFile("), -1)
+ content = bytes.Replace(content, []byte("os.ReadFile("), []byte("os.ReadFile("), -1)
}
// Add +build constraints, for 1.16.
diff --git a/internal/robustio/robustio_flaky.go b/internal/robustio/robustio_flaky.go
index c6f99724468..d5c241857b4 100644
--- a/internal/robustio/robustio_flaky.go
+++ b/internal/robustio/robustio_flaky.go
@@ -9,7 +9,6 @@ package robustio
import (
"errors"
- "io/ioutil"
"math/rand"
"os"
"syscall"
@@ -75,7 +74,7 @@ func rename(oldpath, newpath string) (err error) {
func readFile(filename string) ([]byte, error) {
var b []byte
err := retry(func() (err error, mayRetry bool) {
- b, err = ioutil.ReadFile(filename)
+ b, err = os.ReadFile(filename)
// Unlike in rename, we do not retry errFileNotFound here: it can occur
// as a spurious error, but the file may also genuinely not exist, so the
diff --git a/internal/robustio/robustio_other.go b/internal/robustio/robustio_other.go
index c11dbf9f14b..3a20cac6cf8 100644
--- a/internal/robustio/robustio_other.go
+++ b/internal/robustio/robustio_other.go
@@ -8,7 +8,6 @@
package robustio
import (
- "io/ioutil"
"os"
)
@@ -17,7 +16,7 @@ func rename(oldpath, newpath string) error {
}
func readFile(filename string) ([]byte, error) {
- return ioutil.ReadFile(filename)
+ return os.ReadFile(filename)
}
func removeAll(path string) error {
diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go
index 0fe217b3c16..4d29ebe7f72 100644
--- a/internal/testenv/testenv.go
+++ b/internal/testenv/testenv.go
@@ -10,7 +10,6 @@ import (
"bytes"
"fmt"
"go/build"
- "io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -67,7 +66,7 @@ func hasTool(tool string) error {
switch tool {
case "patch":
// check that the patch tools supports the -o argument
- temp, err := ioutil.TempFile("", "patch-test")
+ temp, err := os.CreateTemp("", "patch-test")
if err != nil {
return err
}
@@ -360,7 +359,7 @@ func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[str
if err != nil {
t.Fatalf("preparing the importcfg failed: %s", err)
}
- ioutil.WriteFile(dstPath, []byte(importcfg), 0655)
+ os.WriteFile(dstPath, []byte(importcfg), 0655)
if err != nil {
t.Fatalf("writing the importcfg failed: %s", err)
}
diff --git a/playground/socket/socket.go b/playground/socket/socket.go
index cdc665316d4..c396aac5196 100644
--- a/playground/socket/socket.go
+++ b/playground/socket/socket.go
@@ -22,7 +22,6 @@ import (
"go/token"
exec "golang.org/x/sys/execabs"
"io"
- "io/ioutil"
"log"
"net"
"net/http"
@@ -356,7 +355,7 @@ func (p *process) start(body string, opt *Options) error {
// (rather than the go tool process).
// This makes Kill work.
- path, err := ioutil.TempDir("", "present-")
+ path, err := os.MkdirTemp("", "present-")
if err != nil {
return err
}
@@ -376,7 +375,7 @@ func (p *process) start(body string, opt *Options) error {
}
hasModfile := false
for _, f := range a.Files {
- err = ioutil.WriteFile(filepath.Join(path, f.Name), f.Data, 0666)
+ err = os.WriteFile(filepath.Join(path, f.Name), f.Data, 0666)
if err != nil {
return err
}
diff --git a/present/parse.go b/present/parse.go
index 4294ea5f9cc..162a382b060 100644
--- a/present/parse.go
+++ b/present/parse.go
@@ -11,9 +11,9 @@ import (
"fmt"
"html/template"
"io"
- "io/ioutil"
"log"
"net/url"
+ "os"
"regexp"
"strings"
"time"
@@ -342,9 +342,9 @@ func (ctx *Context) Parse(r io.Reader, name string, mode ParseMode) (*Doc, error
}
// Parse parses a document from r. Parse reads assets used by the presentation
-// from the file system using ioutil.ReadFile.
+// from the file system using os.ReadFile.
func Parse(r io.Reader, name string, mode ParseMode) (*Doc, error) {
- ctx := Context{ReadFile: ioutil.ReadFile}
+ ctx := Context{ReadFile: os.ReadFile}
return ctx.Parse(r, name, mode)
}
diff --git a/present/parse_test.go b/present/parse_test.go
index 18d1a35080d..0e59857a3a0 100644
--- a/present/parse_test.go
+++ b/present/parse_test.go
@@ -7,7 +7,6 @@ package present
import (
"bytes"
"html/template"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -33,7 +32,7 @@ func TestTestdata(t *testing.T) {
continue
}
t.Run(name, func(t *testing.T) {
- data, err := ioutil.ReadFile(file)
+ data, err := os.ReadFile(file)
if err != nil {
t.Fatalf("%s: %v", file, err)
}
@@ -94,7 +93,7 @@ func diff(prefix string, name1 string, b1 []byte, name2 string, b2 []byte) ([]by
}
func writeTempFile(prefix string, data []byte) (string, error) {
- file, err := ioutil.TempFile("", prefix)
+ file, err := os.CreateTemp("", prefix)
if err != nil {
return "", err
}
diff --git a/refactor/eg/eg_test.go b/refactor/eg/eg_test.go
index 438e6b75e47..4154e9a8f4e 100644
--- a/refactor/eg/eg_test.go
+++ b/refactor/eg/eg_test.go
@@ -16,7 +16,6 @@ import (
"go/parser"
"go/token"
"go/types"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -137,7 +136,7 @@ func Test(t *testing.T) {
continue
}
- gotf, err := ioutil.TempFile("", filepath.Base(filename)+"t")
+ gotf, err := os.CreateTemp("", filepath.Base(filename)+"t")
if err != nil {
t.Fatal(err)
}
diff --git a/refactor/rename/mvpkg_test.go b/refactor/rename/mvpkg_test.go
index b8b4d85da4d..f201ee85aa8 100644
--- a/refactor/rename/mvpkg_test.go
+++ b/refactor/rename/mvpkg_test.go
@@ -8,7 +8,7 @@ import (
"fmt"
"go/build"
"go/token"
- "io/ioutil"
+ "io"
"path/filepath"
"reflect"
"regexp"
@@ -387,7 +387,7 @@ var _ foo.T
t.Errorf("unexpected error opening file: %s", err)
return
}
- bytes, err := ioutil.ReadAll(f)
+ bytes, err := io.ReadAll(f)
f.Close()
if err != nil {
t.Errorf("unexpected error reading file: %s", err)
diff --git a/refactor/rename/rename.go b/refactor/rename/rename.go
index e74e0a64024..a80381c84b1 100644
--- a/refactor/rename/rename.go
+++ b/refactor/rename/rename.go
@@ -19,7 +19,6 @@ import (
"go/types"
exec "golang.org/x/sys/execabs"
"io"
- "io/ioutil"
"log"
"os"
"path"
@@ -579,12 +578,12 @@ func plural(n int) string {
var writeFile = reallyWriteFile
func reallyWriteFile(filename string, content []byte) error {
- return ioutil.WriteFile(filename, content, 0644)
+ return os.WriteFile(filename, content, 0644)
}
func diff(filename string, content []byte) error {
renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid())
- if err := ioutil.WriteFile(renamed, content, 0644); err != nil {
+ if err := os.WriteFile(renamed, content, 0644); err != nil {
return err
}
defer os.Remove(renamed)
diff --git a/refactor/rename/rename_test.go b/refactor/rename/rename_test.go
index 3dfdc18967c..38c59c9d448 100644
--- a/refactor/rename/rename_test.go
+++ b/refactor/rename/rename_test.go
@@ -9,7 +9,6 @@ import (
"fmt"
"go/build"
"go/token"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -1302,7 +1301,7 @@ func TestDiff(t *testing.T) {
// Set up a fake GOPATH in a temporary directory,
// and ensure we're in GOPATH mode.
- tmpdir, err := ioutil.TempDir("", "TestDiff")
+ tmpdir, err := os.MkdirTemp("", "TestDiff")
if err != nil {
t.Fatal(err)
}
@@ -1329,7 +1328,7 @@ func TestDiff(t *testing.T) {
go 1.15
`
- if err := ioutil.WriteFile(filepath.Join(pkgDir, "go.mod"), []byte(modFile), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(pkgDir, "go.mod"), []byte(modFile), 0644); err != nil {
t.Fatal(err)
}
@@ -1339,7 +1338,7 @@ func justHereForTestingDiff() {
justHereForTestingDiff()
}
`
- if err := ioutil.WriteFile(filepath.Join(pkgDir, "rename_test.go"), []byte(goFile), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(pkgDir, "rename_test.go"), []byte(goFile), 0644); err != nil {
t.Fatal(err)
}
diff --git a/txtar/archive.go b/txtar/archive.go
index 81b31454512..fd95f1e64a1 100644
--- a/txtar/archive.go
+++ b/txtar/archive.go
@@ -34,7 +34,7 @@ package txtar
import (
"bytes"
"fmt"
- "io/ioutil"
+ "os"
"strings"
)
@@ -66,7 +66,7 @@ func Format(a *Archive) []byte {
// ParseFile parses the named file as an archive.
func ParseFile(file string) (*Archive, error) {
- data, err := ioutil.ReadFile(file)
+ data, err := os.ReadFile(file)
if err != nil {
return nil, err
}