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="..."> ... ). +// // - 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("", 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" +) + +import _ "os" + +// bar is a function.<1 kind="comment"> +// With a multiline doc comment. +func bar(<2 kind="">) 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") + } else {<8 kind=""> + fmt.Println(<9 kind="">"false") + } + case false:<10 kind=""> + fmt.Println(<11 kind="">"false") + default:<12 kind=""> + fmt.Println(<13 kind="">"default") + } + /* This is a multiline<14 kind="comment"> + block + comment */ + + /* This is a multiline<15 kind="comment"> + block + comment */ + // Followed by another comment. + _ = []int{<16 kind=""> + 1, + 2, + 3, + } + _ = [2]string{<17 kind="">"d", + "e", + } + _ = map[string]int{<18 kind=""> + "a": 1, + "b": 2, + "c": 3, + } + type T struct {<19 kind=""> + f string + g int + h string + } + _ = T{<20 kind=""> + f: "j", + g: 4, + h: "i", + } + x, y := make(<21 kind="">chan bool), make(<22 kind="">chan bool) + select {<23 kind=""> + case val := <-x:<24 kind=""> + if val {<25 kind=""> + fmt.Println(<26 kind="">"true from x") + } else {<27 kind=""> + fmt.Println(<28 kind="">"false from x") + } + case <-y:<29 kind=""> + fmt.Println(<30 kind="">"y") + default:<31 kind=""> + fmt.Println(<32 kind="">"default") + } + // This is a multiline comment<33 kind="comment"> + // that is not a doc comment. + return <34 kind="">` +this string +is not indented` +} 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" +) + +import _ "os" + +// bar is a function.<1 kind="comment"> +// With a multiline doc comment. +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") + } else {<6 kind=""> + fmt.Println("false") + } + case false:<7 kind=""> + fmt.Println("false") + default:<8 kind=""> + fmt.Println("default") + } + /* This is a multiline<9 kind="comment"> + block + comment */ + + /* This is a multiline<10 kind="comment"> + block + comment */ + // Followed by another comment. + _ = []int{<11 kind=""> + 1, + 2, + 3, + } + _ = [2]string{"d", + "e", + } + _ = map[string]int{<12 kind=""> + "a": 1, + "b": 2, + "c": 3, + } + type T struct {<13 kind=""> + f string + g int + h string + } + _ = T{<14 kind=""> + f: "j", + g: 4, + h: "i", + } + 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") + } else {<18 kind=""> + fmt.Println("false from x") + } + case <-y:<19 kind=""> + fmt.Println("y") + default:<20 kind=""> + fmt.Println("default") + } + // This is a multiline comment<21 kind="comment"> + // that is not a doc comment. + return <22 kind="">` +this string +is not indented` +} 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" +) + +import (<1 kind="imports"> + _ "os" ) + +// badBar is a function. +func badBar(<2 kind="">) 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") + } else {<6 kind=""> + fmt.Println(<7 kind="">"false") } + return "" +} 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 }