diff --git a/cmd/bisect/go119.go b/cmd/bisect/go119.go deleted file mode 100644 index debe4e0c253..00000000000 --- a/cmd/bisect/go119.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.20 - -package main - -import "os/exec" - -func cmdInterrupt(cmd *exec.Cmd) { - // cmd.Cancel and cmd.WaitDelay not available before Go 1.20. -} diff --git a/cmd/bisect/go120.go b/cmd/bisect/go120.go index c85edf7b575..d2cf382684d 100644 --- a/cmd/bisect/go120.go +++ b/cmd/bisect/go120.go @@ -2,8 +2,6 @@ // 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/cmd/bisect/rand.go b/cmd/bisect/rand.go deleted file mode 100644 index daa01d3b442..00000000000 --- a/cmd/bisect/rand.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Starting in Go 1.20, the global rand is auto-seeded, -// with a better value than the current Unix nanoseconds. -// Only seed if we're using older versions of Go. - -//go:build !go1.20 - -package main - -import ( - "math/rand" - "time" -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} diff --git a/cmd/bundle/main.go b/cmd/bundle/main.go index fa73eb83a0a..a4831c78776 100644 --- a/cmd/bundle/main.go +++ b/cmd/bundle/main.go @@ -68,6 +68,9 @@ // Update all bundles in the standard library: // // go generate -run bundle std + +//go:debug gotypesalias=0 + package main import ( diff --git a/cmd/callgraph/main.go b/cmd/callgraph/main.go index 4443f172f7e..1b5af1b52e1 100644 --- a/cmd/callgraph/main.go +++ b/cmd/callgraph/main.go @@ -4,6 +4,9 @@ // callgraph: a tool for reporting the call graph of a Go program. // See Usage for details, or run with -help. + +//go:debug gotypesalias=0 + package main // import "golang.org/x/tools/cmd/callgraph" // TODO(adonovan): @@ -23,14 +26,12 @@ import ( "bytes" "flag" "fmt" - "go/build" "go/token" "io" "os" "runtime" "text/template" - "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/cha" "golang.org/x/tools/go/callgraph/rta" @@ -52,11 +53,9 @@ var ( formatFlag = flag.String("format", "{{.Caller}}\t--{{.Dynamic}}-{{.Line}}:{{.Column}}-->\t{{.Callee}}", "A template expression specifying how to format an edge") -) -func init() { - flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) -} + tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)") +) const Usage = `callgraph: display the call graph of a Go program. @@ -177,9 +176,10 @@ func doCallgraph(dir, gopath, algo, format string, tests bool, args []string) er } cfg := &packages.Config{ - Mode: packages.LoadAllSyntax, - Tests: tests, - Dir: dir, + Mode: packages.LoadAllSyntax, + BuildFlags: []string{"-tags=" + *tagsFlag}, + Tests: tests, + Dir: dir, } if gopath != "" { cfg.Env = append(os.Environ(), "GOPATH="+gopath) // to enable testing diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index e0b4de32387..e6f32bb9979 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.20 +//go:debug gotypesalias=0 package main diff --git a/cmd/deadcode/deadcode_test.go b/cmd/deadcode/deadcode_test.go index f17a1227362..90c067331dc 100644 --- a/cmd/deadcode/deadcode_test.go +++ b/cmd/deadcode/deadcode_test.go @@ -2,8 +2,6 @@ // 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/cmd/eg/eg.go b/cmd/eg/eg.go index 108b9e3009f..07e73d2efe7 100644 --- a/cmd/eg/eg.go +++ b/cmd/eg/eg.go @@ -5,6 +5,9 @@ // The eg command performs example-based refactoring. // For documentation, run the command, or see Help in // golang.org/x/tools/refactor/eg. + +//go:debug gotypesalias=0 + package main // import "golang.org/x/tools/cmd/eg" import ( diff --git a/cmd/godex/godex.go b/cmd/godex/godex.go index e91dbfcea5f..4955600f2d6 100644 --- a/cmd/godex/godex.go +++ b/cmd/godex/godex.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:debug gotypesalias=0 + package main import ( diff --git a/cmd/godex/print.go b/cmd/godex/print.go index da3b2f04e0b..57383e0e7ec 100644 --- a/cmd/godex/print.go +++ b/cmd/godex/print.go @@ -12,8 +12,6 @@ import ( "go/types" "io" "math/big" - - "golang.org/x/tools/internal/aliases" ) // TODO(gri) use tabwriter for alignment? @@ -58,7 +56,7 @@ func (p *printer) printf(format string, args ...interface{}) { // denoted by obj is not an interface and has methods. Otherwise it returns // the zero value. func methodsFor(obj *types.TypeName) (*types.Named, []*types.Selection) { - named, _ := aliases.Unalias(obj.Type()).(*types.Named) + named, _ := types.Unalias(obj.Type()).(*types.Named) if named == nil { // A type name's type can also be the // exported basic type unsafe.Pointer. diff --git a/cmd/godex/writetype.go b/cmd/godex/writetype.go index 6ae365d13a3..bfe36977892 100644 --- a/cmd/godex/writetype.go +++ b/cmd/godex/writetype.go @@ -14,8 +14,6 @@ package main import ( "go/types" - - "golang.org/x/tools/internal/aliases" ) func (p *printer) writeType(this *types.Package, typ types.Type) { @@ -177,9 +175,9 @@ func (p *printer) writeTypeInternal(this *types.Package, typ types.Type, visited p.print(")") } - case *aliases.Alias: + case *types.Alias: // TODO(adonovan): display something aliasy. - p.writeTypeInternal(this, aliases.Unalias(t), visited) + p.writeTypeInternal(this, types.Unalias(t), visited) case *types.Named: s := "" diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go index a665be0769d..1c874cc0e15 100644 --- a/cmd/godoc/main.go +++ b/cmd/godoc/main.go @@ -14,6 +14,8 @@ // http://godoc/pkg/compress/zlib) // +//go:debug gotypesalias=0 + package main import ( diff --git a/cmd/goimports/goimports.go b/cmd/goimports/goimports.go index dcb5023a2e7..7463e641e95 100644 --- a/cmd/goimports/goimports.go +++ b/cmd/goimports/goimports.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:debug gotypesalias=0 + package main import ( diff --git a/cmd/gomvpkg/main.go b/cmd/gomvpkg/main.go index 5de1e44062d..d54b7070dec 100644 --- a/cmd/gomvpkg/main.go +++ b/cmd/gomvpkg/main.go @@ -4,6 +4,9 @@ // The gomvpkg command moves go packages, updating import declarations. // See the -help message or Usage constant for details. + +//go:debug gotypesalias=0 + package main import ( diff --git a/cmd/gorename/gorename_test.go b/cmd/gorename/gorename_test.go deleted file mode 100644 index f72b6f4a429..00000000000 --- a/cmd/gorename/gorename_test.go +++ /dev/null @@ -1,383 +0,0 @@ -// Copyright 2017 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 main_test - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "testing" - - "golang.org/x/tools/internal/testenv" -) - -type test struct { - offset, from, to string // specify the arguments - fileSpecified bool // true if the offset or from args specify a specific file - pkgs map[string][]string - wantErr bool - wantOut string // a substring expected to be in the output - packages map[string][]string // a map of the package name to the files contained within, which will be numbered by i.go where i is the index -} - -// Test that renaming that would modify cgo files will produce an error and not modify the file. -func TestGeneratedFiles(t *testing.T) { - testenv.NeedsTool(t, "go") - testenv.NeedsTool(t, "cgo") - - tmp, bin, cleanup := buildGorename(t) - defer cleanup() - - srcDir := filepath.Join(tmp, "src") - err := os.Mkdir(srcDir, os.ModePerm) - if err != nil { - t.Fatal(err) - } - - var env = []string{fmt.Sprintf("GOPATH=%s", tmp)} - for _, envVar := range os.Environ() { - if !strings.HasPrefix(envVar, "GOPATH=") { - env = append(env, envVar) - } - } - // gorename currently requires GOPATH mode. - env = append(env, "GO111MODULE=off") - - // Testing renaming in packages that include cgo files: - for iter, renameTest := range []test{ - { - // Test: variable not used in any cgo file -> no error - from: `"mytest"::f`, to: "g", - packages: map[string][]string{ - "mytest": []string{`package mytest; func f() {}`, - `package mytest -// #include -import "C" - -func z() {C.puts(nil)}`}, - }, - wantErr: false, - wantOut: "Renamed 1 occurrence in 1 file in 1 package.", - }, { - // Test: to name used in cgo file -> rename error - from: `"mytest"::f`, to: "g", - packages: map[string][]string{ - "mytest": []string{`package mytest; func f() {}`, - `package mytest -// #include -import "C" - -func g() {C.puts(nil)}`}, - }, - wantErr: true, - wantOut: "conflicts with func in same block", - }, - { - // Test: from name in package in cgo file -> error - from: `"mytest"::f`, to: "g", - packages: map[string][]string{ - "mytest": []string{`package mytest - -// #include -import "C" - -func f() { C.puts(nil); } -`}, - }, - wantErr: true, - wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:", - }, { - // Test: from name in cgo file -> error - from: filepath.Join("mytest", "0.go") + `::f`, to: "g", - fileSpecified: true, - packages: map[string][]string{ - "mytest": []string{`package mytest - -// #include -import "C" - -func f() { C.puts(nil); } -`}, - }, - wantErr: true, - wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:", - }, { - // Test: offset in cgo file -> identifier in cgo error - offset: filepath.Join("main", "0.go") + `:#78`, to: "bar", - fileSpecified: true, - wantErr: true, - packages: map[string][]string{ - "main": {`package main - -// #include -import "C" -import "fmt" - -func main() { - foo := 1 - C.close(2) - fmt.Println(foo) -} -`}, - }, - wantOut: "cannot rename identifiers in generated file containing DO NOT EDIT marker:", - }, { - // Test: from identifier appears in cgo file in another package -> error - from: `"test"::Foo`, to: "Bar", - packages: map[string][]string{ - "test": []string{ - `package test - -func Foo(x int) (int){ - return x * 2 -} -`, - }, - "main": []string{ - `package main - -import "test" -import "fmt" -// #include -import "C" - -func fun() { - x := test.Foo(3) - C.close(3) - fmt.Println(x) -} -`, - }, - }, - wantErr: true, - wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:", - }, { - // Test: from identifier doesn't appear in cgo file that includes modified package -> rename successful - from: `"test".Foo::x`, to: "y", - packages: map[string][]string{ - "test": []string{ - `package test - -func Foo(x int) (int){ - return x * 2 -} -`, - }, - "main": []string{ - `package main -import "test" -import "fmt" -// #include -import "C" - -func fun() { - x := test.Foo(3) - C.close(3) - fmt.Println(x) -} -`, - }, - }, - wantErr: false, - wantOut: "Renamed 2 occurrences in 1 file in 1 package.", - }, { - // Test: from name appears in cgo file in same package -> error - from: `"mytest"::f`, to: "g", - packages: map[string][]string{ - "mytest": []string{`package mytest; func f() {}`, - `package mytest -// #include -import "C" - -func z() {C.puts(nil); f()}`, - `package mytest -// #include -import "C" - -func foo() {C.close(3); f()}`, - }, - }, - wantErr: true, - wantOut: "gorename: refusing to modify generated files containing DO NOT EDIT marker:", - }, { - // Test: from name in file, identifier not used in cgo file -> rename successful - from: filepath.Join("mytest", "0.go") + `::f`, to: "g", - fileSpecified: true, - packages: map[string][]string{ - "mytest": []string{`package mytest; func f() {}`, - `package mytest -// #include -import "C" - -func z() {C.puts(nil)}`}, - }, - wantErr: false, - wantOut: "Renamed 1 occurrence in 1 file in 1 package.", - }, { - // Test: from identifier imported to another package but does not modify cgo file -> rename successful - from: `"test".Foo`, to: "Bar", - packages: map[string][]string{ - "test": []string{ - `package test - -func Foo(x int) (int){ - return x * 2 -} -`, - }, - "main": []string{ - `package main -// #include -import "C" - -func fun() { - C.close(3) -} -`, - `package main -import "test" -import "fmt" -func g() { fmt.Println(test.Foo(3)) } -`, - }, - }, - wantErr: false, - wantOut: "Renamed 2 occurrences in 2 files in 2 packages.", - }, - } { - // Write the test files - testCleanup := setUpPackages(t, srcDir, renameTest.packages) - - // Set up arguments - var args []string - - var arg, val string - if renameTest.offset != "" { - arg, val = "-offset", renameTest.offset - } else { - arg, val = "-from", renameTest.from - } - - prefix := fmt.Sprintf("%d: %s %q -to %q", iter, arg, val, renameTest.to) - - if renameTest.fileSpecified { - // add the src dir to the value of the argument - val = filepath.Join(srcDir, val) - } - - args = append(args, arg, val, "-to", renameTest.to) - - // Run command - cmd := exec.Command(bin, args...) - cmd.Args[0] = "gorename" - cmd.Env = env - - // Check the output - out, err := cmd.CombinedOutput() - // errors should result in no changes to files - if err != nil { - if !renameTest.wantErr { - t.Errorf("%s: received unexpected error %s", prefix, err) - } - // Compare output - if ok := strings.Contains(string(out), renameTest.wantOut); !ok { - t.Errorf("%s: unexpected command output: %s (want: %s)", prefix, out, renameTest.wantOut) - } - // Check that no files were modified - if modified := modifiedFiles(t, srcDir, renameTest.packages); len(modified) != 0 { - t.Errorf("%s: files unexpectedly modified: %s", prefix, modified) - } - - } else { - if !renameTest.wantErr { - if ok := strings.Contains(string(out), renameTest.wantOut); !ok { - t.Errorf("%s: unexpected command output: %s (want: %s)", prefix, out, renameTest.wantOut) - } - } else { - t.Errorf("%s: command succeeded unexpectedly, output: %s", prefix, out) - } - } - testCleanup() - } -} - -// buildGorename builds the gorename executable. -// It returns its path, and a cleanup function. -func buildGorename(t *testing.T) (tmp, bin string, cleanup func()) { - if runtime.GOOS == "android" { - t.Skipf("the dependencies are not available on android") - } - - tmp, err := os.MkdirTemp("", "gorename-regtest-") - if err != nil { - t.Fatal(err) - } - - defer func() { - if cleanup == nil { // probably, go build failed. - os.RemoveAll(tmp) - } - }() - - bin = filepath.Join(tmp, "gorename") - if runtime.GOOS == "windows" { - bin += ".exe" - } - cmd := exec.Command("go", "build", "-o", bin) - if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Building gorename: %v\n%s", err, out) - } - return tmp, bin, func() { os.RemoveAll(tmp) } -} - -// setUpPackages sets up the files in a temporary directory provided by arguments. -func setUpPackages(t *testing.T, dir string, packages map[string][]string) (cleanup func()) { - var pkgDirs []string - - for pkgName, files := range packages { - // Create a directory for the package. - pkgDir := filepath.Join(dir, pkgName) - pkgDirs = append(pkgDirs, pkgDir) - - if err := os.Mkdir(pkgDir, os.ModePerm); err != nil { - t.Fatal(err) - } - // Write the packages files - for i, val := range files { - file := filepath.Join(pkgDir, strconv.Itoa(i)+".go") - if err := os.WriteFile(file, []byte(val), os.ModePerm); err != nil { - t.Fatal(err) - } - } - } - return func() { - for _, dir := range pkgDirs { - os.RemoveAll(dir) - } - } -} - -// modifiedFiles returns a list of files that were renamed (without the prefix dir). -func modifiedFiles(t *testing.T, dir string, packages map[string][]string) (results []string) { - - for pkgName, files := range packages { - pkgDir := filepath.Join(dir, pkgName) - - for i, val := range files { - file := filepath.Join(pkgDir, strconv.Itoa(i)+".go") - // read file contents and compare to val - 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)) - } - } - } - return results -} diff --git a/cmd/gorename/main.go b/cmd/gorename/main.go deleted file mode 100644 index 98625fff669..00000000000 --- a/cmd/gorename/main.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2014 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 gorename command performs precise type-safe renaming of -// identifiers in Go source code. -// -// Run with -help for usage information, or view the Usage constant in -// package golang.org/x/tools/refactor/rename, which contains most of -// the implementation. -package main // import "golang.org/x/tools/cmd/gorename" - -import ( - "flag" - "fmt" - "go/build" - "log" - "os" - - "golang.org/x/tools/go/buildutil" - "golang.org/x/tools/refactor/rename" -) - -var ( - offsetFlag = flag.String("offset", "", "file and byte offset of identifier to be renamed, e.g. 'file.go:#123'. For use by editors.") - fromFlag = flag.String("from", "", "identifier to be renamed; see -help for formats") - toFlag = flag.String("to", "", "new name for identifier") - helpFlag = flag.Bool("help", false, "show usage message") -) - -func init() { - flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) - flag.BoolVar(&rename.Force, "force", false, "proceed, even if conflicts were reported") - flag.BoolVar(&rename.Verbose, "v", false, "print verbose information") - flag.BoolVar(&rename.Diff, "d", false, "display diffs instead of rewriting files") - flag.StringVar(&rename.DiffCmd, "diffcmd", "diff", "diff command invoked when using -d") -} - -func main() { - log.SetPrefix("gorename: ") - log.SetFlags(0) - flag.Parse() - if len(flag.Args()) > 0 { - log.Fatal("surplus arguments") - } - - if *helpFlag || (*offsetFlag == "" && *fromFlag == "" && *toFlag == "") { - fmt.Print(rename.Usage) - return - } - - if err := rename.Main(&build.Default, *offsetFlag, *fromFlag, *toFlag); err != nil { - if err != rename.ConflictError { - log.Fatal(err) - } - os.Exit(1) - } -} diff --git a/cmd/gotype/gotype.go b/cmd/gotype/gotype.go index 4a731f26233..09b66207e63 100644 --- a/cmd/gotype/gotype.go +++ b/cmd/gotype/gotype.go @@ -85,6 +85,9 @@ To verify the output of a pipe: echo "package foo" | gotype */ + +//go:debug gotypesalias=0 + package main import ( diff --git a/cmd/ssadump/main.go b/cmd/ssadump/main.go index cfb9122b24d..2ecf04fba50 100644 --- a/cmd/ssadump/main.go +++ b/cmd/ssadump/main.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // ssadump: a tool for displaying and interpreting the SSA form of Go programs. + +//go:debug gotypesalias=0 + package main // import "golang.org/x/tools/cmd/ssadump" import ( @@ -14,7 +17,6 @@ import ( "runtime" "runtime/pprof" - "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/interp" @@ -38,11 +40,12 @@ T [T]race execution of the program. Best for single-threaded programs! cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") args stringListValue + + tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)") ) func init() { flag.Var(&mode, "build", ssa.BuilderModeDoc) - flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) flag.Var(&args, "arg", "add argument to interpreted program") } @@ -76,8 +79,9 @@ func doMain() error { } cfg := &packages.Config{ - Mode: packages.LoadSyntax, - Tests: *testFlag, + BuildFlags: []string{"-tags=" + *tagsFlag}, + Mode: packages.LoadSyntax, + Tests: *testFlag, } // Choose types.Sizes from conf.Build. diff --git a/cmd/stringer/endtoend_test.go b/cmd/stringer/endtoend_test.go index 2b9afa370c5..2b7d6a786a5 100644 --- a/cmd/stringer/endtoend_test.go +++ b/cmd/stringer/endtoend_test.go @@ -17,6 +17,8 @@ import ( "os" "path" "path/filepath" + "reflect" + "slices" "strings" "sync" "testing" @@ -197,6 +199,127 @@ func TestConstValueChange(t *testing.T) { } } +var testfileSrcs = map[string]string{ + "go.mod": "module foo", + + // Normal file in the package. + "main.go": `package foo + +type Foo int + +const ( + fooX Foo = iota + fooY + fooZ +) +`, + + // Test file in the package. + "main_test.go": `package foo + +type Bar int + +const ( + barX Bar = iota + barY + barZ +) +`, + + // Test file in the test package. + "main_pkg_test.go": `package foo_test + +type Baz int + +const ( + bazX Baz = iota + bazY + bazZ +) +`, +} + +// Test stringer on types defined in different kinds of tests. +// The generated code should not interfere between itself. +func TestTestFiles(t *testing.T) { + testenv.NeedsTool(t, "go") + stringer := stringerPath(t) + + dir := t.TempDir() + t.Logf("TestTestFiles in: %s \n", dir) + for name, src := range testfileSrcs { + source := filepath.Join(dir, name) + err := os.WriteFile(source, []byte(src), 0666) + if err != nil { + t.Fatalf("write file: %s", err) + } + } + + // Must run stringer in the temp directory, see TestTags. + err := runInDir(t, dir, stringer, "-type=Foo,Bar,Baz", dir) + if err != nil { + t.Fatalf("run stringer: %s", err) + } + + // Check that stringer has created the expected files. + content, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("read dir: %s", err) + } + gotFiles := []string{} + for _, f := range content { + if !f.IsDir() { + gotFiles = append(gotFiles, f.Name()) + } + } + wantFiles := []string{ + // Original. + "go.mod", + "main.go", + "main_test.go", + "main_pkg_test.go", + // Generated. + "foo_string.go", + "bar_string_test.go", + "baz_string_test.go", + } + slices.Sort(gotFiles) + slices.Sort(wantFiles) + if !reflect.DeepEqual(gotFiles, wantFiles) { + t.Errorf("stringer generated files:\n%s\n\nbut want:\n%s", + strings.Join(gotFiles, "\n"), + strings.Join(wantFiles, "\n"), + ) + } + + // Run go test as a smoke test. + err = runInDir(t, dir, "go", "test", "-count=1", ".") + if err != nil { + t.Fatalf("go test: %s", err) + } +} + +// The -output flag cannot be used in combiation with matching types across multiple packages. +func TestCollidingOutput(t *testing.T) { + testenv.NeedsTool(t, "go") + stringer := stringerPath(t) + + dir := t.TempDir() + for name, src := range testfileSrcs { + source := filepath.Join(dir, name) + err := os.WriteFile(source, []byte(src), 0666) + if err != nil { + t.Fatalf("write file: %s", err) + } + } + + // Must run stringer in the temp directory, see TestTags. + err := runInDir(t, dir, stringer, "-type=Foo,Bar,Baz", "-output=somefile.go", dir) + if err == nil { + t.Fatal("unexpected stringer success") + } +} + var exe struct { path string err error diff --git a/cmd/stringer/golden_test.go b/cmd/stringer/golden_test.go index a26eef35e36..2a81c0855aa 100644 --- a/cmd/stringer/golden_test.go +++ b/cmd/stringer/golden_test.go @@ -455,11 +455,6 @@ func TestGolden(t *testing.T) { for _, test := range golden { 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) @@ -468,16 +463,24 @@ func TestGolden(t *testing.T) { t.Fatal(err) } - g.parsePackage([]string{absFile}, nil) + pkgs := loadPackages([]string{absFile}, nil, test.trimPrefix, test.lineComment, t.Logf) + if len(pkgs) != 1 { + t.Fatalf("got %d parsed packages but expected 1", len(pkgs)) + } // 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]) + + g := Generator{ + pkg: pkgs[0], + logf: t.Logf, + } + g.generate(tokens[1], findValues(tokens[1], pkgs[0])) 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) + t.Errorf("%s: got(%d)\n====\n%q====\nexpected(%d)\n====\n%q", test.name, len(got), got, len(test.output), test.output) } }) } diff --git a/cmd/stringer/multifile_test.go b/cmd/stringer/multifile_test.go new file mode 100644 index 00000000000..7a7ae669065 --- /dev/null +++ b/cmd/stringer/multifile_test.go @@ -0,0 +1,464 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// For os.CopyFS +//go:build go1.23 + +package main + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + + "golang.org/x/tools/internal/diffp" + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/txtar" +) + +// This file contains a test that checks the output files existence +// and content when stringer has types from multiple different input +// files to choose from. +// +// Input is specified in a txtar string. + +// Several tests expect the type Foo generated in some package. +func expectFooString(pkg string) []byte { + return []byte(fmt.Sprintf(` +// Header comment ignored. + +package %s + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[fooX-0] + _ = x[fooY-1] + _ = x[fooZ-2] +} + +const _Foo_name = "fooXfooYfooZ" + +var _Foo_index = [...]uint8{0, 4, 8, 12} + +func (i Foo) String() string { + if i < 0 || i >= Foo(len(_Foo_index)-1) { + return "Foo(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Foo_name[_Foo_index[i]:_Foo_index[i+1]] +}`, pkg)) +} + +func TestMultifileStringer(t *testing.T) { + testenv.NeedsTool(t, "go") + stringer := stringerPath(t) + + tests := []struct { + name string + args []string + archive []byte + expectFiles map[string][]byte + }{ + { + name: "package only", + args: []string{"-type=Foo"}, + archive: []byte(` +-- go.mod -- +module foo + +-- main.go -- +package main + +type Foo int + +const ( + fooX Foo = iota + fooY + fooZ +)`), + expectFiles: map[string][]byte{ + "foo_string.go": expectFooString("main"), + }, + }, + { + name: "test package only", + args: []string{"-type=Foo"}, + archive: []byte(` +-- go.mod -- +module foo + +-- main.go -- +package main + +func main() {} + +-- main_test.go -- +package main + +type Foo int + +const ( + fooX Foo = iota + fooY + fooZ +)`), + expectFiles: map[string][]byte{ + "foo_string_test.go": expectFooString("main"), + }, + }, + { + name: "x_test package only", + args: []string{"-type=Foo"}, + archive: []byte(` +-- go.mod -- +module foo + +-- main.go -- +package main + +func main() {} + +-- main_test.go -- +package main_test + +type Foo int + +const ( + fooX Foo = iota + fooY + fooZ +)`), + expectFiles: map[string][]byte{ + "foo_string_test.go": expectFooString("main_test"), + }, + }, + { + // Re-declaring the type in a less prioritized package does not change our output. + name: "package over test package", + args: []string{"-type=Foo"}, + archive: []byte(` +-- go.mod -- +module foo + +-- main.go -- +package main + +type Foo int + +const ( + fooX Foo = iota + fooY + fooZ +) + +-- main_test.go -- +package main + +type Foo int + +const ( + otherX Foo = iota + otherY + otherZ +) +`), + expectFiles: map[string][]byte{ + "foo_string.go": expectFooString("main"), + }, + }, + { + // Re-declaring the type in a less prioritized package does not change our output. + name: "package over x_test package", + args: []string{"-type=Foo"}, + archive: []byte(` +-- go.mod -- +module foo + +-- main.go -- +package main + +type Foo int + +const ( + fooX Foo = iota + fooY + fooZ +) + +-- main_test.go -- +package main_test + +type Foo int + +const ( + otherX Foo = iota + otherY + otherZ +) +`), + expectFiles: map[string][]byte{ + "foo_string.go": expectFooString("main"), + }, + }, + { + // Re-declaring the type in a less prioritized package does not change our output. + name: "test package over x_test package", + args: []string{"-type=Foo"}, + archive: []byte(` +-- go.mod -- +module foo + +-- main.go -- +package main + +-- main_test.go -- +package main + +type Foo int + +const ( + fooX Foo = iota + fooY + fooZ +) + +-- main_pkg_test.go -- +package main_test + +type Foo int + +const ( + otherX Foo = iota + otherY + otherZ +)`), + expectFiles: map[string][]byte{ + "foo_string_test.go": expectFooString("main"), + }, + }, + { + name: "unique type in each package variant", + args: []string{"-type=Foo,Bar,Baz"}, + archive: []byte(` +-- go.mod -- +module foo + +-- main.go -- +package main + +type Foo int + +const fooX Foo = 1 + +-- main_test.go -- +package main + +type Bar int + +const barX Bar = 1 + +-- main_pkg_test.go -- +package main_test + +type Baz int + +const bazX Baz = 1 +`), + expectFiles: map[string][]byte{ + "foo_string.go": []byte(` +// Header comment ignored. + +package main + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[fooX-1] +} + +const _Foo_name = "fooX" + +var _Foo_index = [...]uint8{0, 4} + +func (i Foo) String() string { + i -= 1 + if i < 0 || i >= Foo(len(_Foo_index)-1) { + return "Foo(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _Foo_name[_Foo_index[i]:_Foo_index[i+1]] +}`), + + "bar_string_test.go": []byte(` +// Header comment ignored. + +package main + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[barX-1] +} + +const _Bar_name = "barX" + +var _Bar_index = [...]uint8{0, 4} + +func (i Bar) String() string { + i -= 1 + if i < 0 || i >= Bar(len(_Bar_index)-1) { + return "Bar(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _Bar_name[_Bar_index[i]:_Bar_index[i+1]] +}`), + + "baz_string_test.go": []byte(` +// Header comment ignored. + +package main_test + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[bazX-1] +} + +const _Baz_name = "bazX" + +var _Baz_index = [...]uint8{0, 4} + +func (i Baz) String() string { + i -= 1 + if i < 0 || i >= Baz(len(_Baz_index)-1) { + return "Baz(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _Baz_name[_Baz_index[i]:_Baz_index[i+1]] +}`), + }, + }, + + { + name: "package over test package with custom output", + args: []string{"-type=Foo", "-output=custom_output.go"}, + archive: []byte(` +-- go.mod -- +module foo + +-- main.go -- +package main + +type Foo int + +const ( + fooX Foo = iota + fooY + fooZ +) + +-- main_test.go -- +package main + +type Foo int + +const ( + otherX Foo = iota + otherY + otherZ +)`), + expectFiles: map[string][]byte{ + "custom_output.go": expectFooString("main"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + + arFS, err := txtar.FS(txtar.Parse(tt.archive)) + if err != nil { + t.Fatalf("txtar.FS: %s", err) + } + err = os.CopyFS(tmpDir, arFS) + if err != nil { + t.Fatalf("copy fs: %s", err) + } + before := dirContent(t, tmpDir) + + // Must run stringer in the temp directory, see TestTags. + args := append(tt.args, tmpDir) + err = runInDir(t, tmpDir, stringer, args...) + if err != nil { + t.Fatalf("run stringer: %s", err) + } + + // Check that all !path files have been created with the expected content. + for f, want := range tt.expectFiles { + got, err := os.ReadFile(filepath.Join(tmpDir, f)) + if errors.Is(err, os.ErrNotExist) { + t.Errorf("expected file not written during test: %s", f) + continue + } + if err != nil { + t.Fatalf("read file %q: %s", f, err) + } + // Trim data for more robust comparison. + got = trimHeader(bytes.TrimSpace(got)) + want = trimHeader(bytes.TrimSpace(want)) + if !bytes.Equal(want, got) { + t.Errorf("file %s does not have the expected content:\n%s", f, diffp.Diff("want", want, "got", got)) + } + } + + // Check that nothing else has been created. + after := dirContent(t, tmpDir) + for f := range after { + if _, expected := tt.expectFiles[f]; !expected && !before[f] { + t.Errorf("found %q in output directory, it is neither input or expected output", f) + } + } + + }) + } +} + +func dirContent(t *testing.T, dir string) map[string]bool { + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("read dir: %s", err) + } + + out := map[string]bool{} + for _, e := range entries { + out[e.Name()] = true + } + return out +} + +// trimHeader that stringer puts in file. +// It depends on location and interferes with comparing file content. +func trimHeader(s []byte) []byte { + if !bytes.HasPrefix(s, []byte("//")) { + return s + } + _, after, ok := bytes.Cut(s, []byte{'\n'}) + if ok { + return after + } + return s +} diff --git a/cmd/stringer/stringer.go b/cmd/stringer/stringer.go index 2b19c93e8ea..94eaee844a4 100644 --- a/cmd/stringer/stringer.go +++ b/cmd/stringer/stringer.go @@ -58,6 +58,11 @@ // where t is the lower-cased name of the first type listed. It can be overridden // with the -output flag. // +// Types can also be declared in tests, in which case type declarations in the +// non-test package or its test variant are preferred over types defined in the +// package with suffix "_test". +// The default output file for type declarations in tests is t_string_test.go with t picked as above. +// // The -linecomment flag tells stringer to generate the text of any line comment, trimmed // of leading spaces, instead of the constant name. For instance, if the constants above had a // Pill prefix, one could write @@ -65,6 +70,9 @@ // PillAspirin // Aspirin // // to suppress it in the output. + +//go:debug gotypesalias=0 + package main // import "golang.org/x/tools/cmd/stringer" import ( @@ -128,10 +136,6 @@ func main() { // Parse the package once. var dir string - g := Generator{ - trimPrefix: *trimprefix, - lineComment: *linecomment, - } // TODO(suzmue): accept other patterns for packages (directories, list of files, import paths, etc). if len(args) == 1 && isDirectory(args[0]) { dir = args[0] @@ -142,33 +146,90 @@ func main() { dir = filepath.Dir(args[0]) } - g.parsePackage(args, tags) + // For each type, generate code in the first package where the type is declared. + // The order of packages is as follows: + // package x + // package x compiled for tests + // package x_test + // + // Each package pass could result in a separate generated file. + // These files must have the same package and test/not-test nature as the types + // from which they were generated. + // + // Types will be excluded when generated, to avoid repetitions. + pkgs := loadPackages(args, tags, *trimprefix, *linecomment, nil /* logf */) + sort.Slice(pkgs, func(i, j int) bool { + // Put x_test packages last. + iTest := strings.HasSuffix(pkgs[i].name, "_test") + jTest := strings.HasSuffix(pkgs[j].name, "_test") + if iTest != jTest { + return !iTest + } - // Print the header and package clause. - g.Printf("// Code generated by \"stringer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " ")) - g.Printf("\n") - g.Printf("package %s", g.pkg.name) - g.Printf("\n") - g.Printf("import \"strconv\"\n") // Used by all methods. + return len(pkgs[i].files) < len(pkgs[j].files) + }) + for _, pkg := range pkgs { + g := Generator{ + pkg: pkg, + } - // Run generate for each type. - for _, typeName := range types { - g.generate(typeName) + // Print the header and package clause. + g.Printf("// Code generated by \"stringer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " ")) + g.Printf("\n") + g.Printf("package %s", g.pkg.name) + g.Printf("\n") + g.Printf("import \"strconv\"\n") // Used by all methods. + + // Run generate for types that can be found. Keep the rest for the remainingTypes iteration. + var foundTypes, remainingTypes []string + for _, typeName := range types { + values := findValues(typeName, pkg) + if len(values) > 0 { + g.generate(typeName, values) + foundTypes = append(foundTypes, typeName) + } else { + remainingTypes = append(remainingTypes, typeName) + } + } + if len(foundTypes) == 0 { + // This package didn't have any of the relevant types, skip writing a file. + continue + } + if len(remainingTypes) > 0 && output != nil && *output != "" { + log.Fatalf("cannot write to single file (-output=%q) when matching types are found in multiple packages", *output) + } + types = remainingTypes + + // Format the output. + src := g.format() + + // Write to file. + outputName := *output + if outputName == "" { + // Type names will be unique across packages since only the first + // match is picked. + // So there won't be collisions between a package compiled for tests + // and the separate package of tests (package foo_test). + outputName = filepath.Join(dir, baseName(pkg, foundTypes[0])) + } + err := os.WriteFile(outputName, src, 0644) + if err != nil { + log.Fatalf("writing output: %s", err) + } } - // Format the output. - src := g.format() - - // Write to file. - outputName := *output - if outputName == "" { - baseName := fmt.Sprintf("%s_string.go", types[0]) - outputName = filepath.Join(dir, strings.ToLower(baseName)) + if len(types) > 0 { + log.Fatalf("no values defined for types: %s", strings.Join(types, ",")) } - err := os.WriteFile(outputName, src, 0644) - if err != nil { - log.Fatalf("writing output: %s", err) +} + +// baseName that will put the generated code together with pkg. +func baseName(pkg *Package, typename string) string { + suffix := "string.go" + if pkg.hasTestFiles { + suffix = "string_test.go" } + return fmt.Sprintf("%s_%s", strings.ToLower(typename), suffix) } // isDirectory reports whether the named file is a directory. @@ -186,9 +247,6 @@ type Generator struct { buf bytes.Buffer // Accumulated output. pkg *Package // Package we are scanning. - trimPrefix string - lineComment bool - logf func(format string, args ...interface{}) // test logging hook; nil when not testing } @@ -209,54 +267,74 @@ type File struct { } type Package struct { - name string - defs map[*ast.Ident]types.Object - files []*File + name string + defs map[*ast.Ident]types.Object + files []*File + hasTestFiles bool } -// parsePackage analyzes the single package constructed from the patterns and tags. -// parsePackage exits if there is an error. -func (g *Generator) parsePackage(patterns []string, tags []string) { +// loadPackages analyzes the single package constructed from the patterns and tags. +// loadPackages exits if there is an error. +// +// Returns all variants (such as tests) of the package. +// +// logf is a test logging hook. It can be nil when not testing. +func loadPackages( + patterns, tags []string, + trimPrefix string, lineComment bool, + logf func(format string, args ...interface{}), +) []*Package { cfg := &packages.Config{ - Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax, - // TODO: Need to think about constants in test files. Maybe write type_string_test.go - // in a separate pass? For later. - Tests: false, + Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedFiles, + // Tests are included, let the caller decide how to fold them in. + Tests: true, BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))}, - Logf: g.logf, + Logf: logf, } pkgs, err := packages.Load(cfg, patterns...) if err != nil { log.Fatal(err) } - if len(pkgs) != 1 { - log.Fatalf("error: %d packages matching %v", len(pkgs), strings.Join(patterns, " ")) + if len(pkgs) == 0 { + log.Fatalf("error: no packages matching %v", strings.Join(patterns, " ")) } - g.addPackage(pkgs[0]) -} -// addPackage adds a type checked Package and its syntax files to the generator. -func (g *Generator) addPackage(pkg *packages.Package) { - g.pkg = &Package{ - name: pkg.Name, - defs: pkg.TypesInfo.Defs, - files: make([]*File, len(pkg.Syntax)), - } + out := make([]*Package, len(pkgs)) + for i, pkg := range pkgs { + p := &Package{ + name: pkg.Name, + defs: pkg.TypesInfo.Defs, + files: make([]*File, len(pkg.Syntax)), + } + + for j, file := range pkg.Syntax { + p.files[j] = &File{ + file: file, + pkg: p, - for i, file := range pkg.Syntax { - g.pkg.files[i] = &File{ - file: file, - pkg: g.pkg, - trimPrefix: g.trimPrefix, - lineComment: g.lineComment, + trimPrefix: trimPrefix, + lineComment: lineComment, + } } + + // Keep track of test files, since we might want to generated + // code that ends up in that kind of package. + // Can be replaced once https://go.dev/issue/38445 lands. + for _, f := range pkg.GoFiles { + if strings.HasSuffix(f, "_test.go") { + p.hasTestFiles = true + break + } + } + + out[i] = p } + return out } -// generate produces the String method for the named type. -func (g *Generator) generate(typeName string) { +func findValues(typeName string, pkg *Package) []Value { values := make([]Value, 0, 100) - for _, file := range g.pkg.files { + for _, file := range pkg.files { // Set the state for this run of the walker. file.typeName = typeName file.values = nil @@ -265,10 +343,11 @@ func (g *Generator) generate(typeName string) { values = append(values, file.values...) } } + return values +} - if len(values) == 0 { - log.Fatalf("no values defined for type %s", typeName) - } +// generate produces the String method for the named type. +func (g *Generator) generate(typeName string, values []Value) { // Generate code that will fail if the constants change value. g.Printf("func _() {\n") g.Printf("\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n") diff --git a/go.mod b/go.mod index 003d83773df..9715167f426 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/google/go-cmp v0.6.0 github.com/yuin/goldmark v1.4.13 golang.org/x/mod v0.21.0 - golang.org/x/net v0.29.0 + golang.org/x/net v0.30.0 golang.org/x/sync v0.8.0 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 ) -require golang.org/x/sys v0.25.0 // indirect +require golang.org/x/sys v0.26.0 // indirect diff --git a/go.sum b/go.sum index eae2ee53cc7..459786d0b91 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,11 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go index a73a0967ca5..b0d711b4182 100644 --- a/go/analysis/internal/checker/checker_test.go +++ b/go/analysis/internal/checker/checker_test.go @@ -223,10 +223,6 @@ func NewT1() *T1 { return &T1{T} } // duplicate list errors with findings (issue #67790) {name: "list-error-findings", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 3}, } { - if test.name == "despite-error" && testenv.Go1Point() < 20 { - // The behavior in the comment on the despite-error test only occurs for Go 1.20+. - continue - } if got := checker.Run(test.pattern, test.analyzers); got != test.code { t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code) } diff --git a/go/analysis/passes/assign/assign.go b/go/analysis/passes/assign/assign.go index 3bfd501226f..0d95fefcb5a 100644 --- a/go/analysis/passes/assign/assign.go +++ b/go/analysis/passes/assign/assign.go @@ -18,7 +18,6 @@ import ( "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/astutil" "golang.org/x/tools/go/ast/inspector" ) @@ -78,7 +77,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // isMapIndex returns true if e is a map index expression. func isMapIndex(info *types.Info, e ast.Expr) bool { - if idx, ok := astutil.Unparen(e).(*ast.IndexExpr); ok { + if idx, ok := ast.Unparen(e).(*ast.IndexExpr); ok { if typ := info.Types[idx.X].Type; typ != nil { _, ok := typ.Underlying().(*types.Map) return ok diff --git a/go/analysis/passes/bools/bools.go b/go/analysis/passes/bools/bools.go index 564329774ef..8cec6e8224a 100644 --- a/go/analysis/passes/bools/bools.go +++ b/go/analysis/passes/bools/bools.go @@ -14,7 +14,6 @@ import ( "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/astutil" "golang.org/x/tools/go/ast/inspector" ) @@ -169,7 +168,7 @@ func (op boolOp) checkSuspect(pass *analysis.Pass, exprs []ast.Expr) { // seen[e] is already true; any newly processed exprs are added to seen. func (op boolOp) split(e ast.Expr, seen map[*ast.BinaryExpr]bool) (exprs []ast.Expr) { for { - e = astutil.Unparen(e) + e = ast.Unparen(e) if b, ok := e.(*ast.BinaryExpr); ok && b.Op == op.tok { seen[b] = true exprs = append(exprs, op.split(b.Y, seen)...) diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go index 4e864397574..26ec0683158 100644 --- a/go/analysis/passes/cgocall/cgocall.go +++ b/go/analysis/passes/cgocall/cgocall.go @@ -19,7 +19,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" - "golang.org/x/tools/go/ast/astutil" ) const debug = false @@ -65,7 +64,7 @@ func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(t // Is this a C.f() call? var name string - if sel, ok := astutil.Unparen(call.Fun).(*ast.SelectorExpr); ok { + if sel, ok := ast.Unparen(call.Fun).(*ast.SelectorExpr); ok { if id, ok := sel.X.(*ast.Ident); ok && id.Name == "C" { name = sel.Sel.Name } diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go index 8cc6c4a058b..f56c3e622fb 100644 --- a/go/analysis/passes/composite/composite.go +++ b/go/analysis/passes/composite/composite.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -72,7 +71,7 @@ func run(pass *analysis.Pass) (interface{}, error) { return } var structuralTypes []types.Type - switch typ := aliases.Unalias(typ).(type) { + switch typ := types.Unalias(typ).(type) { case *types.TypeParam: terms, err := typeparams.StructuralTerms(typ) if err != nil { @@ -146,7 +145,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // isLocalType reports whether typ belongs to the same package as pass. // TODO(adonovan): local means "internal to a function"; rename to isSamePackageType. func isLocalType(pass *analysis.Pass, typ types.Type) bool { - switch x := aliases.Unalias(typ).(type) { + switch x := types.Unalias(typ).(type) { case *types.Struct: // struct literals are local types return true diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go index 0d63cd16124..03496cb3037 100644 --- a/go/analysis/passes/copylock/copylock.go +++ b/go/analysis/passes/copylock/copylock.go @@ -16,9 +16,7 @@ import ( "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/astutil" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/versions" ) @@ -253,7 +251,7 @@ func (path typePath) String() string { } func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath { - x = astutil.Unparen(x) // ignore parens on rhs + x = ast.Unparen(x) // ignore parens on rhs if _, ok := x.(*ast.CompositeLit); ok { return nil @@ -263,7 +261,7 @@ func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath { return nil } if star, ok := x.(*ast.StarExpr); ok { - if _, ok := astutil.Unparen(star.X).(*ast.CallExpr); ok { + if _, ok := ast.Unparen(star.X).(*ast.CallExpr); ok { // A call may return a pointer to a zero value. return nil } @@ -287,7 +285,7 @@ func lockPath(tpkg *types.Package, typ types.Type, seen map[types.Type]bool) typ } seen[typ] = true - if tpar, ok := aliases.Unalias(typ).(*types.TypeParam); ok { + if tpar, ok := types.Unalias(typ).(*types.TypeParam); ok { terms, err := typeparams.StructuralTerms(tpar) if err != nil { return nil // invalid type diff --git a/go/analysis/passes/deepequalerrors/deepequalerrors.go b/go/analysis/passes/deepequalerrors/deepequalerrors.go index 95cd9a061eb..70b5e39ecf8 100644 --- a/go/analysis/passes/deepequalerrors/deepequalerrors.go +++ b/go/analysis/passes/deepequalerrors/deepequalerrors.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/aliases" ) const Doc = `check for calls of reflect.DeepEqual on error values @@ -102,7 +101,7 @@ func containsError(typ types.Type) bool { return true } } - case *types.Named, *aliases.Alias: + case *types.Named, *types.Alias: return check(t.Underlying()) // We list the remaining valid type kinds for completeness. diff --git a/go/analysis/passes/defers/cmd/defers/main.go b/go/analysis/passes/defers/cmd/defers/main.go index b3dc8b94eca..ffa5ae2da9b 100644 --- a/go/analysis/passes/defers/cmd/defers/main.go +++ b/go/analysis/passes/defers/cmd/defers/main.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // The defers command runs the defers analyzer. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/directive/directive_test.go b/go/analysis/passes/directive/directive_test.go index f20a07e321f..8f6ae0578e5 100644 --- a/go/analysis/passes/directive/directive_test.go +++ b/go/analysis/passes/directive/directive_test.go @@ -10,11 +10,9 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/directive" - "golang.org/x/tools/internal/testenv" ) func Test(t *testing.T) { - testenv.NeedsGo1Point(t, 16) analyzer := *directive.Analyzer analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { defer func() { diff --git a/go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go b/go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go index 47d383d2d0e..9ec4b9b505e 100644 --- a/go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go +++ b/go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go @@ -1,6 +1,9 @@ // 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:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/findcall/cmd/findcall/main.go b/go/analysis/passes/findcall/cmd/findcall/main.go index e0ce9137d61..1ada9668313 100644 --- a/go/analysis/passes/findcall/cmd/findcall/main.go +++ b/go/analysis/passes/findcall/cmd/findcall/main.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // The findcall command runs the findcall analyzer. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/httpmux/cmd/httpmux/main.go b/go/analysis/passes/httpmux/cmd/httpmux/main.go index e8a631157dc..5933df923da 100644 --- a/go/analysis/passes/httpmux/cmd/httpmux/main.go +++ b/go/analysis/passes/httpmux/cmd/httpmux/main.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // The httpmux command runs the httpmux analyzer. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/httpresponse/httpresponse.go b/go/analysis/passes/httpresponse/httpresponse.go index e1ca9b2f514..91ebe29de11 100644 --- a/go/analysis/passes/httpresponse/httpresponse.go +++ b/go/analysis/passes/httpresponse/httpresponse.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) @@ -137,7 +136,7 @@ func isHTTPFuncOrMethodOnClient(info *types.Info, expr *ast.CallExpr) bool { if analysisutil.IsNamedType(typ, "net/http", "Client") { return true // method on http.Client. } - ptr, ok := aliases.Unalias(typ).(*types.Pointer) + ptr, ok := types.Unalias(typ).(*types.Pointer) return ok && analysisutil.IsNamedType(ptr.Elem(), "net/http", "Client") // method on *http.Client. } diff --git a/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go b/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go index 42250f93df8..32390be1643 100644 --- a/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go +++ b/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // The ifaceassert command runs the ifaceassert analyzer. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go index f7f071dc8be..a4fa8d31c4e 100644 --- a/go/analysis/passes/internal/analysisutil/util.go +++ b/go/analysis/passes/internal/analysisutil/util.go @@ -15,7 +15,6 @@ import ( "os" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/analysisinternal" ) @@ -121,7 +120,7 @@ func Imports(pkg *types.Package, path string) bool { // This function avoids allocating the concatenation of "pkg.Name", // which is important for the performance of syntax matching. func IsNamedType(t types.Type, pkgPath string, names ...string) bool { - n, ok := aliases.Unalias(t).(*types.Named) + n, ok := types.Unalias(t).(*types.Named) if !ok { return false } diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index 8b282027d41..581cb13af98 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go @@ -10,29 +10,10 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/loopclosure" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/testfiles" ) -func Test(t *testing.T) { - // legacy loopclosure test expectations are incorrect > 1.21. - testenv.SkipAfterGo1Point(t, 21) - - testdata := analysistest.TestData() - analysistest.Run(t, testdata, loopclosure.Analyzer, - "a", "golang.org/...", "subtests", "typeparams") -} - -func TestVersions22(t *testing.T) { - t.Skip("Disabled for golang.org/cl/603895. Fix and re-enable.") - testenv.NeedsGo1Point(t, 22) - +func TestVersions(t *testing.T) { dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "src", "versions", "go22.txtar")) analysistest.Run(t, dir, loopclosure.Analyzer, "golang.org/fake/versions") } - -func TestVersions18(t *testing.T) { - t.Skip("Disabled for golang.org/cl/603895. Fix and re-enable.") - dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "src", "versions", "go18.txtar")) - analysistest.Run(t, dir, loopclosure.Analyzer, "golang.org/fake/versions") -} diff --git a/go/analysis/passes/loopclosure/testdata/src/versions/go22.txtar b/go/analysis/passes/loopclosure/testdata/src/versions/go22.txtar index eef54081b17..5e53f3dbadf 100644 --- a/go/analysis/passes/loopclosure/testdata/src/versions/go22.txtar +++ b/go/analysis/passes/loopclosure/testdata/src/versions/go22.txtar @@ -1,13 +1,13 @@ Test loopclosure at go version go1.22. -The go1.19 build tag is necessary to force the file version. +The go1.21 build tag is necessary to force the file version. -- go.mod -- module golang.org/fake/versions go 1.22 -- pre.go -- -//go:build go1.19 +//go:build go1.21 package versions diff --git a/go/analysis/passes/lostcancel/cmd/lostcancel/main.go b/go/analysis/passes/lostcancel/cmd/lostcancel/main.go index 0bba8465242..3f2ac7c38f5 100644 --- a/go/analysis/passes/lostcancel/cmd/lostcancel/main.go +++ b/go/analysis/passes/lostcancel/cmd/lostcancel/main.go @@ -4,6 +4,9 @@ // The lostcancel command applies the golang.org/x/tools/go/analysis/passes/lostcancel // analysis to the specified packages of Go source code. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/nilness/cmd/nilness/main.go b/go/analysis/passes/nilness/cmd/nilness/main.go index 136ac254a45..91b4d5c44b3 100644 --- a/go/analysis/passes/nilness/cmd/nilness/main.go +++ b/go/analysis/passes/nilness/cmd/nilness/main.go @@ -4,6 +4,9 @@ // The nilness command applies the golang.org/x/tools/go/analysis/passes/nilness // analysis to the specified packages of Go source code. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/nilness/nilness.go b/go/analysis/passes/nilness/nilness.go index 0c7e9c5d06f..faaf12a9385 100644 --- a/go/analysis/passes/nilness/nilness.go +++ b/go/analysis/passes/nilness/nilness.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/go/analysis/passes/buildssa" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ssa" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -300,7 +299,7 @@ func nilnessOf(stack []fact, v ssa.Value) nilness { } case *ssa.MakeInterface: // A MakeInterface is non-nil unless its operand is a type parameter. - tparam, ok := aliases.Unalias(v.X.Type()).(*types.TypeParam) + tparam, ok := types.Unalias(v.X.Type()).(*types.TypeParam) if !ok { return isnonnil } diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index c548cb1c1dc..2d79d0b0334 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -24,7 +24,6 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -161,7 +160,7 @@ func maybePrintfWrapper(info *types.Info, decl ast.Decl) *printfWrapper { // Check final parameter is "args ...interface{}". args := params.At(nparams - 1) - iface, ok := aliases.Unalias(args.Type().(*types.Slice).Elem()).(*types.Interface) + iface, ok := types.Unalias(args.Type().(*types.Slice).Elem()).(*types.Interface) if !ok || !iface.Empty() { return nil } @@ -515,7 +514,7 @@ func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, fn *types.F // non-constant format string and no arguments: // if msg contains "%", misformatting occurs. // Report the problem and suggest a fix: fmt.Printf("%s", msg). - if idx == len(call.Args)-1 { + if !suppressNonconstants && idx == len(call.Args)-1 { pass.Report(analysis.Diagnostic{ Pos: formatArg.Pos(), End: formatArg.End(), @@ -1013,7 +1012,7 @@ func checkPrint(pass *analysis.Pass, call *ast.CallExpr, fn *types.Func) { typ := params.At(firstArg).Type() typ = typ.(*types.Slice).Elem() - it, ok := aliases.Unalias(typ).(*types.Interface) + it, ok := types.Unalias(typ).(*types.Interface) if !ok || !it.Empty() { // Skip variadic functions accepting non-interface{} args. return @@ -1101,3 +1100,12 @@ func (ss stringSet) Set(flag string) error { } return nil } + +// suppressNonconstants suppresses reporting printf calls with +// non-constant formatting strings (proposal #60529) when true. +// +// This variable is to allow for staging the transition to newer +// versions of x/tools by vendoring. +// +// Remove this after the 1.24 release. +var suppressNonconstants bool diff --git a/go/analysis/passes/printf/types.go b/go/analysis/passes/printf/types.go index 017c8a247ec..f7e50f98a9d 100644 --- a/go/analysis/passes/printf/types.go +++ b/go/analysis/passes/printf/types.go @@ -10,7 +10,6 @@ import ( "go/types" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -73,7 +72,7 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool { return true } - if typ, _ := aliases.Unalias(typ).(*types.TypeParam); typ != nil { + if typ, _ := types.Unalias(typ).(*types.TypeParam); typ != nil { // Avoid infinite recursion through type parameters. if m.seen[typ] { return true @@ -276,7 +275,7 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool { } func isConvertibleToString(typ types.Type) bool { - if bt, ok := aliases.Unalias(typ).(*types.Basic); ok && bt.Kind() == types.UntypedNil { + if bt, ok := types.Unalias(typ).(*types.Basic); ok && bt.Kind() == types.UntypedNil { // We explicitly don't want untyped nil, which is // convertible to both of the interfaces below, as it // would just panic anyway. diff --git a/go/analysis/passes/shadow/cmd/shadow/main.go b/go/analysis/passes/shadow/cmd/shadow/main.go index f9e36ecee95..38de46beb3e 100644 --- a/go/analysis/passes/shadow/cmd/shadow/main.go +++ b/go/analysis/passes/shadow/cmd/shadow/main.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // The shadow command runs the shadow analyzer. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/shift/shift.go b/go/analysis/passes/shift/shift.go index d01eb1eebe5..759ed0043ff 100644 --- a/go/analysis/passes/shift/shift.go +++ b/go/analysis/passes/shift/shift.go @@ -21,7 +21,6 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -100,7 +99,7 @@ func checkLongShift(pass *analysis.Pass, node ast.Node, x, y ast.Expr) { return } var structuralTypes []types.Type - switch t := aliases.Unalias(t).(type) { + switch t := types.Unalias(t).(type) { case *types.TypeParam: terms, err := typeparams.StructuralTerms(t) if err != nil { diff --git a/go/analysis/passes/slog/slog_test.go b/go/analysis/passes/slog/slog_test.go index a0db7fdb33d..b73a5870d70 100644 --- a/go/analysis/passes/slog/slog_test.go +++ b/go/analysis/passes/slog/slog_test.go @@ -8,11 +8,9 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/testenv" ) func Test(t *testing.T) { - testenv.NeedsGo1Point(t, 21) testdata := analysistest.TestData() analysistest.Run(t, testdata, Analyzer, "a", "b") } diff --git a/go/analysis/passes/stdversion/stdversion_test.go b/go/analysis/passes/stdversion/stdversion_test.go index e1f71fac3f5..71dc1de0ec8 100644 --- a/go/analysis/passes/stdversion/stdversion_test.go +++ b/go/analysis/passes/stdversion/stdversion_test.go @@ -15,15 +15,17 @@ import ( ) func Test(t *testing.T) { - t.Skip("Disabled for golang.org/cl/603895. Fix and re-enable.") + testenv.NeedsGo1Point(t, 23) // TODO(#68658): Waiting on 1.22 backport. + // The test relies on go1.21 std symbols, but the analyzer // itself requires the go1.22 implementation of versions.FileVersions. - testenv.NeedsGo1Point(t, 22) - dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "test.txtar")) analysistest.Run(t, dir, stdversion.Analyzer, - "example.com/a", - "example.com/sub", - "example.com/sub20", - "example.com/old") + "example.com/basic", + "example.com/despite", + "example.com/mod20", + "example.com/mod21", + "example.com/mod22", + "example.com/old", + ) } diff --git a/go/analysis/passes/stdversion/testdata/test.txtar b/go/analysis/passes/stdversion/testdata/test.txtar index 0d27f112b04..cb04407be9d 100644 --- a/go/analysis/passes/stdversion/testdata/test.txtar +++ b/go/analysis/passes/stdversion/testdata/test.txtar @@ -2,18 +2,17 @@ Test of "too new" diagnostics from the stdversion analyzer. This test references go1.21 and go1.22 symbols from std. -It uses a txtar file due to golang/go#37054. - See also gopls/internal/test/marker/testdata/diagnostics/stdversion.txt which runs the same test within the gopls analysis driver, to ensure coverage of per-file Go version support. -- go.work -- -go 1.21 +go 1.22 use . -use sub -use sub20 +use mod20 +use mod21 +use mod22 use old -- go.mod -- @@ -21,8 +20,9 @@ module example.com go 1.21 --- a/a.go -- -package a +-- basic/basic.go -- +// File version is 1.21. +package basic import "go/types" @@ -43,13 +43,77 @@ func _() { a.Underlying() // no diagnostic } --- sub/go.mod -- -module example.com/sub +-- despite/errors.go -- +// File version is 1.21. + +// Check that RunDespiteErrors is enabled. +package ignore + +import "go/types" + +func _() { + // report something before the syntax error. + _ = new(types.Info).FileVersions // want `types.FileVersions requires go1.22 or later \(module is go1.21\)` +} + +invalid syntax // exercise RunDespiteErrors + +-- mod20/go.mod -- +module example.com/mod20 + +go 1.20 + +-- mod20/notag.go -- +// The 1.20 module is before the forward compatibility regime: +// The file's build tag effects selection, but +// not language semantics, so stdversion is silent. + +package mod20 + +import "go/types" + +func _() { + var _ types.Alias +} + +-- mod20/tag16.go -- +// The 1.20 module is before the forward compatibility regime: +// The file's build tag effects selection, but +// not language semantics, so stdversion is silent. + +//go:build go1.16 + +package mod20 + +import "bytes" +import "go/types" + +var _ = bytes.Clone +var _ = types.Alias + +-- mod20/tag22.go -- +// The 1.20 module is before the forward compatibility regime: +// The file's build tag effects selection, but +// not language semantics, so stdversion is silent. + +//go:build go1.22 + +package mod20 + +import "bytes" +import "go/types" + +var _ = bytes.Clone +var _ = types.Alias + +-- mod21/go.mod -- +module example.com/mod21 go 1.21 --- sub/sub.go -- -package sub +-- mod21/notag.go -- +// File version is 1.21. +package mod21 import "go/types" @@ -70,79 +134,91 @@ func _() { a.Underlying() // no diagnostic } -invalid syntax // exercise RunDespiteErrors +-- mod21/tag16.go -- +// File version is 1.21. +// +// The module is within the forward compatibility regime so +// the build tag (1.16) can modify the file version, but it cannot +// go below the 1.21 "event horizon" (#68658). --- sub/tagged.go -- -//go:build go1.22 +//go:build go1.16 -package sub +package mod21 +import "bytes" import "go/types" -func _() { - // old package-level type - var _ types.Info +var _ = bytes.Clone +var _ = types.Alias // want `types.Alias requires go1.22 or later \(module is go1.21\)` - // new field of older type - _ = new(types.Info).FileVersions +-- mod21/tag22.go -- +// File version is 1.22. +// +// The module is within the forward compatibility regime so +// the build tag (1.22) updates the file version to 1.22. - // new method of older type - new(types.Info).PkgNameOf +//go:build go1.22 - // new package-level type - var a types.Alias +package mod21 - // new method of new type - a.Underlying() -} +import "bytes" +import "go/types" --- old/go.mod -- -module example.com/old +var _ = bytes.Clone +var _ = types.Alias -go 1.5 +-- mod22/go.mod -- +module example.com/mod22 --- old/old.go -- -package old +go 1.22 + +-- mod22/notag.go -- +// File version is 1.22. +package mod22 import "go/types" -var _ types.Alias // no diagnostic: go.mod is too old for us to care +func _() { + var _ = bytes.Clone + var _ = types.Alias +} --- sub/oldtagged.go -- -// The file Go version (1.16) overrides the go.mod Go version (1.21), -// even when this means a downgrade (#67123). -// (stdversion is silent for go.mod versions before 1.21: -// before the forward compatibility regime, the meaning -// of the go.mod version was not clearly defined.) +-- mod22/tag16.go -- +// File version is 1.21. +// +// The module is within the forward compatibility regime so +// the build tag (1.16) can modify the file version, but it cannot +// go below the 1.21 "event horizon" (#68658). //go:build go1.16 -package sub +package mod22 import "bytes" import "go/types" -var _ = bytes.Clone // want `bytes.Clone requires go1.20 or later \(file is go1.16\)` -var _ = types.Alias // want `types.Alias requires go1.22 or later \(file is go1.16\)` +var _ = bytes.Clone +var _ = types.Alias // want `types.Alias requires go1.22 or later \(file is go1.21\)` --- sub20/go.mod -- -module example.com/sub20 +-- old/go.mod -- +module example.com/old -go 1.20 +go 1.5 --- sub20/oldtagged.go -- -// Same test again, but with a go1.20 mod, -// before the forward compatibility regime: -// The file's build tag effects selection, but -// not language semantics, so stdversion is silent. +-- old/notag.go -- +package old -//go:build go1.16 +import "go/types" -package sub +var _ types.Alias // no diagnostic: go.mod is too old for us to care -import "bytes" -import "go/types" +-- old/tag21.go -- +// The build tag is ignored due to the module version. -var _ = bytes.Clone -var _ = types.Alias +//go:build go1.21 + +package old + +import "go/types" +var _ = types.Alias // no diagnostic: go.mod is too old for us to care diff --git a/go/analysis/passes/stringintconv/cmd/stringintconv/main.go b/go/analysis/passes/stringintconv/cmd/stringintconv/main.go index 118b9579a50..8dfb9a2056d 100644 --- a/go/analysis/passes/stringintconv/cmd/stringintconv/main.go +++ b/go/analysis/passes/stringintconv/cmd/stringintconv/main.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // The stringintconv command runs the stringintconv analyzer. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/stringintconv/string.go b/go/analysis/passes/stringintconv/string.go index a3afbf696e1..108600a2baf 100644 --- a/go/analysis/passes/stringintconv/string.go +++ b/go/analysis/passes/stringintconv/string.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/typeparams" ) @@ -248,7 +247,7 @@ func run(pass *analysis.Pass) (interface{}, error) { func structuralTypes(t types.Type) ([]types.Type, error) { var structuralTypes []types.Type - if tp, ok := aliases.Unalias(t).(*types.TypeParam); ok { + if tp, ok := types.Unalias(t).(*types.TypeParam); ok { terms, err := typeparams.StructuralTerms(tp) if err != nil { return nil, err diff --git a/go/analysis/passes/testinggoroutine/testinggoroutine.go b/go/analysis/passes/testinggoroutine/testinggoroutine.go index 828f95bc862..effcdc5700b 100644 --- a/go/analysis/passes/testinggoroutine/testinggoroutine.go +++ b/go/analysis/passes/testinggoroutine/testinggoroutine.go @@ -14,10 +14,8 @@ import ( "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/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/aliases" ) //go:embed doc.go @@ -186,7 +184,7 @@ func withinScope(scope ast.Node, x *types.Var) bool { func goAsyncCall(info *types.Info, goStmt *ast.GoStmt, toDecl func(*types.Func) *ast.FuncDecl) *asyncCall { call := goStmt.Call - fun := astutil.Unparen(call.Fun) + fun := ast.Unparen(call.Fun) if id := funcIdent(fun); id != nil { if lit := funcLitInScope(id); lit != nil { return &asyncCall{region: lit, async: goStmt, scope: nil, fun: fun} @@ -213,7 +211,7 @@ func tRunAsyncCall(info *types.Info, call *ast.CallExpr) *asyncCall { return nil } - fun := astutil.Unparen(call.Args[1]) + fun := ast.Unparen(call.Args[1]) if lit, ok := fun.(*ast.FuncLit); ok { // function lit? return &asyncCall{region: lit, async: call, scope: lit, fun: fun} } @@ -243,7 +241,7 @@ var forbidden = []string{ // Returns (nil, nil, nil) if call is not of this form. func forbiddenMethod(info *types.Info, call *ast.CallExpr) (*types.Var, *types.Selection, *types.Func) { // Compare to typeutil.StaticCallee. - fun := astutil.Unparen(call.Fun) + fun := ast.Unparen(call.Fun) selExpr, ok := fun.(*ast.SelectorExpr) if !ok { return nil, nil, nil @@ -254,7 +252,7 @@ func forbiddenMethod(info *types.Info, call *ast.CallExpr) (*types.Var, *types.S } var x *types.Var - if id, ok := astutil.Unparen(selExpr.X).(*ast.Ident); ok { + if id, ok := ast.Unparen(selExpr.X).(*ast.Ident); ok { x, _ = info.Uses[id].(*types.Var) } if x == nil { @@ -271,7 +269,7 @@ func forbiddenMethod(info *types.Info, call *ast.CallExpr) (*types.Var, *types.S func formatMethod(sel *types.Selection, fn *types.Func) string { var ptr string rtype := sel.Recv() - if p, ok := aliases.Unalias(rtype).(*types.Pointer); ok { + if p, ok := types.Unalias(rtype).(*types.Pointer); ok { ptr = "*" rtype = p.Elem() } diff --git a/go/analysis/passes/testinggoroutine/util.go b/go/analysis/passes/testinggoroutine/util.go index ad815f19010..8c7a51ca525 100644 --- a/go/analysis/passes/testinggoroutine/util.go +++ b/go/analysis/passes/testinggoroutine/util.go @@ -8,7 +8,6 @@ import ( "go/ast" "go/types" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/typeparams" ) @@ -56,7 +55,7 @@ func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool { } func funcIdent(fun ast.Expr) *ast.Ident { - switch fun := astutil.Unparen(fun).(type) { + switch fun := ast.Unparen(fun).(type) { case *ast.IndexExpr, *ast.IndexListExpr: x, _, _, _ := typeparams.UnpackIndexExpr(fun) // necessary? id, _ := x.(*ast.Ident) diff --git a/go/analysis/passes/unmarshal/cmd/unmarshal/main.go b/go/analysis/passes/unmarshal/cmd/unmarshal/main.go index 1a17cd64de3..fd69013fa59 100644 --- a/go/analysis/passes/unmarshal/cmd/unmarshal/main.go +++ b/go/analysis/passes/unmarshal/cmd/unmarshal/main.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // The unmarshal command runs the unmarshal analyzer. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go index 14e4a6c1e4b..272ae7fe045 100644 --- a/go/analysis/passes/unsafeptr/unsafeptr.go +++ b/go/analysis/passes/unsafeptr/unsafeptr.go @@ -15,9 +15,7 @@ import ( "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/astutil" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/internal/aliases" ) //go:embed doc.go @@ -70,7 +68,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // Check unsafe.Pointer safety rules according to // https://golang.org/pkg/unsafe/#Pointer. - switch x := astutil.Unparen(x).(type) { + switch x := ast.Unparen(x).(type) { case *ast.SelectorExpr: // "(6) Conversion of a reflect.SliceHeader or // reflect.StringHeader Data field to or from Pointer." @@ -89,7 +87,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // by the time we get to the conversion at the end. // For now approximate by saying that *Header is okay // but Header is not. - pt, ok := aliases.Unalias(info.Types[x.X].Type).(*types.Pointer) + pt, ok := types.Unalias(info.Types[x.X].Type).(*types.Pointer) if ok && isReflectHeader(pt.Elem()) { return true } @@ -119,7 +117,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // isSafeArith reports whether x is a pointer arithmetic expression that is safe // to convert to unsafe.Pointer. func isSafeArith(info *types.Info, x ast.Expr) bool { - switch x := astutil.Unparen(x).(type) { + switch x := ast.Unparen(x).(type) { case *ast.CallExpr: // Base case: initial conversion from unsafe.Pointer to uintptr. return len(x.Args) == 1 && diff --git a/go/analysis/passes/unusedresult/cmd/unusedresult/main.go b/go/analysis/passes/unusedresult/cmd/unusedresult/main.go index 8116c6e06e9..635883c4051 100644 --- a/go/analysis/passes/unusedresult/cmd/unusedresult/main.go +++ b/go/analysis/passes/unusedresult/cmd/unusedresult/main.go @@ -4,6 +4,9 @@ // The unusedresult command applies the golang.org/x/tools/go/analysis/passes/unusedresult // analysis to the specified packages of Go source code. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index 76f42b052e4..c27d26dd6ec 100644 --- a/go/analysis/passes/unusedresult/unusedresult.go +++ b/go/analysis/passes/unusedresult/unusedresult.go @@ -24,7 +24,6 @@ import ( "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/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" ) @@ -101,7 +100,7 @@ func run(pass *analysis.Pass) (interface{}, error) { (*ast.ExprStmt)(nil), } inspect.Preorder(nodeFilter, func(n ast.Node) { - call, ok := astutil.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr) + call, ok := ast.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr) if !ok { return // not a call statement } diff --git a/go/analysis/passes/unusedwrite/unusedwrite.go b/go/analysis/passes/unusedwrite/unusedwrite.go index a99c5483351..3f651fc26d5 100644 --- a/go/analysis/passes/unusedwrite/unusedwrite.go +++ b/go/analysis/passes/unusedwrite/unusedwrite.go @@ -12,7 +12,6 @@ import ( "golang.org/x/tools/go/analysis/passes/buildssa" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ssa" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -32,7 +31,6 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) for _, fn := range ssainput.SrcFuncs { - // TODO(taking): Iterate over fn._Instantiations() once exported. If so, have 1 report per Pos(). reports := checkStores(fn) for _, store := range reports { switch addr := store.Addr.(type) { @@ -143,7 +141,7 @@ func hasStructOrArrayType(v ssa.Value) bool { // func (t T) f() { ...} // the receiver object is of type *T: // t0 = local T (t) *T - if tp, ok := aliases.Unalias(alloc.Type()).(*types.Pointer); ok { + if tp, ok := types.Unalias(alloc.Type()).(*types.Pointer); ok { return isStructOrArray(tp.Elem()) } return false diff --git a/go/ast/astutil/util.go b/go/ast/astutil/util.go index 6bdcf70ac27..ca71e3e1055 100644 --- a/go/ast/astutil/util.go +++ b/go/ast/astutil/util.go @@ -7,13 +7,5 @@ package astutil import "go/ast" // Unparen returns e with any enclosing parentheses stripped. -// TODO(adonovan): use go1.22's ast.Unparen. -func Unparen(e ast.Expr) ast.Expr { - for { - p, ok := e.(*ast.ParenExpr) - if !ok { - return e - } - e = p.X - } -} +// Deprecated: use [ast.Unparen]. +func Unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } diff --git a/go/ast/inspector/inspector.go b/go/ast/inspector/inspector.go index 1fc1de0bd10..0e0ba4c035c 100644 --- a/go/ast/inspector/inspector.go +++ b/go/ast/inspector/inspector.go @@ -73,6 +73,15 @@ func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) { // check, Preorder is almost twice as fast as Nodes. The two // features seem to contribute similar slowdowns (~1.4x each). + // This function is equivalent to the PreorderSeq call below, + // but to avoid the additional dynamic call (which adds 13-35% + // to the benchmarks), we expand it out. + // + // in.PreorderSeq(types...)(func(n ast.Node) bool { + // f(n) + // return true + // }) + mask := maskOf(types) for i := 0; i < len(in.events); { ev := in.events[i] diff --git a/go/ast/inspector/inspector_test.go b/go/ast/inspector/inspector_test.go index 5d7cb6e44eb..a19ba653e0a 100644 --- a/go/ast/inspector/inspector_test.go +++ b/go/ast/inspector/inspector_test.go @@ -160,7 +160,8 @@ func TestInspectPruning(t *testing.T) { compare(t, nodesA, nodesB) } -func compare(t *testing.T, nodesA, nodesB []ast.Node) { +// compare calls t.Error if !slices.Equal(nodesA, nodesB). +func compare[N comparable](t *testing.T, nodesA, nodesB []N) { if len(nodesA) != len(nodesB) { t.Errorf("inconsistent node lists: %d vs %d", len(nodesA), len(nodesB)) } else { diff --git a/go/ast/inspector/iter.go b/go/ast/inspector/iter.go new file mode 100644 index 00000000000..b7e959114cb --- /dev/null +++ b/go/ast/inspector/iter.go @@ -0,0 +1,85 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.23 + +package inspector + +import ( + "go/ast" + "iter" +) + +// PreorderSeq returns an iterator that visits all the +// nodes of the files supplied to New in depth-first order. +// It visits each node n before n's children. +// The complete traversal sequence is determined by ast.Inspect. +// +// The types argument, if non-empty, enables type-based +// filtering of events: only nodes whose type matches an +// element of the types slice are included in the sequence. +func (in *Inspector) PreorderSeq(types ...ast.Node) iter.Seq[ast.Node] { + + // This implementation is identical to Preorder, + // except that it supports breaking out of the loop. + + return func(yield func(ast.Node) bool) { + mask := maskOf(types) + for i := 0; i < len(in.events); { + ev := in.events[i] + if ev.index > i { + // push + if ev.typ&mask != 0 { + if !yield(ev.node) { + break + } + } + pop := ev.index + if in.events[pop].typ&mask == 0 { + // Subtrees do not contain types: skip them and pop. + i = pop + 1 + continue + } + } + i++ + } + } +} + +// All[N] returns an iterator over all the nodes of type N. +// N must be a pointer-to-struct type that implements ast.Node. +// +// Example: +// +// for call := range All[*ast.CallExpr](in) { ... } +func All[N interface { + *S + ast.Node +}, S any](in *Inspector) iter.Seq[N] { + + // To avoid additional dynamic call overheads, + // we duplicate rather than call the logic of PreorderSeq. + + mask := typeOf((N)(nil)) + return func(yield func(N) bool) { + for i := 0; i < len(in.events); { + ev := in.events[i] + if ev.index > i { + // push + if ev.typ&mask != 0 { + if !yield(ev.node.(N)) { + break + } + } + pop := ev.index + if in.events[pop].typ&mask == 0 { + // Subtrees do not contain types: skip them and pop. + i = pop + 1 + continue + } + } + i++ + } + } +} diff --git a/go/ast/inspector/iter_test.go b/go/ast/inspector/iter_test.go new file mode 100644 index 00000000000..2f52998c558 --- /dev/null +++ b/go/ast/inspector/iter_test.go @@ -0,0 +1,83 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.23 + +package inspector_test + +import ( + "go/ast" + "iter" + "slices" + "testing" + + "golang.org/x/tools/go/ast/inspector" +) + +// TestPreorderSeq checks PreorderSeq against Preorder. +func TestPreorderSeq(t *testing.T) { + inspect := inspector.New(netFiles) + + nodeFilter := []ast.Node{(*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)} + + // reference implementation + var want []ast.Node + inspect.Preorder(nodeFilter, func(n ast.Node) { + want = append(want, n) + }) + + // Check entire sequence. + got := slices.Collect(inspect.PreorderSeq(nodeFilter...)) + compare(t, got, want) + + // Check that break works. + got = firstN(10, inspect.PreorderSeq(nodeFilter...)) + compare(t, got, want[:10]) +} + +// TestAll checks All against Preorder. +func TestAll(t *testing.T) { + inspect := inspector.New(netFiles) + + // reference implementation + var want []*ast.CallExpr + inspect.Preorder([]ast.Node{(*ast.CallExpr)(nil)}, func(n ast.Node) { + want = append(want, n.(*ast.CallExpr)) + }) + + // Check entire sequence. + got := slices.Collect(inspector.All[*ast.CallExpr](inspect)) + compare(t, got, want) + + // Check that break works. + got = firstN(10, inspector.All[*ast.CallExpr](inspect)) + compare(t, got, want[:10]) +} + +// firstN(n, seq), returns a slice of up to n elements of seq. +func firstN[T any](n int, seq iter.Seq[T]) (res []T) { + for x := range seq { + res = append(res, x) + if len(res) == n { + break + } + } + return res +} + +// BenchmarkAllCalls is like BenchmarkInspectCalls, +// but using the single-type filtering iterator, All. +// (The iterator adds about 5-15%.) +func BenchmarkAllCalls(b *testing.B) { + inspect := inspector.New(netFiles) + b.ResetTimer() + + // Measure marginal cost of traversal. + var ncalls int + for range b.N { + for range inspector.All[*ast.CallExpr](inspect) { + ncalls++ + } + } +} diff --git a/go/callgraph/callgraph_test.go b/go/callgraph/callgraph_test.go index aa8aca08597..f90c52f8541 100644 --- a/go/callgraph/callgraph_test.go +++ b/go/callgraph/callgraph_test.go @@ -4,7 +4,6 @@ package callgraph_test import ( - "log" "sync" "testing" @@ -13,9 +12,10 @@ import ( "golang.org/x/tools/go/callgraph/rta" "golang.org/x/tools/go/callgraph/static" "golang.org/x/tools/go/callgraph/vta" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/txtar" ) // Benchmarks comparing different callgraph algorithms implemented in @@ -47,7 +47,12 @@ import ( // CHA callgraph. // * All algorithms are unsound w.r.t. reflection. -const httpEx = `package main +const httpEx = ` +-- go.mod -- +module x.io + +-- main.go -- +package main import ( "fmt" @@ -70,23 +75,12 @@ var ( main *ssa.Function ) -func example() (*ssa.Program, *ssa.Function) { +func example(t testing.TB) (*ssa.Program, *ssa.Function) { once.Do(func() { - var conf loader.Config - f, err := conf.ParseFile("", httpEx) - if err != nil { - log.Fatal(err) - } - conf.CreateFromFiles(f.Name.Name, f) - - lprog, err := conf.Load() - if err != nil { - log.Fatalf("test 'package %s': Load: %s", f.Name.Name, err) - } - prog = ssautil.CreateProgram(lprog, ssa.InstantiateGenerics) + pkgs := testfiles.LoadPackages(t, txtar.Parse([]byte(httpEx)), ".") + prog, ssapkgs := ssautil.Packages(pkgs, ssa.InstantiateGenerics) prog.Build() - - main = prog.Package(lprog.Created[0].Pkg).Members["main"].(*ssa.Function) + main = ssapkgs[0].Members["main"].(*ssa.Function) }) return prog, main } @@ -106,7 +100,7 @@ func logStats(b *testing.B, cnd bool, name string, cg *callgraph.Graph, main *ss func BenchmarkStatic(b *testing.B) { b.StopTimer() - prog, main := example() + prog, main := example(b) b.StartTimer() for i := 0; i < b.N; i++ { @@ -117,7 +111,7 @@ func BenchmarkStatic(b *testing.B) { func BenchmarkCHA(b *testing.B) { b.StopTimer() - prog, main := example() + prog, main := example(b) b.StartTimer() for i := 0; i < b.N; i++ { @@ -128,7 +122,7 @@ func BenchmarkCHA(b *testing.B) { func BenchmarkRTA(b *testing.B) { b.StopTimer() - _, main := example() + _, main := example(b) b.StartTimer() for i := 0; i < b.N; i++ { @@ -140,7 +134,7 @@ func BenchmarkRTA(b *testing.B) { func BenchmarkVTA(b *testing.B) { b.StopTimer() - prog, main := example() + prog, main := example(b) b.StartTimer() for i := 0; i < b.N; i++ { @@ -151,7 +145,7 @@ func BenchmarkVTA(b *testing.B) { func BenchmarkVTA2(b *testing.B) { b.StopTimer() - prog, main := example() + prog, main := example(b) b.StartTimer() for i := 0; i < b.N; i++ { @@ -163,7 +157,7 @@ func BenchmarkVTA2(b *testing.B) { func BenchmarkVTA3(b *testing.B) { b.StopTimer() - prog, main := example() + prog, main := example(b) b.StartTimer() for i := 0; i < b.N; i++ { @@ -176,7 +170,7 @@ func BenchmarkVTA3(b *testing.B) { func BenchmarkVTAAlt(b *testing.B) { b.StopTimer() - prog, main := example() + prog, main := example(b) b.StartTimer() for i := 0; i < b.N; i++ { @@ -188,7 +182,7 @@ func BenchmarkVTAAlt(b *testing.B) { func BenchmarkVTAAlt2(b *testing.B) { b.StopTimer() - prog, main := example() + prog, main := example(b) b.StartTimer() for i := 0; i < b.N; i++ { diff --git a/go/callgraph/cha/cha_test.go b/go/callgraph/cha/cha_test.go index b8cdaa3e578..5ac64e17244 100644 --- a/go/callgraph/cha/cha_test.go +++ b/go/callgraph/cha/cha_test.go @@ -13,21 +13,22 @@ import ( "bytes" "fmt" "go/ast" - "go/build" - "go/parser" "go/token" "go/types" "os" + "path/filepath" "sort" "strings" "testing" - "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/cha" - "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/txtar" ) var inputs = []string{ @@ -52,23 +53,19 @@ func expectation(f *ast.File) (string, token.Pos) { // the WANT comment at the end of the file. func TestCHA(t *testing.T) { for _, filename := range inputs { - prog, f, mainPkg, err := loadProgInfo(filename, ssa.InstantiateGenerics) - if err != nil { - t.Error(err) - continue - } + pkg, ssapkg := loadFile(t, filename, ssa.InstantiateGenerics) - want, pos := expectation(f) + want, pos := expectation(pkg.Syntax[0]) if pos == token.NoPos { t.Error(fmt.Errorf("No WANT: comment in %s", filename)) continue } - cg := cha.CallGraph(prog) + cg := cha.CallGraph(ssapkg.Prog) - if got := printGraph(cg, mainPkg.Pkg, "dynamic", "Dynamic calls"); got != want { + if got := printGraph(cg, pkg.Types, "dynamic", "Dynamic calls"); got != want { t.Errorf("%s: got:\n%s\nwant:\n%s", - prog.Fset.Position(pos), got, want) + ssapkg.Prog.Fset.Position(pos), got, want) } } } @@ -76,21 +73,18 @@ func TestCHA(t *testing.T) { // TestCHAGenerics is TestCHA tailored for testing generics, func TestCHAGenerics(t *testing.T) { filename := "testdata/generics.go" - prog, f, mainPkg, err := loadProgInfo(filename, ssa.InstantiateGenerics) - if err != nil { - t.Fatal(err) - } + pkg, ssapkg := loadFile(t, filename, ssa.InstantiateGenerics) - want, pos := expectation(f) + want, pos := expectation(pkg.Syntax[0]) if pos == token.NoPos { t.Fatal(fmt.Errorf("No WANT: comment in %s", filename)) } - cg := cha.CallGraph(prog) + cg := cha.CallGraph(ssapkg.Prog) - if got := printGraph(cg, mainPkg.Pkg, "", "All calls"); got != want { + if got := printGraph(cg, pkg.Types, "", "All calls"); got != want { t.Errorf("%s: got:\n%s\nwant:\n%s", - prog.Fset.Position(pos), got, want) + ssapkg.Prog.Fset.Position(pos), got, want) } } @@ -109,39 +103,43 @@ func TestCHAUnexported(t *testing.T) { // We use CHA to build a callgraph, then check that it has the // appropriate set of edges. - main := `package main - import "p2" - type I1 interface { m() } - type S1 struct { p2.I2 } - func (s S1) m() { } - func main() { - var s S1 - var o I1 = s - o.m() - p2.Foo(s) - }` - - p2 := `package p2 - type I2 interface { m() } - type S2 struct { } - func (s S2) m() { } - func Foo(i I2) { i.m() }` + const src = ` +-- go.mod -- +module x.io +go 1.18 + +-- main/main.go -- +package main + +import "x.io/p2" + +type I1 interface { m() } +type S1 struct { p2.I2 } +func (s S1) m() { } +func main() { + var s S1 + var o I1 = s + o.m() + p2.Foo(s) +} + +-- p2/p2.go -- +package p2 + +type I2 interface { m() } +type S2 struct { } +func (s S2) m() { } +func Foo(i I2) { i.m() } +` want := `All calls - main.init --> p2.init - main.main --> (main.S1).m - main.main --> p2.Foo - p2.Foo --> (p2.S2).m` + x.io/main.init --> x.io/p2.init + x.io/main.main --> (x.io/main.S1).m + x.io/main.main --> x.io/p2.Foo + x.io/p2.Foo --> (x.io/p2.S2).m` - conf := loader.Config{ - Build: fakeContext(map[string]string{"main": main, "p2": p2}), - } - conf.Import("main") - iprog, err := conf.Load() - if err != nil { - t.Fatalf("Load failed: %v", err) - } - prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics) + pkgs := testfiles.LoadPackages(t, txtar.Parse([]byte(src)), "./...") + prog, _ := ssautil.Packages(pkgs, ssa.InstantiateGenerics) prog.Build() cg := cha.CallGraph(prog) @@ -154,39 +152,35 @@ func TestCHAUnexported(t *testing.T) { } } -// Simplifying wrapper around buildutil.FakeContext for single-file packages. -func fakeContext(pkgs map[string]string) *build.Context { - pkgs2 := make(map[string]map[string]string) - for path, content := range pkgs { - pkgs2[path] = map[string]string{"x.go": content} - } - return buildutil.FakeContext(pkgs2) -} +// loadFile loads a built SSA package for a single-file "x.io/main" package. +// (Ideally all uses would be converted over to txtar files with explicit go.mod files.) +func loadFile(t testing.TB, filename string, mode ssa.BuilderMode) (*packages.Package, *ssa.Package) { + testenv.NeedsGoPackages(t) -func loadProgInfo(filename string, mode ssa.BuilderMode) (*ssa.Program, *ast.File, *ssa.Package, error) { - content, err := os.ReadFile(filename) + data, err := os.ReadFile(filename) if err != nil { - return nil, nil, nil, fmt.Errorf("couldn't read file '%s': %s", filename, err) + t.Fatal(err) } - - conf := loader.Config{ - ParserMode: parser.ParseComments, + dir := t.TempDir() + cfg := &packages.Config{ + Mode: packages.LoadAllSyntax, + Dir: dir, + Overlay: map[string][]byte{ + filepath.Join(dir, "go.mod"): []byte("module x.io\ngo 1.22"), + filepath.Join(dir, "main/main.go"): data, + }, + Env: append(os.Environ(), "GO111MODULES=on", "GOPATH=", "GOWORK=off", "GOPROXY=off"), } - f, err := conf.ParseFile(filename, content) + pkgs, err := packages.Load(cfg, "./main") if err != nil { - return nil, nil, nil, err + t.Fatal(err) } - - conf.CreateFromFiles("main", f) - iprog, err := conf.Load() - if err != nil { - return nil, nil, nil, err + if num := packages.PrintErrors(pkgs); num > 0 { + t.Fatalf("packages contained %d errors", num) } - - prog := ssautil.CreateProgram(iprog, mode) + prog, ssapkgs := ssautil.Packages(pkgs, mode) prog.Build() - - return prog, f, prog.Package(iprog.Created[0].Pkg), nil + return pkgs[0], ssapkgs[0] } // printGraph returns a string representation of cg involving only edges diff --git a/go/callgraph/cha/testdata/func.go b/go/callgraph/cha/testdata/func.go index a12f3f1fd3f..4db581d9bc9 100644 --- a/go/callgraph/cha/testdata/func.go +++ b/go/callgraph/cha/testdata/func.go @@ -1,5 +1,3 @@ -// +build ignore - package main // Test of dynamic function calls; no interfaces. diff --git a/go/callgraph/cha/testdata/generics.go b/go/callgraph/cha/testdata/generics.go index 0323c7582b6..3a63091b1bd 100644 --- a/go/callgraph/cha/testdata/generics.go +++ b/go/callgraph/cha/testdata/generics.go @@ -1,6 +1,3 @@ -//go:build ignore -// +build ignore - package main // Test of generic function calls. @@ -38,12 +35,12 @@ func f(h func(), g func(I), k func(A), a A, b B) { // (*A).Foo --> (A).Foo // (*B).Foo --> (B).Foo // f --> Bar -// f --> instantiated[main.A] -// f --> instantiated[main.A] -// f --> instantiated[main.B] +// f --> instantiated[x.io/main.A] +// f --> instantiated[x.io/main.A] +// f --> instantiated[x.io/main.B] // instantiated --> (*A).Foo // instantiated --> (*B).Foo // instantiated --> (A).Foo // instantiated --> (B).Foo -// instantiated[main.A] --> (A).Foo -// instantiated[main.B] --> (B).Foo +// instantiated[x.io/main.A] --> (A).Foo +// instantiated[x.io/main.B] --> (B).Foo diff --git a/go/callgraph/cha/testdata/iface.go b/go/callgraph/cha/testdata/iface.go index 8ca65e160aa..cd147f96d3b 100644 --- a/go/callgraph/cha/testdata/iface.go +++ b/go/callgraph/cha/testdata/iface.go @@ -1,5 +1,3 @@ -// +build ignore - package main // Test of interface calls. None of the concrete types are ever diff --git a/go/callgraph/cha/testdata/recv.go b/go/callgraph/cha/testdata/recv.go index a92255e06db..0ff16d3b34a 100644 --- a/go/callgraph/cha/testdata/recv.go +++ b/go/callgraph/cha/testdata/recv.go @@ -1,5 +1,3 @@ -// +build ignore - package main type I interface { diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go index cd3afa0be74..b489b0178c8 100644 --- a/go/callgraph/rta/rta.go +++ b/go/callgraph/rta/rta.go @@ -45,7 +45,6 @@ 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/aliases" ) // A Result holds the results of Rapid Type Analysis, which includes the @@ -374,7 +373,7 @@ func (r *rta) interfaces(C types.Type) []*types.Interface { // and update the 'implements' relation. r.interfaceTypes.Iterate(func(I types.Type, v interface{}) { iinfo := v.(*interfaceTypeInfo) - if I := aliases.Unalias(I).(*types.Interface); implements(cinfo, iinfo) { + if I := types.Unalias(I).(*types.Interface); implements(cinfo, iinfo) { iinfo.implementations = append(iinfo.implementations, C) cinfo.implements = append(cinfo.implements, I) } @@ -417,7 +416,7 @@ func (r *rta) implementations(I *types.Interface) []types.Type { // Adapted from needMethods in go/ssa/builder.go func (r *rta) addRuntimeType(T types.Type, skip bool) { // Never record aliases. - T = aliases.Unalias(T) + T = types.Unalias(T) if prev, ok := r.result.RuntimeTypes.At(T).(bool); ok { if skip && !prev { @@ -456,11 +455,11 @@ func (r *rta) addRuntimeType(T types.Type, skip bool) { // Each package maintains its own set of types it has visited. var n *types.Named - switch T := aliases.Unalias(T).(type) { + switch T := types.Unalias(T).(type) { case *types.Named: n = T case *types.Pointer: - n, _ = aliases.Unalias(T.Elem()).(*types.Named) + n, _ = types.Unalias(T.Elem()).(*types.Named) } if n != nil { owner := n.Obj().Pkg() @@ -479,7 +478,7 @@ func (r *rta) addRuntimeType(T types.Type, skip bool) { } switch t := T.(type) { - case *aliases.Alias: + case *types.Alias: panic("unreachable") case *types.Basic: diff --git a/go/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go index 49e21330738..6e0b2dda7b5 100644 --- a/go/callgraph/rta/rta_test.go +++ b/go/callgraph/rta/rta_test.go @@ -19,11 +19,8 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/rta" - "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/aliases" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/testfiles" "golang.org/x/tools/txtar" ) @@ -43,7 +40,12 @@ func TestRTA(t *testing.T) { } for _, archive := range archivePaths { t.Run(archive, func(t *testing.T) { - pkgs := loadPackages(t, archive) + ar, err := txtar.ParseFile(archive) + if err != nil { + t.Fatal(err) + } + + pkgs := testfiles.LoadPackages(t, ar, "./...") // find the file which contains the expected result var f *ast.File @@ -84,42 +86,6 @@ func TestRTA(t *testing.T) { } } -// loadPackages unpacks the archive to a temporary directory and loads all packages within it. -func loadPackages(t *testing.T, archive string) []*packages.Package { - testenv.NeedsGoPackages(t) - - ar, err := txtar.ParseFile(archive) - if err != nil { - t.Fatal(err) - } - - fs, err := txtar.FS(ar) - if err != nil { - t.Fatal(err) - } - dir := testfiles.CopyToTmp(t, fs) - - var baseConfig = &packages.Config{ - Mode: packages.NeedSyntax | - packages.NeedTypesInfo | - packages.NeedDeps | - packages.NeedName | - packages.NeedFiles | - packages.NeedImports | - packages.NeedCompiledGoFiles | - packages.NeedTypes, - Dir: dir, - } - pkgs, err := packages.Load(baseConfig, "./...") - if err != nil { - t.Fatal(err) - } - if num := packages.PrintErrors(pkgs); num > 0 { - t.Fatalf("packages contained %d errors", num) - } - return pkgs -} - // check tests the RTA analysis results against the test expectations // defined by a comment starting with a line "WANT:". // @@ -257,7 +223,7 @@ func check(t *testing.T, f *ast.File, pkg *ssa.Package, res *rta.Result) { got := make(stringset) res.RuntimeTypes.Iterate(func(key types.Type, value interface{}) { if !value.(bool) { // accessible to reflection - typ := types.TypeString(aliases.Unalias(key), types.RelativeTo(pkg.Pkg)) + typ := types.TypeString(types.Unalias(key), types.RelativeTo(pkg.Pkg)) got[typ] = true } }) diff --git a/go/callgraph/static/static_test.go b/go/callgraph/static/static_test.go index cf8392d2f7b..a0c587824d7 100644 --- a/go/callgraph/static/static_test.go +++ b/go/callgraph/static/static_test.go @@ -6,19 +6,27 @@ package static_test import ( "fmt" - "go/parser" "reflect" "sort" "testing" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/static" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/txtar" ) -const input = `package main +const input = ` + +-- go.mod -- +module x.io + +go 1.22 + +-- p/p.go -- +package main type C int func (C) f() @@ -51,7 +59,15 @@ func main() { } ` -const genericsInput = `package P +const genericsInput = ` + +-- go.mod -- +module x.io + +go 1.22 + +-- p/p.go -- +package p type I interface { F() @@ -83,50 +99,34 @@ func TestStatic(t *testing.T) { for _, e := range []struct { input string want []string - // typeparams must be true if input uses type parameters - typeparams bool }{ {input, []string{ "(*C).f -> (C).f", "f -> (C).f", "f -> f$1", "f -> g", - }, false}, + }}, {genericsInput, []string{ "(*A).F -> (A).F", "(*B).F -> (B).F", - "f -> instantiated[P.A]", - "f -> instantiated[P.B]", - "instantiated[P.A] -> (A).F", - "instantiated[P.B] -> (B).F", - }, true}, + "f -> instantiated[x.io/p.A]", + "f -> instantiated[x.io/p.B]", + "instantiated[x.io/p.A] -> (A).F", + "instantiated[x.io/p.B] -> (B).F", + }}, } { - conf := loader.Config{ParserMode: parser.ParseComments} - f, err := conf.ParseFile("P.go", e.input) - if err != nil { - t.Error(err) - continue - } - - conf.CreateFromFiles("P", f) - iprog, err := conf.Load() - if err != nil { - t.Error(err) - continue - } - - P := iprog.Created[0].Pkg - - prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics) + pkgs := testfiles.LoadPackages(t, txtar.Parse([]byte(e.input)), "./p") + prog, _ := ssautil.Packages(pkgs, ssa.InstantiateGenerics) prog.Build() + p := pkgs[0].Types cg := static.CallGraph(prog) var edges []string callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error { edges = append(edges, fmt.Sprintf("%s -> %s", - e.Caller.Func.RelString(P), - e.Callee.Func.RelString(P))) + e.Caller.Func.RelString(p), + e.Callee.Func.RelString(p))) return nil }) sort.Strings(edges) diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 1a9ed7cb321..61e841a39ae 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -11,7 +11,6 @@ import ( "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -676,7 +675,7 @@ func (b *builder) multiconvert(c *ssa.MultiConvert) { // This is a adaptation of x/exp/typeparams.NormalTerms which x/tools cannot depend on. var terms []*types.Term var err error - switch typ := aliases.Unalias(typ).(type) { + switch typ := types.Unalias(typ).(type) { case *types.TypeParam: terms, err = typeparams.StructuralTerms(typ) case *types.Union: @@ -745,7 +744,7 @@ func (b *builder) addInFlowEdge(s, d node) { // Creates const, pointer, global, func, and local nodes based on register instructions. func (b *builder) nodeFromVal(val ssa.Value) node { - if p, ok := aliases.Unalias(val.Type()).(*types.Pointer); ok && !types.IsInterface(p.Elem()) && !isFunction(p.Elem()) { + if p, ok := types.Unalias(val.Type()).(*types.Pointer); ok && !types.IsInterface(p.Elem()) && !isFunction(p.Elem()) { // Nested pointer to interfaces are modeled as a special // nestedPtrInterface node. if i := interfaceUnderPtr(p.Elem()); i != nil { diff --git a/go/callgraph/vta/graph_test.go b/go/callgraph/vta/graph_test.go index b32da4f54a6..ace80859e21 100644 --- a/go/callgraph/vta/graph_test.go +++ b/go/callgraph/vta/graph_test.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/go/callgraph/cha" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/aliases" ) func TestNodeInterface(t *testing.T) { @@ -27,7 +26,7 @@ func TestNodeInterface(t *testing.T) { // - "foo" function // - "main" function and its // - first register instruction t0 := *gl - prog, _, err := testProg("testdata/src/simple.go", ssa.BuilderMode(0)) + prog, _, err := testProg(t, "testdata/src/simple.go", ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load testdata/src/simple.go program: %v", err) } @@ -38,7 +37,7 @@ func TestNodeInterface(t *testing.T) { reg := firstRegInstr(main) // t0 := *gl X := pkg.Type("X").Type() gl := pkg.Var("gl") - glPtrType, ok := aliases.Unalias(gl.Type()).(*types.Pointer) + glPtrType, ok := types.Unalias(gl.Type()).(*types.Pointer) if !ok { t.Fatalf("could not cast gl variable to pointer type") } @@ -72,8 +71,8 @@ func TestNodeInterface(t *testing.T) { {panicArg{}, "Panic", nil}, {recoverReturn{}, "Recover", nil}, } { - if test.s != test.n.String() { - t.Errorf("want %s; got %s", test.s, test.n.String()) + if removeModulePrefix(test.s) != removeModulePrefix(test.n.String()) { + t.Errorf("want %s; got %s", removeModulePrefix(test.s), removeModulePrefix(test.n.String())) } if test.t != test.n.Type() { t.Errorf("want %s; got %s", test.t, test.n.Type()) @@ -81,9 +80,15 @@ func TestNodeInterface(t *testing.T) { } } +// removeModulePrefix removes the "x.io/" module name prefix throughout s. +// (It is added by testProg.) +func removeModulePrefix(s string) string { + return strings.ReplaceAll(s, "x.io/", "") +} + func TestVtaGraph(t *testing.T) { // Get the basic type int from a real program. - prog, _, err := testProg("testdata/src/simple.go", ssa.BuilderMode(0)) + prog, _, err := testProg(t, "testdata/src/simple.go", ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load testdata/src/simple.go program: %v", err) } @@ -151,7 +156,7 @@ func vtaGraphStr(g vtaGraph) []string { } sort.Strings(succStr) entry := fmt.Sprintf("%v -> %v", n.String(), strings.Join(succStr, ", ")) - vgs = append(vgs, entry) + vgs = append(vgs, removeModulePrefix(entry)) } return vgs } @@ -197,7 +202,7 @@ func TestVTAGraphConstruction(t *testing.T) { "testdata/src/panic.go", } { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file, ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } diff --git a/go/callgraph/vta/helpers_test.go b/go/callgraph/vta/helpers_test.go index 64e26f1a479..59a9277f759 100644 --- a/go/callgraph/vta/helpers_test.go +++ b/go/callgraph/vta/helpers_test.go @@ -8,16 +8,17 @@ import ( "bytes" "fmt" "go/ast" - "go/parser" "os" + "path/filepath" "sort" "strings" "testing" "golang.org/x/tools/go/callgraph" + "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" ) @@ -37,32 +38,49 @@ func want(f *ast.File) []string { // testProg returns an ssa representation of a program at // `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 := os.ReadFile(path) - if err != nil { - return nil, nil, err - } +func testProg(t testing.TB, path string, mode ssa.BuilderMode) (*ssa.Program, []string, error) { + // Set debug mode to exercise DebugRef instructions. + pkg, ssapkg := loadFile(t, path, mode|ssa.GlobalDebug) + return ssapkg.Prog, want(pkg.Syntax[0]), nil +} - conf := loader.Config{ - ParserMode: parser.ParseComments, - } +// loadFile loads a built SSA package for a single-file package "x.io/testdata". +// (Ideally all uses would be converted over to txtar files with explicit go.mod files.) +// +// TODO(adonovan): factor with similar loadFile in cha/cha_test.go. +func loadFile(t testing.TB, filename string, mode ssa.BuilderMode) (*packages.Package, *ssa.Package) { + testenv.NeedsGoPackages(t) - f, err := conf.ParseFile(path, content) + data, err := os.ReadFile(filename) if err != nil { - return nil, nil, err + t.Fatal(err) } - - conf.CreateFromFiles("testdata", f) - iprog, err := conf.Load() + dir := t.TempDir() + cfg := &packages.Config{ + Mode: packages.LoadAllSyntax, + Dir: dir, + Overlay: map[string][]byte{ + filepath.Join(dir, "go.mod"): fmt.Appendf(nil, "module x.io\ngo 1.%d", testenv.Go1Point()), + + filepath.Join(dir, "testdata", filepath.Base(filename)): data, + }, + } + pkgs, err := packages.Load(cfg, "./testdata") if err != nil { - return nil, nil, err + t.Fatal(err) } - - prog := ssautil.CreateProgram(iprog, mode) - // Set debug mode to exercise DebugRef instructions. - prog.Package(iprog.Created[0].Pkg).SetDebugMode(true) + if len(pkgs) != 1 { + t.Fatalf("got %d packages, want 1", len(pkgs)) + } + if len(pkgs[0].Syntax) != 1 { + t.Fatalf("got %d files, want 1", len(pkgs[0].Syntax)) + } + if num := packages.PrintErrors(pkgs); num > 0 { + t.Fatalf("packages contained %d errors", num) + } + prog, ssapkgs := ssautil.Packages(pkgs, mode) prog.Build() - return prog, want(f), nil + return pkgs[0], ssapkgs[0] } func firstRegInstr(f *ssa.Function) ssa.Value { @@ -112,7 +130,7 @@ func callGraphStr(g *callgraph.Graph) []string { sort.Strings(cs) entry := fmt.Sprintf("%v: %v", funcName(f), strings.Join(cs, "; ")) - gs = append(gs, entry) + gs = append(gs, removeModulePrefix(entry)) } return gs } diff --git a/go/callgraph/vta/testdata/src/simple.go b/go/callgraph/vta/testdata/src/simple.go index 71ddbe37163..24f2d458834 100644 --- a/go/callgraph/vta/testdata/src/simple.go +++ b/go/callgraph/vta/testdata/src/simple.go @@ -2,9 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// go:build ignore - -package testdata +package main var gl int diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go index 141eb077f9c..bbd8400ec9b 100644 --- a/go/callgraph/vta/utils.go +++ b/go/callgraph/vta/utils.go @@ -8,7 +8,6 @@ import ( "go/types" "golang.org/x/tools/go/ssa" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -24,7 +23,7 @@ func isReferenceNode(n node) bool { return true } - if _, ok := aliases.Unalias(n.Type()).(*types.Pointer); ok { + if _, ok := types.Unalias(n.Type()).(*types.Pointer); ok { return true } @@ -162,7 +161,7 @@ func siteCallees(c ssa.CallInstruction, callees calleesFunc) func(yield func(*ss } func canHaveMethods(t types.Type) bool { - t = aliases.Unalias(t) + t = types.Unalias(t) if _, ok := t.(*types.Named); ok { return true } diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go index 1780bf6568a..ce441eb7e1b 100644 --- a/go/callgraph/vta/vta_test.go +++ b/go/callgraph/vta/vta_test.go @@ -48,7 +48,7 @@ func TestVTACallGraph(t *testing.T) { for _, file := range files { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file, ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } @@ -77,7 +77,7 @@ func TestVTACallGraph(t *testing.T) { // enabled by having an arbitrary function set as input to CallGraph // instead of the whole program (i.e., ssautil.AllFunctions(prog)). func TestVTAProgVsFuncSet(t *testing.T) { - prog, want, err := testProg("testdata/src/callgraph_nested_ptr.go", ssa.BuilderMode(0)) + prog, want, err := testProg(t, "testdata/src/callgraph_nested_ptr.go", ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test `testdata/src/callgraph_nested_ptr.go`: %s", err) } @@ -154,7 +154,7 @@ func TestVTACallGraphGenerics(t *testing.T) { } for _, file := range files { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(file, ssa.InstantiateGenerics) + prog, want, err := testProg(t, file, ssa.InstantiateGenerics) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } @@ -174,7 +174,7 @@ func TestVTACallGraphGenerics(t *testing.T) { func TestVTACallGraphGo117(t *testing.T) { file := "testdata/src/go117.go" - prog, want, err := testProg(file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file, ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } diff --git a/go/expect/extract.go b/go/expect/extract.go index c571c5ba4e9..1ca67d24958 100644 --- a/go/expect/extract.go +++ b/go/expect/extract.go @@ -42,7 +42,7 @@ func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error // there are ways you can break the parser such that it will not add all the // comments to the ast, which may result in files where the tests are silently // not run. - file, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors) + file, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors|parser.SkipObjectResolution) if file == nil { return nil, err } diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go index b0eb1ddf867..7a021ebb4b2 100644 --- a/go/internal/gccgoimporter/parser.go +++ b/go/internal/gccgoimporter/parser.go @@ -21,7 +21,6 @@ import ( "text/scanner" "unicode/utf8" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) @@ -256,7 +255,7 @@ func (p *parser) parseField(pkg *types.Package) (field *types.Var, tag string) { if aname, ok := p.aliases[n]; ok { name = aname } else { - switch typ := aliases.Unalias(typesinternal.Unpointer(typ)).(type) { + switch typ := types.Unalias(typesinternal.Unpointer(typ)).(type) { case *types.Basic: name = typ.Name() case *types.Named: @@ -575,7 +574,7 @@ func (p *parser) parseNamedType(nlist []interface{}) types.Type { t := obj.Type() p.update(t, nlist) - nt, ok := aliases.Unalias(t).(*types.Named) + nt, ok := types.Unalias(t).(*types.Named) if !ok { // This can happen for unsafe.Pointer, which is a TypeName holding a Basic type. pt := p.parseType(pkg) @@ -1330,7 +1329,7 @@ func (p *parser) parsePackage() *types.Package { } p.fixups = nil for _, typ := range p.typeList { - if it, ok := aliases.Unalias(typ).(*types.Interface); ok { + if it, ok := types.Unalias(typ).(*types.Interface); ok { it.Complete() } } diff --git a/go/packages/doc.go b/go/packages/doc.go index 3531ac8f5fc..f1931d10eeb 100644 --- a/go/packages/doc.go +++ b/go/packages/doc.go @@ -64,7 +64,7 @@ graph using the Imports fields. The Load function can be configured by passing a pointer to a Config as the first argument. A nil Config is equivalent to the zero Config, which -causes Load to run in LoadFiles mode, collecting minimal information. +causes Load to run in [LoadFiles] mode, collecting minimal information. See the documentation for type Config for details. As noted earlier, the Config.Mode controls the amount of detail @@ -72,14 +72,14 @@ reported about the loaded packages. See the documentation for type LoadMode for details. Most tools should pass their command-line arguments (after any flags) -uninterpreted to [Load], so that it can interpret them +uninterpreted to Load, so that it can interpret them according to the conventions of the underlying build system. See the Example function for typical usage. # The driver protocol -[Load] may be used to load Go packages even in Go projects that use +Load may be used to load Go packages even in Go projects that use alternative build systems, by installing an appropriate "driver" program for the build system and specifying its location in the GOPACKAGESDRIVER environment variable. @@ -97,6 +97,15 @@ JSON-encoded [DriverRequest] message providing additional information is written to the driver's standard input. The driver must write a JSON-encoded [DriverResponse] message to its standard output. (This message differs from the JSON schema produced by 'go list'.) + +The value of the PWD environment variable seen by the driver process +is the preferred name of its working directory. (The working directory +may have other aliases due to symbolic links; see the comment on the +Dir field of [exec.Cmd] for related information.) +When the driver process emits in its response the name of a file +that is a descendant of this directory, it must use an absolute path +that has the value of PWD as a prefix, to ensure that the returned +filenames satisfy the original query. */ package packages // import "golang.org/x/tools/go/packages" diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go index 9a0e7ad92c2..7387e7fd10e 100644 --- a/go/packages/gopackages/main.go +++ b/go/packages/gopackages/main.go @@ -6,6 +6,9 @@ // how to use golang.org/x/tools/go/packages to load, parse, // type-check, and print one or more Go packages. // Its precise output is unspecified and may change. + +//go:debug gotypesalias=0 + package main import ( @@ -37,6 +40,7 @@ type application struct { Deps bool `flag:"deps" help:"show dependencies too"` Test bool `flag:"test" help:"include any tests implied by the patterns"` Mode string `flag:"mode" help:"mode (one of files, imports, types, syntax, allsyntax)"` + Tags string `flag:"tags" help:"comma-separated list of extra build tags (see: go help buildconstraint)"` Private bool `flag:"private" help:"show non-exported declarations too (if -mode=syntax)"` PrintJSON bool `flag:"json" help:"print package in JSON form"` BuildFlags stringListValue `flag:"buildflag" help:"pass argument to underlying build system (may be repeated)"` @@ -95,7 +99,7 @@ func (app *application) Run(ctx context.Context, args ...string) error { cfg := &packages.Config{ Mode: packages.LoadSyntax, Tests: app.Test, - BuildFlags: app.BuildFlags, + BuildFlags: append([]string{"-tags=" + app.Tags}, app.BuildFlags...), Env: env, } diff --git a/go/packages/internal/nodecount/nodecount.go b/go/packages/internal/nodecount/nodecount.go index a9f25bfdc6c..4b36a579ac0 100644 --- a/go/packages/internal/nodecount/nodecount.go +++ b/go/packages/internal/nodecount/nodecount.go @@ -13,6 +13,9 @@ // A typical distribution is 40% identifiers, 10% literals, 8% // selectors, and 6% calls; around 3% each of BinaryExpr, BlockStmt, // AssignStmt, Field, and Comment; and the rest accounting for 20%. + +//go:debug gotypesalias=0 + package main import ( diff --git a/go/packages/loadmode_string.go b/go/packages/loadmode_string.go index 5c080d21b54..5fcad6ea6db 100644 --- a/go/packages/loadmode_string.go +++ b/go/packages/loadmode_string.go @@ -9,49 +9,46 @@ import ( "strings" ) -var allModes = []LoadMode{ - NeedName, - NeedFiles, - NeedCompiledGoFiles, - NeedImports, - NeedDeps, - NeedExportFile, - NeedTypes, - NeedSyntax, - NeedTypesInfo, - NeedTypesSizes, +var modes = [...]struct { + mode LoadMode + name string +}{ + {NeedName, "NeedName"}, + {NeedFiles, "NeedFiles"}, + {NeedCompiledGoFiles, "NeedCompiledGoFiles"}, + {NeedImports, "NeedImports"}, + {NeedDeps, "NeedDeps"}, + {NeedExportFile, "NeedExportFile"}, + {NeedTypes, "NeedTypes"}, + {NeedSyntax, "NeedSyntax"}, + {NeedTypesInfo, "NeedTypesInfo"}, + {NeedTypesSizes, "NeedTypesSizes"}, + {NeedModule, "NeedModule"}, + {NeedEmbedFiles, "NeedEmbedFiles"}, + {NeedEmbedPatterns, "NeedEmbedPatterns"}, } -var modeStrings = []string{ - "NeedName", - "NeedFiles", - "NeedCompiledGoFiles", - "NeedImports", - "NeedDeps", - "NeedExportFile", - "NeedTypes", - "NeedSyntax", - "NeedTypesInfo", - "NeedTypesSizes", -} - -func (mod LoadMode) String() string { - m := mod - if m == 0 { +func (mode LoadMode) String() string { + if mode == 0 { return "LoadMode(0)" } var out []string - for i, x := range allModes { - if x > m { - break + // named bits + for _, item := range modes { + if (mode & item.mode) != 0 { + mode ^= item.mode + out = append(out, item.name) } - if (m & x) != 0 { - out = append(out, modeStrings[i]) - m = m ^ x + } + // unnamed residue + if mode != 0 { + if out == nil { + return fmt.Sprintf("LoadMode(%#x)", int(mode)) } + out = append(out, fmt.Sprintf("%#x", int(mode))) } - if m != 0 { - out = append(out, "Unknown") + if len(out) == 1 { + return out[0] } - return fmt.Sprintf("LoadMode(%s)", strings.Join(out, "|")) + return "(" + strings.Join(out, "|") + ")" } diff --git a/go/packages/packages.go b/go/packages/packages.go index 0b6bfaff808..f227f1bab10 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -46,10 +46,10 @@ import ( // // Unfortunately there are a number of open bugs related to // interactions among the LoadMode bits: -// - https://github.com/golang/go/issues/56633 -// - https://github.com/golang/go/issues/56677 -// - https://github.com/golang/go/issues/58726 -// - https://github.com/golang/go/issues/63517 +// - https://github.com/golang/go/issues/56633 +// - https://github.com/golang/go/issues/56677 +// - https://github.com/golang/go/issues/58726 +// - https://github.com/golang/go/issues/63517 type LoadMode int const ( @@ -103,25 +103,37 @@ const ( // NeedEmbedPatterns adds EmbedPatterns. NeedEmbedPatterns + + // Be sure to update loadmode_string.go when adding new items! ) const ( + // LoadFiles loads the name and file names for the initial packages. + // // Deprecated: LoadFiles exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadFiles = NeedName | NeedFiles | NeedCompiledGoFiles + // LoadImports loads the name, file names, and import mapping for the initial packages. + // // Deprecated: LoadImports exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadImports = LoadFiles | NeedImports + // LoadTypes loads exported type information for the initial packages. + // // Deprecated: LoadTypes exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadTypes = LoadImports | NeedTypes | NeedTypesSizes + // LoadSyntax loads typed syntax for the initial packages. + // // Deprecated: LoadSyntax exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadSyntax = LoadTypes | NeedSyntax | NeedTypesInfo + // LoadAllSyntax loads typed syntax for the initial packages and all dependencies. + // // Deprecated: LoadAllSyntax exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadAllSyntax = LoadSyntax | NeedDeps @@ -236,14 +248,13 @@ type Config struct { // Load loads and returns the Go packages named by the given patterns. // -// Config specifies loading options; -// nil behaves the same as an empty Config. +// The cfg parameter specifies loading options; nil behaves the same as an empty [Config]. // // The [Config.Mode] field is a set of bits that determine what kinds // of information should be computed and returned. Modes that require // more information tend to be slower. See [LoadMode] for details // and important caveats. Its zero value is equivalent to -// NeedName | NeedFiles | NeedCompiledGoFiles. +// [NeedName] | [NeedFiles] | [NeedCompiledGoFiles]. // // Each call to Load returns a new set of [Package] instances. // The Packages and their Imports form a directed acyclic graph. @@ -260,7 +271,7 @@ type Config struct { // Errors associated with a particular package are recorded in the // corresponding Package's Errors list, and do not cause Load to // return an error. Clients may need to handle such errors before -// proceeding with further analysis. The PrintErrors function is +// proceeding with further analysis. The [PrintErrors] function is // provided for convenient display of all errors. func Load(cfg *Config, patterns ...string) ([]*Package, error) { ld := newLoader(cfg) @@ -763,6 +774,7 @@ func newLoader(cfg *Config) *loader { // because we load source if export data is missing. if ld.ParseFile == nil { ld.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) { + // We implicitly promise to keep doing ast.Object resolution. :( const mode = parser.AllErrors | parser.ParseComments return parser.ParseFile(fset, filename, src, mode) } diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 26dbc13df31..e78d3cdb881 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2280,59 +2280,67 @@ func TestLoadModeStrings(t *testing.T) { }, { packages.NeedName, - "LoadMode(NeedName)", + "NeedName", }, { packages.NeedFiles, - "LoadMode(NeedFiles)", + "NeedFiles", }, { packages.NeedCompiledGoFiles, - "LoadMode(NeedCompiledGoFiles)", + "NeedCompiledGoFiles", }, { packages.NeedImports, - "LoadMode(NeedImports)", + "NeedImports", }, { packages.NeedDeps, - "LoadMode(NeedDeps)", + "NeedDeps", }, { packages.NeedExportFile, - "LoadMode(NeedExportFile)", + "NeedExportFile", }, { packages.NeedTypes, - "LoadMode(NeedTypes)", + "NeedTypes", }, { packages.NeedSyntax, - "LoadMode(NeedSyntax)", + "NeedSyntax", }, { packages.NeedTypesInfo, - "LoadMode(NeedTypesInfo)", + "NeedTypesInfo", }, { packages.NeedTypesSizes, - "LoadMode(NeedTypesSizes)", + "NeedTypesSizes", }, { packages.NeedName | packages.NeedExportFile, - "LoadMode(NeedName|NeedExportFile)", + "(NeedName|NeedExportFile)", }, { packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedDeps | packages.NeedExportFile | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypesSizes, - "LoadMode(NeedName|NeedFiles|NeedCompiledGoFiles|NeedImports|NeedDeps|NeedExportFile|NeedTypes|NeedSyntax|NeedTypesInfo|NeedTypesSizes)", + "(NeedName|NeedFiles|NeedCompiledGoFiles|NeedImports|NeedDeps|NeedExportFile|NeedTypes|NeedSyntax|NeedTypesInfo|NeedTypesSizes)", }, { - packages.NeedName | 8192, - "LoadMode(NeedName|Unknown)", + packages.NeedName | packages.NeedModule, + "(NeedName|NeedModule)", }, { - 4096, - "LoadMode(Unknown)", + packages.NeedName | 0x10000, // off the end (future use) + "(NeedName|0x10000)", + }, + { + packages.NeedName | 0x400, // needInternalDepsErrors + "(NeedName|0x400)", + }, + { + 0x1000, + "LoadMode(0x1000)", }, } diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 55943e45d24..b109fbf3cd3 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -82,7 +82,6 @@ import ( "runtime" "sync" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/versions" ) @@ -854,7 +853,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { if types.IsInterface(rt) { // If v may be an interface type I (after instantiating), // we must emit a check that v is non-nil. - if recv, ok := aliases.Unalias(sel.recv).(*types.TypeParam); ok { + if recv, ok := types.Unalias(sel.recv).(*types.TypeParam); ok { // Emit a nil check if any possible instantiation of the // type parameter is an interface type. if typeSetOf(recv).Len() > 0 { @@ -2508,7 +2507,7 @@ func (b *builder) rangeFunc(fn *Function, x Value, tk, tv types.Type, rng *ast.R name: fmt.Sprintf("%s$%d", fn.Name(), anonIdx+1), Signature: ysig, Synthetic: "range-over-func yield", - pos: rangePosition(rng), + pos: rng.Range, parent: fn, anonIdx: int32(len(fn.AnonFuncs)), Pkg: fn.Pkg, @@ -2566,6 +2565,8 @@ func (b *builder) rangeFunc(fn *Function, x Value, tk, tv types.Type, rng *ast.R emitJump(fn, done) fn.currentBlock = done + // pop the stack for the range-over-func + fn.targets = fn.targets.tail } // buildYieldResume emits to fn code for how to resume execution once a call to @@ -2967,7 +2968,7 @@ func (b *builder) buildFromSyntax(fn *Function) { func (b *builder) buildYieldFunc(fn *Function) { // See builder.rangeFunc for detailed documentation on how fn is set up. // - // In psuedo-Go this roughly builds: + // In pseudo-Go this roughly builds: // func yield(_k tk, _v tv) bool { // if jump != READY { panic("yield function called after range loop exit") } // jump = BUSY @@ -2998,6 +2999,7 @@ func (b *builder) buildYieldFunc(fn *Function) { } } fn.targets = &targets{ + tail: fn.targets, _continue: ycont, // `break` statement targets fn.parent.targets._break. } @@ -3075,6 +3077,8 @@ func (b *builder) buildYieldFunc(fn *Function) { // unreachable. emitJump(fn, ycont) } + // pop the stack for the yield function + fn.targets = fn.targets.tail // Clean up exits and promote any unresolved exits to fn.parent. for _, e := range fn.exits { @@ -3104,17 +3108,17 @@ func (b *builder) buildYieldFunc(fn *Function) { fn.finishBody() } -// addRuntimeType records t as a runtime type, -// along with all types derivable from it using reflection. +// addMakeInterfaceType records non-interface type t as the type of +// the operand a MakeInterface operation, for [Program.RuntimeTypes]. // -// Acquires prog.runtimeTypesMu. -func addRuntimeType(prog *Program, t types.Type) { - prog.runtimeTypesMu.Lock() - defer prog.runtimeTypesMu.Unlock() - forEachReachable(&prog.MethodSets, t, func(t types.Type) bool { - prev, _ := prog.runtimeTypes.Set(t, true).(bool) - return !prev // already seen? - }) +// Acquires prog.makeInterfaceTypesMu. +func addMakeInterfaceType(prog *Program, t types.Type) { + prog.makeInterfaceTypesMu.Lock() + defer prog.makeInterfaceTypesMu.Unlock() + if prog.makeInterfaceTypes == nil { + prog.makeInterfaceTypes = make(map[types.Type]unit) + } + prog.makeInterfaceTypes[t] = unit{} } // Build calls Package.Build for each package in prog. diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go index 55dc79fe464..5b8767c33ea 100644 --- a/go/ssa/builder_generic_test.go +++ b/go/ssa/builder_generic_test.go @@ -31,7 +31,7 @@ import ( // serialized using go/types.Type.String(). // See x/tools/go/expect for details on the syntax. func TestGenericBodies(t *testing.T) { - for _, contents := range []string{ + for _, content := range []string{ ` package p00 @@ -516,48 +516,27 @@ func TestGenericBodies(t *testing.T) { } `, } { - contents := contents - pkgname := packageName(t, contents) + pkgname := parsePackageClause(t, content) t.Run(pkgname, func(t *testing.T) { - // Parse - conf := loader.Config{ParserMode: parser.ParseComments} - f, err := conf.ParseFile("file.go", contents) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles(pkgname, f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssa.NewProgram(lprog.Fset, ssa.SanityCheckFunctions) - for _, info := range lprog.AllPackages { - if info.TransitivelyErrorFree { - prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) - } - } - p := prog.Package(lprog.Package(pkgname).Pkg) - p.Build() + t.Parallel() + ssapkg, ppkg := buildPackage(t, content, ssa.SanityCheckFunctions) + fset := ssapkg.Prog.Fset // Collect all notes in f, i.e. comments starting with "//@ types". - notes, err := expect.ExtractGo(prog.Fset, f) + notes, err := expect.ExtractGo(fset, ppkg.Syntax[0]) if err != nil { t.Errorf("expect.ExtractGo: %v", err) } // Collect calls to the builtin print function. fns := make(map[*ssa.Function]bool) - for _, mem := range p.Members { + for _, mem := range ssapkg.Members { if fn, ok := mem.(*ssa.Function); ok { fns[fn] = true } } probes := callsTo(fns, "print") - expectations := matchNotes(prog.Fset, notes, probes) + expectations := matchNotes(fset, notes, probes) for call := range probes { if expectations[call] == nil { @@ -842,16 +821,6 @@ func TestInstructionString(t *testing.T) { } } -// packageName is a test helper to extract the package name from a string -// containing the content of a go file. -func packageName(t testing.TB, content string) string { - f, err := parser.ParseFile(token.NewFileSet(), "", content, parser.PackageClauseOnly) - if err != nil { - t.Fatalf("parsing the file %q failed with error: %s", content, err) - } - return f.Name.Name -} - func logFunction(t testing.TB, fn *ssa.Function) { // TODO: Consider adding a ssa.Function.GoString() so this can be logged to t via '%#v'. var buf bytes.Buffer diff --git a/go/ssa/builder_go120_test.go b/go/ssa/builder_go120_test.go deleted file mode 100644 index 2472a9d9287..00000000000 --- a/go/ssa/builder_go120_test.go +++ /dev/null @@ -1,102 +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.20 -// +build go1.20 - -package ssa_test - -import ( - "go/ast" - "go/parser" - "go/token" - "go/types" - "testing" - - "golang.org/x/tools/go/ssa" - "golang.org/x/tools/go/ssa/ssautil" -) - -func TestBuildPackageGo120(t *testing.T) { - tests := []struct { - name string - src string - importer types.Importer - }{ - {"slice to array", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, - {"slice to zero length array", "package p; var s []byte; var _ = ([0]byte)(s)", nil}, - {"slice to zero length array type parameter", "package p; var s []byte; func f[T ~[0]byte]() { tmp := (T)(s); var z T; _ = tmp == z}", nil}, - {"slice to non-zero length array type parameter", "package p; var s []byte; func h[T ~[1]byte | [4]byte]() { tmp := T(s); var z T; _ = tmp == z}", nil}, - {"slice to maybe-zero length array type parameter", "package p; var s []byte; func g[T ~[0]byte | [4]byte]() { tmp := T(s); var z T; _ = tmp == z}", nil}, - { - "rune sequence to sequence cast patterns", ` - package p - // Each of fXX functions describes a 1.20 legal cast between sequences of runes - // as []rune, pointers to rune arrays, rune arrays, or strings. - // - // Comments listed given the current emitted instructions [approximately]. - // If multiple conversions are needed, these are separated by |. - // rune was selected as it leads to string casts (byte is similar). - // The length 2 is not significant. - // Multiple array lengths may occur in a cast in practice (including 0). - func f00[S string, D string](s S) { _ = D(s) } // ChangeType - func f01[S string, D []rune](s S) { _ = D(s) } // Convert - func f02[S string, D []rune | string](s S) { _ = D(s) } // ChangeType | Convert - func f03[S [2]rune, D [2]rune](s S) { _ = D(s) } // ChangeType - func f04[S *[2]rune, D *[2]rune](s S) { _ = D(s) } // ChangeType - func f05[S []rune, D string](s S) { _ = D(s) } // Convert - func f06[S []rune, D [2]rune](s S) { _ = D(s) } // SliceToArrayPointer; Deref - func f07[S []rune, D [2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer; Deref | Convert - func f08[S []rune, D *[2]rune](s S) { _ = D(s) } // SliceToArrayPointer - func f09[S []rune, D *[2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer; Deref | Convert - func f10[S []rune, D *[2]rune | [2]rune](s S) { _ = D(s) } // SliceToArrayPointer | SliceToArrayPointer; Deref - func f11[S []rune, D *[2]rune | [2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer | SliceToArrayPointer; Deref | Convert - func f12[S []rune, D []rune](s S) { _ = D(s) } // ChangeType - func f13[S []rune, D []rune | string](s S) { _ = D(s) } // Convert | ChangeType - func f14[S []rune, D []rune | [2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer; Deref - func f15[S []rune, D []rune | [2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer; Deref | Convert - func f16[S []rune, D []rune | *[2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer - func f17[S []rune, D []rune | *[2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | Convert - func f18[S []rune, D []rune | *[2]rune | [2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | SliceToArrayPointer; Deref - func f19[S []rune, D []rune | *[2]rune | [2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | SliceToArrayPointer; Deref | Convert - func f20[S []rune | string, D string](s S) { _ = D(s) } // Convert | ChangeType - func f21[S []rune | string, D []rune](s S) { _ = D(s) } // Convert | ChangeType - func f22[S []rune | string, D []rune | string](s S) { _ = D(s) } // ChangeType | Convert | Convert | ChangeType - func f23[S []rune | [2]rune, D [2]rune](s S) { _ = D(s) } // SliceToArrayPointer; Deref | ChangeType - func f24[S []rune | *[2]rune, D *[2]rune](s S) { _ = D(s) } // SliceToArrayPointer | ChangeType - `, nil, - }, - { - "matching named and underlying types", ` - package p - type a string - type b string - func g0[S []rune | a | b, D []rune | a | b](s S) { _ = D(s) } - func g1[S []rune | ~string, D []rune | a | b](s S) { _ = D(s) } - func g2[S []rune | a | b, D []rune | ~string](s S) { _ = D(s) } - func g3[S []rune | ~string, D []rune |~string](s S) { _ = D(s) } - `, nil, - }, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "p.go", tc.src, 0) - if err != nil { - t.Error(err) - } - files := []*ast.File{f} - - pkg := types.NewPackage("p", "") - conf := &types.Config{Importer: tc.importer} - _, _, err = ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - }) - } -} diff --git a/go/ssa/builder_go122_test.go b/go/ssa/builder_go122_test.go deleted file mode 100644 index bde5bae9292..00000000000 --- a/go/ssa/builder_go122_test.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.22 -// +build go1.22 - -package ssa_test - -import ( - "fmt" - "go/ast" - "go/parser" - "go/token" - "go/types" - "testing" - - "golang.org/x/tools/go/expect" - "golang.org/x/tools/go/ssa" - "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/testenv" -) - -// TestMultipleGoversions tests that globals initialized to equivalent -// function literals are compiled based on the different GoVersion in each file. -func TestMultipleGoversions(t *testing.T) { - var contents = map[string]string{ - "post.go": ` - //go:build go1.22 - package p - - var distinct = func(l []int) { - for i := range l { - print(&i) - } - } - `, - "pre.go": ` - package p - - var same = func(l []int) { - for i := range l { - print(&i) - } - } - `, - } - - fset := token.NewFileSet() - var files []*ast.File - for _, fname := range []string{"post.go", "pre.go"} { - file, err := parser.ParseFile(fset, fname, contents[fname], 0) - if err != nil { - t.Fatal(err) - } - files = append(files, file) - } - - pkg := types.NewPackage("p", "") - conf := &types.Config{Importer: nil, GoVersion: "go1.21"} - p, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions) - if err != nil { - t.Fatal(err) - } - - // Test that global is initialized to a function literal that was - // compiled to have the expected for loop range variable lifetime for i. - for _, test := range []struct { - global *ssa.Global - want string // basic block to []*ssa.Alloc. - }{ - {p.Var("same"), "map[entry:[new int (i)]]"}, // i is allocated in the entry block. - {p.Var("distinct"), "map[rangeindex.body:[new int (i)]]"}, // i is allocated in the body block. - } { - // Find the function the test.name global is initialized to. - var fn *ssa.Function - for _, b := range p.Func("init").Blocks { - for _, instr := range b.Instrs { - if s, ok := instr.(*ssa.Store); ok && s.Addr == test.global { - fn, _ = s.Val.(*ssa.Function) - } - } - } - if fn == nil { - t.Fatalf("Failed to find *ssa.Function for initial value of global %s", test.global) - } - - allocs := make(map[string][]string) // block comments -> []Alloc - for _, b := range fn.Blocks { - for _, instr := range b.Instrs { - if a, ok := instr.(*ssa.Alloc); ok { - allocs[b.Comment] = append(allocs[b.Comment], a.String()) - } - } - } - if got := fmt.Sprint(allocs); got != test.want { - t.Errorf("[%s:=%s] expected the allocations to be in the basic blocks %q, got %q", test.global, fn, test.want, got) - } - } -} - -const rangeOverIntSrc = ` -package p - -type I uint8 - -func noKey(x int) { - for range x { - // does not crash - } -} - -func untypedConstantOperand() { - for i := range 10 { - print(i) /*@ types("int")*/ - } -} - -func unsignedOperand(x uint64) { - for i := range x { - print(i) /*@ types("uint64")*/ - } -} - -func namedOperand(x I) { - for i := range x { - print(i) /*@ types("p.I")*/ - } -} - -func typeparamOperand[T int](x T) { - for i := range x { - print(i) /*@ types("T")*/ - } -} - -func assignment(x I) { - var k I - for k = range x { - print(k) /*@ types("p.I")*/ - } -} -` - -// TestRangeOverInt tests that, in a range-over-int (#61405), -// the type of each range var v (identified by print(v) calls) -// has the expected type. -func TestRangeOverInt(t *testing.T) { - testenv.NeedsGoExperiment(t, "range") - - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "p.go", rangeOverIntSrc, parser.ParseComments) - if err != nil { - t.Fatal(err) - } - - pkg := types.NewPackage("p", "") - conf := &types.Config{} - p, _, err := ssautil.BuildPackage(conf, fset, pkg, []*ast.File{f}, ssa.SanityCheckFunctions) - if err != nil { - t.Fatal(err) - } - - // Collect all notes in f, i.e. comments starting with "//@ types". - notes, err := expect.ExtractGo(fset, f) - if err != nil { - t.Fatal(err) - } - - // Collect calls to the built-in print function. - fns := make(map[*ssa.Function]bool) - for _, mem := range p.Members { - if fn, ok := mem.(*ssa.Function); ok { - fns[fn] = true - } - } - probes := callsTo(fns, "print") - expectations := matchNotes(fset, notes, probes) - - for call := range probes { - if expectations[call] == nil { - t.Errorf("Unmatched call: %v @ %s", call, fset.Position(call.Pos())) - } - } - - // Check each expectation. - for call, note := range expectations { - var args []string - for _, a := range call.Args { - args = append(args, a.Type().String()) - } - if got, want := fmt.Sprint(args), fmt.Sprint(note.Args); got != want { - at := fset.Position(call.Pos()) - t.Errorf("%s: arguments to print had types %s, want %s", at, got, want) - logFunction(t, probes[call]) - } - } -} diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index f6fae50bb67..bc1989c58b7 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -8,29 +8,27 @@ import ( "bytes" "fmt" "go/ast" - "go/build" "go/importer" "go/parser" "go/token" "go/types" + "io/fs" "os" "os/exec" "path/filepath" "reflect" + "runtime" "sort" "strings" "testing" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/go/buildutil" - "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/testfiles" ) func isEmpty(f *ssa.Function) bool { return f.Blocks == nil } @@ -175,11 +173,8 @@ func main() { func TestNoIndirectCreatePackage(t *testing.T) { testenv.NeedsGoBuild(t) // for go/packages - dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "indirect.txtar")) - pkgs, err := loadPackages(dir, "testdata/a") - if err != nil { - t.Fatal(err) - } + fs := openTxtar(t, filepath.Join(analysistest.TestData(), "indirect.txtar")) + pkgs := loadPackages(t, fs, "testdata/a") a := pkgs[0] // Create a from syntax, its direct deps b from types, but not indirect deps c. @@ -207,27 +202,6 @@ func TestNoIndirectCreatePackage(t *testing.T) { } } -// loadPackages loads packages from the specified directory, using LoadSyntax. -func loadPackages(dir string, patterns ...string) ([]*packages.Package, error) { - cfg := &packages.Config{ - Dir: dir, - Mode: packages.LoadSyntax, - Env: append(os.Environ(), - "GO111MODULES=on", - "GOPATH=", - "GOWORK=off", - "GOPROXY=off"), - } - pkgs, err := packages.Load(cfg, patterns...) - if err != nil { - return nil, err - } - if packages.PrintErrors(pkgs) > 0 { - return nil, fmt.Errorf("there were errors") - } - return pkgs, nil -} - // TestRuntimeTypes tests that (*Program).RuntimeTypes() includes all necessary types. func TestRuntimeTypes(t *testing.T) { testenv.NeedsGoBuild(t) // for importer.Default() @@ -374,37 +348,23 @@ func init(): } for _, test := range tests { // Create a single-file main package. - var conf loader.Config - f, err := conf.ParseFile("", test.input) - if err != nil { - t.Errorf("test %q: %s", test.input[:15], err) - continue - } - conf.CreateFromFiles(f.Name.Name, f) - - lprog, err := conf.Load() - if err != nil { - t.Errorf("test 'package %s': Load: %s", f.Name.Name, err) - continue - } - prog := ssautil.CreateProgram(lprog, test.mode) - mainPkg := prog.Package(lprog.Created[0].Pkg) - prog.Build() + mainPkg, _ := buildPackage(t, test.input, test.mode) + name := mainPkg.Pkg.Name() initFunc := mainPkg.Func("init") if initFunc == nil { - t.Errorf("test 'package %s': no init function", f.Name.Name) + t.Errorf("test 'package %s': no init function", name) continue } var initbuf bytes.Buffer - _, err = initFunc.WriteTo(&initbuf) + _, err := initFunc.WriteTo(&initbuf) if err != nil { - t.Errorf("test 'package %s': WriteTo: %s", f.Name.Name, err) + t.Errorf("test 'package %s': WriteTo: %s", name, err) continue } if initbuf.String() != test.want { - t.Errorf("test 'package %s': got %s, want %s", f.Name.Name, initbuf.String(), test.want) + t.Errorf("test 'package %s': got %s, want %s", name, initbuf.String(), test.want) } } } @@ -444,23 +404,7 @@ var ( t interface{} = new(struct{*T}) ) ` - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles(f.Name.Name, f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.BuilderMode(0)) - prog.Build() + pkg, _ := buildPackage(t, input, ssa.BuilderMode(0)) // Enumerate reachable synthetic functions want := map[string]string{ @@ -484,7 +428,7 @@ var ( "P.init": "package initializer", } var seen []string // may contain dups - for fn := range ssautil.AllFunctions(prog) { + for fn := range ssautil.AllFunctions(pkg.Prog) { if fn.Synthetic == "" { continue } @@ -555,24 +499,7 @@ func h(error) // t8 = phi [1: t7, 3: t4] #e // ... - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.BuilderMode(0)) - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p, _ := buildPackage(t, input, ssa.BuilderMode(0)) g := p.Func("g") phis := 0 @@ -621,24 +548,7 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) // func init func() // var init$guard bool - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.BuilderMode(0)) - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p, _ := buildPackage(t, input, ssa.BuilderMode(0)) if load := p.Func("Load"); load.Signature.TypeParams().Len() != 1 { t.Errorf("expected a single type param T for Load got %q", load.Signature) @@ -674,25 +584,8 @@ var indirect = R[int].M // var thunk func(S[int]) int // var wrapper func(R[int]) int - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - for _, mode := range []ssa.BuilderMode{ssa.BuilderMode(0), ssa.InstantiateGenerics} { - // Create and build SSA - prog := ssautil.CreateProgram(lprog, mode) - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p, _ := buildPackage(t, input, mode) for _, entry := range []struct { name string // name of the package variable @@ -773,63 +666,52 @@ var indirect = R[int].M // TestTypeparamTest builds SSA over compilable examples in $GOROOT/test/typeparam/*.go. func TestTypeparamTest(t *testing.T) { - // Tests use a fake goroot to stub out standard libraries with delcarations in + // Tests use a fake goroot to stub out standard libraries with declarations in // testdata/src. Decreases runtime from ~80s to ~1s. - dir := filepath.Join(build.Default.GOROOT, "test", "typeparam") + if runtime.GOARCH == "wasm" { + // Consistent flakes on wasm (#64726, #69409, #69410). + // Needs more investigation, but more likely a wasm issue + // Disabling for now. + t.Skip("Consistent flakes on wasm (e.g. https://go.dev/issues/64726)") + } + + // located GOROOT based on the relative path of errors in $GOROOT/src/errors + stdPkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedFiles, + }, "errors") + if err != nil { + t.Fatalf("Failed to load errors package from std: %s", err) + } + goroot := filepath.Dir(filepath.Dir(filepath.Dir(stdPkgs[0].GoFiles[0]))) + + dir := filepath.Join(goroot, "test", "typeparam") // Collect all of the .go files in - list, err := os.ReadDir(dir) + fsys := os.DirFS(dir) + entries, err := fs.ReadDir(fsys, ".") if err != nil { t.Fatal(err) } - for _, entry := range list { - if entry.Name() == "issue58513.go" { - continue // uses runtime.Caller; unimplemented by go/ssa/interp - } + for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { continue // Consider standalone go files. } - input := filepath.Join(dir, entry.Name()) t.Run(entry.Name(), func(t *testing.T) { - src, err := os.ReadFile(input) + src, err := fs.ReadFile(fsys, entry.Name()) if err != nil { t.Fatal(err) } + // Only build test files that can be compiled, or compiled and run. if !bytes.HasPrefix(src, []byte("// run")) && !bytes.HasPrefix(src, []byte("// compile")) { t.Skipf("not detected as a run test") } - t.Logf("Input: %s\n", input) + t.Logf("Input: %s\n", entry.Name()) - ctx := build.Default // copy - ctx.GOROOT = "testdata" // fake goroot. Makes tests ~1s. tests take ~80s. - - reportErr := func(err error) { - t.Error(err) - } - conf := loader.Config{Build: &ctx, TypeChecker: types.Config{Error: reportErr}} - if _, err := conf.FromArgs([]string{input}, true); err != nil { - t.Fatalf("FromArgs(%s) failed: %s", input, err) - } - - iprog, err := conf.Load() - if iprog != nil { - for _, pkg := range iprog.Created { - for i, e := range pkg.Errors { - t.Errorf("Loading pkg %s error[%d]=%s", pkg, i, e) - } - } - } - if err != nil { - t.Fatalf("conf.Load(%s) failed: %s", input, err) - } - - mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics - prog := ssautil.CreateProgram(iprog, mode) - prog.Build() + _, _ = buildPackage(t, string(src), ssa.SanityCheckFunctions|ssa.InstantiateGenerics) }) } } @@ -851,24 +733,7 @@ func sliceMax(s []int) []int { return s[a():b():c()] } ` - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.BuilderMode(0)) - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p, _ := buildPackage(t, input, ssa.BuilderMode(0)) for _, item := range []struct { fn string @@ -900,30 +765,28 @@ func sliceMax(s []int) []int { return s[a():b():c()] } // TestGenericFunctionSelector ensures generic functions from other packages can be selected. func TestGenericFunctionSelector(t *testing.T) { - pkgs := map[string]map[string]string{ - "main": {"m.go": `package main; import "a"; func main() { a.F[int](); a.G[int,string](); a.H(0) }`}, - "a": {"a.go": `package a; func F[T any](){}; func G[S, T any](){}; func H[T any](a T){} `}, - } + fsys := overlayFS(map[string][]byte{ + "go.mod": goMod("example.com", -1), + "main.go": []byte(`package main; import "example.com/a"; func main() { a.F[int](); a.G[int,string](); a.H(0) }`), + "a/a.go": []byte(`package a; func F[T any](){}; func G[S, T any](){}; func H[T any](a T){} `), + }) for _, mode := range []ssa.BuilderMode{ ssa.SanityCheckFunctions, ssa.SanityCheckFunctions | ssa.InstantiateGenerics, } { - conf := loader.Config{ - Build: buildutil.FakeContext(pkgs), - } - conf.Import("main") - lprog, err := conf.Load() - if err != nil { - t.Errorf("Load failed: %s", err) + pkgs := loadPackages(t, fsys, "example.com") // package main + if len(pkgs) != 1 { + t.Fatalf("Expected 1 root package but got %d", len(pkgs)) } - if lprog == nil { - t.Fatalf("Load returned nil *Program") + prog, _ := ssautil.Packages(pkgs, mode) + p := prog.Package(pkgs[0].Types) + p.Build() + + if p.Pkg.Name() != "main" { + t.Fatalf("Expected the second package is main but got %s", p.Pkg.Name()) } - // Create and build SSA - prog := ssautil.CreateProgram(lprog, mode) - p := prog.Package(lprog.Package("main").Pkg) p.Build() var callees []string // callees of the CallInstruction.String() in main(). @@ -940,7 +803,7 @@ func TestGenericFunctionSelector(t *testing.T) { } sort.Strings(callees) // ignore the order in the code. - want := "[a.F[int] a.G[int string] a.H[int]]" + want := "[example.com/a.F[int] example.com/a.G[int string] example.com/a.H[int]]" if got := fmt.Sprint(callees); got != want { t.Errorf("Expected main() to contain calls %v. got %v", want, got) } @@ -1039,7 +902,7 @@ func TestIssue58491Rec(t *testing.T) { // Find the local type result instantiated with int. var found bool for _, rt := range p.Prog.RuntimeTypes() { - if n, ok := aliases.Unalias(rt).(*types.Named); ok { + if n, ok := types.Unalias(rt).(*types.Named); ok { if u, ok := n.Underlying().(*types.Struct); ok { found = true if got, want := n.String(), "p.result"; got != want { @@ -1082,23 +945,8 @@ func TestSyntax(t *testing.T) { var _ = F[P] // unreferenced => not instantiated ` - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.InstantiateGenerics) - prog.Build() + p, _ := buildPackage(t, input, ssa.InstantiateGenerics) + prog := p.Prog // Collect syntax information for all of the functions. got := make(map[string]string) @@ -1171,21 +1019,7 @@ func TestLabels(t *testing.T) { func main() { _:println(1); _:println(2)}`, } for _, test := range tests { - conf := loader.Config{Fset: token.NewFileSet()} - f, err := parser.ParseFile(conf.Fset, "", test, 0) - if err != nil { - t.Errorf("parse error: %s", err) - return - } - conf.CreateFromFiles("main", f) - iprog, err := conf.Load() - if err != nil { - t.Error(err) - continue - } - prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0)) - pkg := prog.Package(iprog.Created[0].Pkg) - pkg.Build() + buildPackage(t, test, ssa.BuilderMode(0)) } } @@ -1219,22 +1053,8 @@ func TestIssue67079(t *testing.T) { // Load the package. const src = `package p; type T int; func (T) f() {}; var _ = (*T).f` - conf := loader.Config{Fset: token.NewFileSet()} - f, err := parser.ParseFile(conf.Fset, "p.go", src, 0) - if err != nil { - t.Fatal(err) - } - conf.CreateFromFiles("p", f) - iprog, err := conf.Load() - if err != nil { - t.Fatal(err) - } - pkg := iprog.Created[0].Pkg - - // Create and build SSA program. - prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0)) - prog.Build() - + spkg, ppkg := buildPackage(t, src, ssa.BuilderMode(0)) + prog := spkg.Prog var g errgroup.Group // Access bodies of all functions. @@ -1253,7 +1073,7 @@ func TestIssue67079(t *testing.T) { // Force building of wrappers. g.Go(func() error { - ptrT := types.NewPointer(pkg.Scope().Lookup("T").Type()) + ptrT := types.NewPointer(ppkg.Types.Scope().Lookup("T").Type()) ptrTf := types.NewMethodSet(ptrT).At(0) // (*T).f symbol prog.MethodValue(ptrTf) return nil @@ -1294,10 +1114,10 @@ func TestGenericAliases(t *testing.T) { } func testGenericAliases(t *testing.T) { - t.Setenv("GOEXPERIMENT", "aliastypeparams=1") + testenv.NeedsGoExperiment(t, "aliastypeparams") const source = ` -package P +package p type A = uint8 type B[T any] = [4]T @@ -1329,22 +1149,9 @@ func f[S any]() { } ` - conf := loader.Config{Fset: token.NewFileSet()} - f, err := parser.ParseFile(conf.Fset, "p.go", source, 0) - if err != nil { - t.Fatal(err) - } - conf.CreateFromFiles("p", f) - iprog, err := conf.Load() - if err != nil { - t.Fatal(err) - } - - // Create and build SSA program. - prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics) - prog.Build() + p, _ := buildPackage(t, source, ssa.InstantiateGenerics) - probes := callsTo(ssautil.AllFunctions(prog), "print") + probes := callsTo(ssautil.AllFunctions(p.Prog), "print") if got, want := len(probes), 3*4*2; got != want { t.Errorf("Found %v probes, expected %v", got, want) } @@ -1401,3 +1208,260 @@ func constString(v ssa.Value) string { } return "" } + +// TestMultipleGoversions tests that globals initialized to equivalent +// function literals are compiled based on the different GoVersion in each file. +func TestMultipleGoversions(t *testing.T) { + var contents = map[string]string{ + "post.go": ` + //go:build go1.22 + package p + + var distinct = func(l []int) { + for i := range l { + print(&i) + } + } + `, + "pre.go": ` + package p + + var same = func(l []int) { + for i := range l { + print(&i) + } + } + `, + } + + fset := token.NewFileSet() + var files []*ast.File + for _, fname := range []string{"post.go", "pre.go"} { + file, err := parser.ParseFile(fset, fname, contents[fname], 0) + if err != nil { + t.Fatal(err) + } + files = append(files, file) + } + + pkg := types.NewPackage("p", "") + conf := &types.Config{Importer: nil, GoVersion: "go1.21"} + p, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions) + if err != nil { + t.Fatal(err) + } + + // Test that global is initialized to a function literal that was + // compiled to have the expected for loop range variable lifetime for i. + for _, test := range []struct { + global *ssa.Global + want string // basic block to []*ssa.Alloc. + }{ + {p.Var("same"), "map[entry:[new int (i)]]"}, // i is allocated in the entry block. + {p.Var("distinct"), "map[rangeindex.body:[new int (i)]]"}, // i is allocated in the body block. + } { + // Find the function the test.name global is initialized to. + var fn *ssa.Function + for _, b := range p.Func("init").Blocks { + for _, instr := range b.Instrs { + if s, ok := instr.(*ssa.Store); ok && s.Addr == test.global { + fn, _ = s.Val.(*ssa.Function) + } + } + } + if fn == nil { + t.Fatalf("Failed to find *ssa.Function for initial value of global %s", test.global) + } + + allocs := make(map[string][]string) // block comments -> []Alloc + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + if a, ok := instr.(*ssa.Alloc); ok { + allocs[b.Comment] = append(allocs[b.Comment], a.String()) + } + } + } + if got := fmt.Sprint(allocs); got != test.want { + t.Errorf("[%s:=%s] expected the allocations to be in the basic blocks %q, got %q", test.global, fn, test.want, got) + } + } +} + +// TestRangeOverInt tests that, in a range-over-int (#61405), +// the type of each range var v (identified by print(v) calls) +// has the expected type. +func TestRangeOverInt(t *testing.T) { + const rangeOverIntSrc = ` + package p + + type I uint8 + + func noKey(x int) { + for range x { + // does not crash + } + } + + func untypedConstantOperand() { + for i := range 10 { + print(i) /*@ types("int")*/ + } + } + + func unsignedOperand(x uint64) { + for i := range x { + print(i) /*@ types("uint64")*/ + } + } + + func namedOperand(x I) { + for i := range x { + print(i) /*@ types("p.I")*/ + } + } + + func typeparamOperand[T int](x T) { + for i := range x { + print(i) /*@ types("T")*/ + } + } + + func assignment(x I) { + var k I + for k = range x { + print(k) /*@ types("p.I")*/ + } + } + ` + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", rangeOverIntSrc, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + + pkg := types.NewPackage("p", "") + conf := &types.Config{} + p, _, err := ssautil.BuildPackage(conf, fset, pkg, []*ast.File{f}, ssa.SanityCheckFunctions) + if err != nil { + t.Fatal(err) + } + + // Collect all notes in f, i.e. comments starting with "//@ types". + notes, err := expect.ExtractGo(fset, f) + if err != nil { + t.Fatal(err) + } + + // Collect calls to the built-in print function. + fns := make(map[*ssa.Function]bool) + for _, mem := range p.Members { + if fn, ok := mem.(*ssa.Function); ok { + fns[fn] = true + } + } + probes := callsTo(fns, "print") + expectations := matchNotes(fset, notes, probes) + + for call := range probes { + if expectations[call] == nil { + t.Errorf("Unmatched call: %v @ %s", call, fset.Position(call.Pos())) + } + } + + // Check each expectation. + for call, note := range expectations { + var args []string + for _, a := range call.Args { + args = append(args, a.Type().String()) + } + if got, want := fmt.Sprint(args), fmt.Sprint(note.Args); got != want { + at := fset.Position(call.Pos()) + t.Errorf("%s: arguments to print had types %s, want %s", at, got, want) + logFunction(t, probes[call]) + } + } +} + +func TestBuildPackageGo120(t *testing.T) { + tests := []struct { + name string + src string + importer types.Importer + }{ + {"slice to array", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, + {"slice to zero length array", "package p; var s []byte; var _ = ([0]byte)(s)", nil}, + {"slice to zero length array type parameter", "package p; var s []byte; func f[T ~[0]byte]() { tmp := (T)(s); var z T; _ = tmp == z}", nil}, + {"slice to non-zero length array type parameter", "package p; var s []byte; func h[T ~[1]byte | [4]byte]() { tmp := T(s); var z T; _ = tmp == z}", nil}, + {"slice to maybe-zero length array type parameter", "package p; var s []byte; func g[T ~[0]byte | [4]byte]() { tmp := T(s); var z T; _ = tmp == z}", nil}, + { + "rune sequence to sequence cast patterns", ` + package p + // Each of fXX functions describes a 1.20 legal cast between sequences of runes + // as []rune, pointers to rune arrays, rune arrays, or strings. + // + // Comments listed given the current emitted instructions [approximately]. + // If multiple conversions are needed, these are separated by |. + // rune was selected as it leads to string casts (byte is similar). + // The length 2 is not significant. + // Multiple array lengths may occur in a cast in practice (including 0). + func f00[S string, D string](s S) { _ = D(s) } // ChangeType + func f01[S string, D []rune](s S) { _ = D(s) } // Convert + func f02[S string, D []rune | string](s S) { _ = D(s) } // ChangeType | Convert + func f03[S [2]rune, D [2]rune](s S) { _ = D(s) } // ChangeType + func f04[S *[2]rune, D *[2]rune](s S) { _ = D(s) } // ChangeType + func f05[S []rune, D string](s S) { _ = D(s) } // Convert + func f06[S []rune, D [2]rune](s S) { _ = D(s) } // SliceToArrayPointer; Deref + func f07[S []rune, D [2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer; Deref | Convert + func f08[S []rune, D *[2]rune](s S) { _ = D(s) } // SliceToArrayPointer + func f09[S []rune, D *[2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer; Deref | Convert + func f10[S []rune, D *[2]rune | [2]rune](s S) { _ = D(s) } // SliceToArrayPointer | SliceToArrayPointer; Deref + func f11[S []rune, D *[2]rune | [2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer | SliceToArrayPointer; Deref | Convert + func f12[S []rune, D []rune](s S) { _ = D(s) } // ChangeType + func f13[S []rune, D []rune | string](s S) { _ = D(s) } // Convert | ChangeType + func f14[S []rune, D []rune | [2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer; Deref + func f15[S []rune, D []rune | [2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer; Deref | Convert + func f16[S []rune, D []rune | *[2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer + func f17[S []rune, D []rune | *[2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | Convert + func f18[S []rune, D []rune | *[2]rune | [2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | SliceToArrayPointer; Deref + func f19[S []rune, D []rune | *[2]rune | [2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | SliceToArrayPointer; Deref | Convert + func f20[S []rune | string, D string](s S) { _ = D(s) } // Convert | ChangeType + func f21[S []rune | string, D []rune](s S) { _ = D(s) } // Convert | ChangeType + func f22[S []rune | string, D []rune | string](s S) { _ = D(s) } // ChangeType | Convert | Convert | ChangeType + func f23[S []rune | [2]rune, D [2]rune](s S) { _ = D(s) } // SliceToArrayPointer; Deref | ChangeType + func f24[S []rune | *[2]rune, D *[2]rune](s S) { _ = D(s) } // SliceToArrayPointer | ChangeType + `, nil, + }, + { + "matching named and underlying types", ` + package p + type a string + type b string + func g0[S []rune | a | b, D []rune | a | b](s S) { _ = D(s) } + func g1[S []rune | ~string, D []rune | a | b](s S) { _ = D(s) } + func g2[S []rune | a | b, D []rune | ~string](s S) { _ = D(s) } + func g3[S []rune | ~string, D []rune |~string](s S) { _ = D(s) } + `, nil, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", tc.src, 0) + if err != nil { + t.Error(err) + } + files := []*ast.File{f} + + pkg := types.NewPackage("p", "") + conf := &types.Config{Importer: tc.importer} + _, _, err = ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} diff --git a/go/ssa/const.go b/go/ssa/const.go index 2a4e0dde28a..865329bfd34 100644 --- a/go/ssa/const.go +++ b/go/ssa/const.go @@ -14,7 +14,6 @@ import ( "strconv" "strings" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -114,7 +113,7 @@ func zeroString(t types.Type, from *types.Package) string { } case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature: return "nil" - case *types.Named, *aliases.Alias: + case *types.Named, *types.Alias: return zeroString(t.Underlying(), from) case *types.Array, *types.Struct: return relType(t, from) + "{}" diff --git a/go/ssa/coretype.go b/go/ssa/coretype.go index 8c218f919fa..d937134227d 100644 --- a/go/ssa/coretype.go +++ b/go/ssa/coretype.go @@ -7,7 +7,6 @@ package ssa import ( "go/types" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -51,7 +50,7 @@ func typeSetOf(typ types.Type) termList { var terms []*types.Term var err error // typeSetOf(t) == typeSetOf(Unalias(t)) - switch typ := aliases.Unalias(typ).(type) { + switch typ := types.Unalias(typ).(type) { case *types.TypeParam: terms, err = typeparams.StructuralTerms(typ) case *types.Union: diff --git a/go/ssa/create.go b/go/ssa/create.go index 423bce87182..2fa3d0757a6 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -193,11 +193,7 @@ func membersFromDecl(pkg *Package, decl ast.Decl, goversion string) { // // The real work of building SSA form for each function is not done // until a subsequent call to Package.Build. -// -// CreatePackage should not be called after building any package in -// the program. func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info *types.Info, importable bool) *Package { - // TODO(adonovan): assert that no package has yet been built. if pkg == nil { panic("nil pkg") // otherwise pkg.Scope below returns types.Universe! } diff --git a/go/ssa/emit.go b/go/ssa/emit.go index c664ff85a0f..176c1e1a748 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -249,7 +249,7 @@ func emitConv(f *Function, val Value, typ types.Type) Value { // non-parameterized, as they are the set of runtime types. t := val.Type() if f.typeparams.Len() == 0 || !f.Prog.isParameterized(t) { - addRuntimeType(f.Prog, t) + addMakeInterfaceType(f.Prog, t) } mi := &MakeInterface{X: val} diff --git a/go/ssa/example_test.go b/go/ssa/example_test.go index cab0b84903b..e0fba0be681 100644 --- a/go/ssa/example_test.go +++ b/go/ssa/example_test.go @@ -60,9 +60,6 @@ func main() { // syntax, perhaps obtained from golang.org/x/tools/go/packages. // In that case, see the other examples for simpler approaches. func Example_buildPackage() { - // Replace interface{} with any for this test. - ssa.SetNormalizeAnyForTesting(true) - defer ssa.SetNormalizeAnyForTesting(false) // Parse the source files. fset := token.NewFileSet() f, err := parser.ParseFile(fset, "hello.go", hello, parser.ParseComments) diff --git a/go/ssa/func.go b/go/ssa/func.go index 2ed63bfd53e..010c128a9ec 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -186,6 +186,20 @@ func targetedBlock(f *Function, tok token.Token) *BasicBlock { return targetedBlock(f.parent, tok) } +// instrs returns an iterator that returns each reachable instruction of the SSA function. +// TODO: return an iter.Seq once x/tools is on 1.23 +func (f *Function) instrs() func(yield func(i Instruction) bool) { + return func(yield func(i Instruction) bool) { + for _, block := range f.Blocks { + for _, instr := range block.Instrs { + if !yield(instr) { + return + } + } + } + } +} + // addResultVar adds a result for a variable v to f.results and v to f.returnVars. func (f *Function) addResultVar(v *types.Var) { result := emitLocalVar(f, v) diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index 25f78492874..32c3a9a08cf 100644 --- a/go/ssa/instantiate_test.go +++ b/go/ssa/instantiate_test.go @@ -2,12 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package ssa - -// Note: Tests use unexported method _Instances. +package ssa_test import ( - "bytes" "fmt" "go/types" "reflect" @@ -15,42 +12,11 @@ import ( "strings" "testing" - "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" ) -// loadProgram creates loader.Program out of p. -func loadProgram(p string) (*loader.Program, error) { - // Parse - var conf loader.Config - f, err := conf.ParseFile("", p) - if err != nil { - return nil, fmt.Errorf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - return nil, fmt.Errorf("Load: %v", err) - } - return lprog, nil -} - -// buildPackage builds and returns ssa representation of package pkg of lprog. -func buildPackage(lprog *loader.Program, pkg string, mode BuilderMode) *Package { - prog := NewProgram(lprog.Fset, mode) - - for _, info := range lprog.AllPackages { - prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) - } - - p := prog.Package(lprog.Package(pkg).Pkg) - p.Build() - return p -} - -// TestNeedsInstance ensures that new method instances can be created via needsInstance, -// that TypeArgs are as expected, and can be accessed via _Instances. +// TestNeedsInstance ensures that new method instances can be created via MethodValue. func TestNeedsInstance(t *testing.T) { const input = ` package p @@ -74,14 +40,11 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) // func init func() // var init$guard bool - lprog, err := loadProgram(input) - if err != err { - t.Fatal(err) - } - - for _, mode := range []BuilderMode{BuilderMode(0), InstantiateGenerics} { - // Create and build SSA - p := buildPackage(lprog, "p", mode) + for _, mode := range []ssa.BuilderMode{ + ssa.SanityCheckFunctions, + ssa.SanityCheckFunctions | ssa.InstantiateGenerics, + } { + p, _ := buildPackage(t, input, mode) prog := p.Prog ptr := p.Type("Pointer").Type().(*types.Named) @@ -96,48 +59,39 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) meth := prog.FuncValue(obj) - b := &builder{} - intSliceTyp := types.NewSlice(types.Typ[types.Int]) - instance := meth.instance([]types.Type{intSliceTyp}, b) - if len(b.fns) != 1 { - t.Errorf("Expected first instance to create a function. got %d created functions", len(b.fns)) + // instantiateLoadMethod returns the first method (Load) of the instantiation *Pointer[T]. + instantiateLoadMethod := func(T types.Type) *ssa.Function { + ptrT, err := types.Instantiate(nil, ptr, []types.Type{T}, false) + if err != nil { + t.Fatalf("Failed to Instantiate %q by %q", ptr, T) + } + methods := types.NewMethodSet(types.NewPointer(ptrT)) + if methods.Len() != 1 { + t.Fatalf("Expected 1 method for %q. got %d", ptrT, methods.Len()) + } + return prog.MethodValue(methods.At(0)) } + + intSliceTyp := types.NewSlice(types.Typ[types.Int]) + instance := instantiateLoadMethod(intSliceTyp) // (*Pointer[[]int]).Load if instance.Origin() != meth { t.Errorf("Expected Origin of %s to be %s. got %s", instance, meth, instance.Origin()) } if len(instance.TypeArgs()) != 1 || !types.Identical(instance.TypeArgs()[0], intSliceTyp) { - t.Errorf("Expected TypeArgs of %s to be %v. got %v", instance, []types.Type{intSliceTyp}, instance.typeargs) - } - instances := allInstances(meth) - if want := []*Function{instance}; !reflect.DeepEqual(instances, want) { - t.Errorf("Expected instances of %s to be %v. got %v", meth, want, instances) + t.Errorf("Expected TypeArgs of %s to be %v. got %v", instance, []types.Type{intSliceTyp}, instance.TypeArgs()) } // A second request with an identical type returns the same Function. - second := meth.instance([]types.Type{types.NewSlice(types.Typ[types.Int])}, b) - if second != instance || len(b.fns) != 1 { - t.Error("Expected second identical instantiation to not create a function") + second := instantiateLoadMethod(types.NewSlice(types.Typ[types.Int])) + if second != instance { + t.Error("Expected second identical instantiation to be the same function") } - // Add a second instance. - inst2 := meth.instance([]types.Type{types.NewSlice(types.Typ[types.Uint])}, b) - instances = allInstances(meth) + // (*Pointer[[]uint]).Load + inst2 := instantiateLoadMethod(types.NewSlice(types.Typ[types.Uint])) - // Note: instance.Name() < inst2.Name() - sort.Slice(instances, func(i, j int) bool { - return instances[i].Name() < instances[j].Name() - }) - if want := []*Function{instance, inst2}; !reflect.DeepEqual(instances, want) { - t.Errorf("Expected instances of %s to be %v. got %v", meth, want, instances) - } - - // TODO(adonovan): tests should not rely on unexported functions. - - // build and sanity check manually created instance. - b.buildFunction(instance) - var buf bytes.Buffer - if !sanityCheck(instance, &buf) { - t.Errorf("sanityCheck of %s failed with: %s", instance, buf.String()) + if instance.Name() >= inst2.Name() { + t.Errorf("Expected name of instance %s to be before instance %v", instance, inst2) } } } @@ -193,13 +147,8 @@ func entry(i int, a A) int { return Id[int](i) } ` - lprog, err := loadProgram(input) - if err != err { - t.Fatal(err) - } - - p := buildPackage(lprog, "p", SanityCheckFunctions) - prog := p.Prog + p, _ := buildPackage(t, input, ssa.SanityCheckFunctions) + all := ssautil.AllFunctions(p.Prog) for _, ti := range []struct { orig string @@ -215,12 +164,18 @@ func entry(i int, a A) int { } { test := ti t.Run(test.instance, func(t *testing.T) { - f := p.Members[test.orig].(*Function) + f := p.Members[test.orig].(*ssa.Function) if f == nil { t.Fatalf("origin function not found") } - i := instanceOf(f, test.instance, prog) + var i *ssa.Function + for _, fn := range instancesOf(all, f) { + if fn.Name() == test.instance { + i = fn + break + } + } if i == nil { t.Fatalf("instance not found") } @@ -249,16 +204,7 @@ func entry(i int, a A) int { } } -func instanceOf(f *Function, name string, prog *Program) *Function { - for _, i := range allInstances(f) { - if i.Name() == name { - return i - } - } - return nil -} - -func tparams(f *Function) string { +func tparams(f *ssa.Function) string { tplist := f.TypeParams() var tps []string for i := 0; i < tplist.Len(); i++ { @@ -267,7 +213,7 @@ func tparams(f *Function) string { return fmt.Sprint(tps) } -func targs(f *Function) string { +func targs(f *ssa.Function) string { var tas []string for _, ta := range f.TypeArgs() { tas = append(tas, ta.String()) @@ -275,10 +221,10 @@ func targs(f *Function) string { return fmt.Sprint(tas) } -func changeTypeInstrs(b *BasicBlock) int { +func changeTypeInstrs(b *ssa.BasicBlock) int { cnt := 0 for _, i := range b.Instrs { - if _, ok := i.(*ChangeType); ok { + if _, ok := i.(*ssa.ChangeType); ok { cnt++ } } @@ -309,13 +255,9 @@ func Foo[T any, S any](t T, s S) { Foo[T, S](t, s) } ` - lprog, err := loadProgram(input) - if err != err { - t.Fatal(err) - } - - p := buildPackage(lprog, "p", SanityCheckFunctions) + p, _ := buildPackage(t, input, ssa.SanityCheckFunctions) + all := ssautil.AllFunctions(p.Prog) for _, test := range []struct { orig string instances string @@ -324,12 +266,12 @@ func Foo[T any, S any](t T, s S) { {"Foo", "[p.Foo[S T] p.Foo[T S]]"}, } { t.Run(test.orig, func(t *testing.T) { - f := p.Members[test.orig].(*Function) + f := p.Members[test.orig].(*ssa.Function) if f == nil { t.Fatalf("origin function not found") } - instances := allInstances(f) + instances := instancesOf(all, f) sort.Slice(instances, func(i, j int) bool { return instances[i].Name() < instances[j].Name() }) if got := fmt.Sprintf("%v", instances); !reflect.DeepEqual(got, test.instances) { @@ -339,22 +281,14 @@ func Foo[T any, S any](t T, s S) { } } -// allInstances returns a new unordered array of all instances of the -// specified function, if generic, or nil otherwise. -// -// Thread-safe. -// -// TODO(adonovan): delete this. The tests should be intensional (e.g. -// "what instances of f are reachable?") not representational (e.g. -// "what is the history of calls to Function.instance?"). -// -// Acquires fn.generic.instancesMu. -func allInstances(fn *Function) []*Function { - if fn.generic == nil { - return nil +// instancesOf returns a new unordered slice of all instances of the +// specified function g in fns. +func instancesOf(fns map[*ssa.Function]bool, g *ssa.Function) []*ssa.Function { + var instances []*ssa.Function + for fn := range fns { + if fn != g && fn.Origin() == g { + instances = append(instances, fn) + } } - - fn.generic.instancesMu.Lock() - defer fn.generic.instancesMu.Unlock() - return mapValues(fn.generic.instances) + return instances } diff --git a/go/ssa/interp/interp_go120_test.go b/go/ssa/interp/interp_go120_test.go deleted file mode 100644 index d8eb2c21341..00000000000 --- a/go/ssa/interp/interp_go120_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.20 -// +build go1.20 - -package interp_test - -func init() { - testdataTests = append(testdataTests, "slice2array.go") -} diff --git a/go/ssa/interp/interp_go121_test.go b/go/ssa/interp/interp_go121_test.go deleted file mode 100644 index 381dc4f636e..00000000000 --- a/go/ssa/interp/interp_go121_test.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.21 -// +build go1.21 - -package interp_test - -func init() { - testdataTests = append(testdataTests, "minmax.go") -} diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index c55fe36c425..8ce9f368aec 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go @@ -117,19 +117,20 @@ var testdataTests = []string{ "deepequal.go", "defer.go", "fieldprom.go", - // "forvarlifetime_old.go", Disabled for golang.org/cl/603895. Fix and re-enable. + "forvarlifetime_old.go", "ifaceconv.go", "ifaceprom.go", "initorder.go", "methprom.go", "mrvchain.go", "range.go", + "rangeoverint.go", "recover.go", "reflect.go", "slice2arrayptr.go", "static.go", "width32.go", - // "rangevarlifetime_old.go", Disabled for golang.org/cl/603895. Fix and re-enable. + "rangevarlifetime_old.go", "fixedbugs/issue52342.go", "fixedbugs/issue55115.go", "fixedbugs/issue52835.go", @@ -137,6 +138,10 @@ var testdataTests = []string{ "fixedbugs/issue66783.go", "typeassert.go", "zeros.go", + "slice2array.go", + "minmax.go", + "rangevarlifetime_go122.go", + "forvarlifetime_go122.go", } func init() { @@ -307,6 +312,11 @@ func TestGorootTest(t *testing.T) { // in $GOROOT/test/typeparam/*.go. func TestTypeparamTest(t *testing.T) { + if runtime.GOARCH == "wasm" { + // See ssa/TestTypeparamTest. + t.Skip("Consistent flakes on wasm (e.g. https://go.dev/issues/64726)") + } + goroot := makeGoroot(t) // Skip known failures for the given reason. diff --git a/go/ssa/interp/ops.go b/go/ssa/interp/ops.go index 588b31b7479..7254676a4d0 100644 --- a/go/ssa/interp/ops.go +++ b/go/ssa/interp/ops.go @@ -17,7 +17,6 @@ import ( "unsafe" "golang.org/x/tools/go/ssa" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -236,8 +235,8 @@ func zero(t types.Type) value { return a case *types.Named: return zero(t.Underlying()) - case *aliases.Alias: - return zero(aliases.Unalias(t)) + case *types.Alias: + return zero(types.Unalias(t)) case *types.Interface: return iface{} // nil type, methodset and value case *types.Slice: diff --git a/go/ssa/interp/interp_go122_test.go b/go/ssa/interp/rangefunc_test.go similarity index 94% rename from go/ssa/interp/interp_go122_test.go rename to go/ssa/interp/rangefunc_test.go index aedb5880f3e..58b7f43eca4 100644 --- a/go/ssa/interp/interp_go122_test.go +++ b/go/ssa/interp/rangefunc_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.22 -// +build go1.22 - package interp_test import ( @@ -19,16 +16,8 @@ import ( "golang.org/x/tools/internal/testenv" ) -func init() { - testdataTests = append(testdataTests, - "rangevarlifetime_go122.go", - "forvarlifetime_go122.go", - ) -} - -// TestExperimentRange tests files in testdata with GOEXPERIMENT=range set. -func TestExperimentRange(t *testing.T) { - testenv.NeedsGoExperiment(t, "range") +func TestIssue69298(t *testing.T) { + testenv.NeedsGo1Point(t, 23) // TODO: Is cwd actually needed here? goroot := makeGoroot(t) @@ -36,7 +25,7 @@ func TestExperimentRange(t *testing.T) { if err != nil { log.Fatal(err) } - run(t, filepath.Join(cwd, "testdata", "rangeoverint.go"), goroot) + run(t, filepath.Join(cwd, "testdata", "fixedbugs/issue69298.go"), goroot) } // TestRangeFunc tests range-over-func in a subprocess. diff --git a/go/ssa/interp/reflect.go b/go/ssa/interp/reflect.go index d7132562290..3143c077790 100644 --- a/go/ssa/interp/reflect.go +++ b/go/ssa/interp/reflect.go @@ -18,7 +18,6 @@ import ( "unsafe" "golang.org/x/tools/go/ssa" - "golang.org/x/tools/internal/aliases" ) type opaqueType struct { @@ -180,7 +179,7 @@ func ext۰reflect۰Zero(fr *frame, args []value) value { func reflectKind(t types.Type) reflect.Kind { switch t := t.(type) { - case *types.Named, *aliases.Alias: + case *types.Named, *types.Alias: return reflectKind(t.Underlying()) case *types.Basic: switch t.Kind() { diff --git a/go/ssa/interp/testdata/fixedbugs/issue69298.go b/go/ssa/interp/testdata/fixedbugs/issue69298.go new file mode 100644 index 00000000000..72ea0f54647 --- /dev/null +++ b/go/ssa/interp/testdata/fixedbugs/issue69298.go @@ -0,0 +1,31 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" +) + +type Seq[V any] func(yield func(V) bool) + +func AppendSeq[Slice ~[]E, E any](s Slice, seq Seq[E]) Slice { + for v := range seq { + s = append(s, v) + } + return s +} + +func main() { + seq := func(yield func(int) bool) { + for i := 0; i < 10; i += 2 { + if !yield(i) { + return + } + } + } + + s := AppendSeq([]int{1, 2}, seq) + fmt.Println(s) +} diff --git a/go/ssa/interp/testdata/forvarlifetime_go122.go b/go/ssa/interp/testdata/forvarlifetime_go122.go index 94c425f7deb..b41a2f82205 100644 --- a/go/ssa/interp/testdata/forvarlifetime_go122.go +++ b/go/ssa/interp/testdata/forvarlifetime_go122.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.22 - package main import ( diff --git a/go/ssa/interp/testdata/forvarlifetime_old.go b/go/ssa/interp/testdata/forvarlifetime_old.go index a89790568e2..13d64e85291 100644 --- a/go/ssa/interp/testdata/forvarlifetime_old.go +++ b/go/ssa/interp/testdata/forvarlifetime_old.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 +//go:build go1.21 // goversion can be pinned to anything strictly before 1.22. diff --git a/go/ssa/interp/testdata/rangeoverint.go b/go/ssa/interp/testdata/rangeoverint.go index 9a02d829764..60df354f4e2 100644 --- a/go/ssa/interp/testdata/rangeoverint.go +++ b/go/ssa/interp/testdata/rangeoverint.go @@ -1,8 +1,6 @@ package main -// Range over integers. - -// Currently requires 1.22 and GOEXPERIMENT=range. +// Range over integers (Go 1.22). import "fmt" diff --git a/go/ssa/interp/testdata/rangevarlifetime_old.go b/go/ssa/interp/testdata/rangevarlifetime_old.go index 345d2a9b205..2326bd851a7 100644 --- a/go/ssa/interp/testdata/rangevarlifetime_old.go +++ b/go/ssa/interp/testdata/rangevarlifetime_old.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 +//go:build go1.21 // goversion can be pinned to anything strictly before 1.22. diff --git a/go/ssa/interp/value.go b/go/ssa/interp/value.go index 94da28fd5b6..8fa0180ba05 100644 --- a/go/ssa/interp/value.go +++ b/go/ssa/interp/value.go @@ -46,7 +46,6 @@ import ( "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/aliases" ) type value interface{} @@ -119,7 +118,7 @@ func usesBuiltinMap(t types.Type) bool { switch t := t.(type) { case *types.Basic, *types.Chan, *types.Pointer: return true - case *types.Named, *aliases.Alias: + case *types.Named, *types.Alias: return usesBuiltinMap(t.Underlying()) case *types.Interface, *types.Array, *types.Struct: return false diff --git a/go/ssa/methods.go b/go/ssa/methods.go index b9560183a95..4b116f43072 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -11,7 +11,7 @@ import ( "go/types" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) // MethodValue returns the Function implementing method sel, building @@ -158,124 +158,23 @@ type methodSet struct { // // Thread-safe. // -// Acquires prog.runtimeTypesMu. +// Acquires prog.makeInterfaceTypesMu. func (prog *Program) RuntimeTypes() []types.Type { - prog.runtimeTypesMu.Lock() - defer prog.runtimeTypesMu.Unlock() - return prog.runtimeTypes.Keys() -} - -// forEachReachable calls f for type T and each type reachable from -// its type through reflection. -// -// The function f must use memoization to break cycles and -// return false when the type has already been visited. -// -// TODO(adonovan): publish in typeutil and share with go/callgraph/rta. -func forEachReachable(msets *typeutil.MethodSetCache, T types.Type, f func(types.Type) bool) { - var visit func(T types.Type, skip bool) - visit = func(T types.Type, skip bool) { - if !skip { - if !f(T) { - return - } - } - - // Recursion over signatures of each method. - tmset := msets.MethodSet(T) - for i := 0; i < tmset.Len(); i++ { - sig := tmset.At(i).Type().(*types.Signature) - // It is tempting to call visit(sig, false) - // but, as noted in golang.org/cl/65450043, - // the Signature.Recv field is ignored by - // types.Identical and typeutil.Map, which - // is confusing at best. - // - // More importantly, the true signature rtype - // reachable from a method using reflection - // has no receiver but an extra ordinary parameter. - // For the Read method of io.Reader we want: - // func(Reader, []byte) (int, error) - // but here sig is: - // func([]byte) (int, error) - // with .Recv = Reader (though it is hard to - // notice because it doesn't affect Signature.String - // or types.Identical). - // - // TODO(adonovan): construct and visit the correct - // non-method signature with an extra parameter - // (though since unnamed func types have no methods - // there is essentially no actual demand for this). - // - // TODO(adonovan): document whether or not it is - // safe to skip non-exported methods (as RTA does). - visit(sig.Params(), true) // skip the Tuple - visit(sig.Results(), true) // skip the Tuple - } - - switch T := T.(type) { - case *aliases.Alias: - visit(aliases.Unalias(T), skip) // emulates the pre-Alias behavior - - case *types.Basic: - // nop - - case *types.Interface: - // nop---handled by recursion over method set. - - case *types.Pointer: - visit(T.Elem(), false) - - case *types.Slice: - visit(T.Elem(), false) - - case *types.Chan: - visit(T.Elem(), false) - - case *types.Map: - visit(T.Key(), false) - visit(T.Elem(), false) - - case *types.Signature: - if T.Recv() != nil { - panic(fmt.Sprintf("Signature %s has Recv %s", T, T.Recv())) - } - visit(T.Params(), true) // skip the Tuple - visit(T.Results(), true) // skip the Tuple - - case *types.Named: - // A pointer-to-named type can be derived from a named - // type via reflection. It may have methods too. - visit(types.NewPointer(T), false) - - // Consider 'type T struct{S}' where S has methods. - // Reflection provides no way to get from T to struct{S}, - // only to S, so the method set of struct{S} is unwanted, - // so set 'skip' flag during recursion. - visit(T.Underlying(), true) // skip the unnamed type - - case *types.Array: - visit(T.Elem(), false) - - case *types.Struct: - for i, n := 0, T.NumFields(); i < n; i++ { - // TODO(adonovan): document whether or not - // it is safe to skip non-exported fields. - visit(T.Field(i).Type(), false) - } - - case *types.Tuple: - for i, n := 0, T.Len(); i < n; i++ { - visit(T.At(i).Type(), false) - } - - case *types.TypeParam, *types.Union: - // forEachReachable must not be called on parameterized types. - panic(T) - - default: - panic(T) - } + prog.makeInterfaceTypesMu.Lock() + defer prog.makeInterfaceTypesMu.Unlock() + + // Compute the derived types on demand, since many SSA clients + // never call RuntimeTypes, and those that do typically call + // it once (often within ssautil.AllFunctions, which will + // eventually not use it; see Go issue #69291.) This + // eliminates the need to eagerly compute all the element + // types during SSA building. + var runtimeTypes []types.Type + add := func(t types.Type) { runtimeTypes = append(runtimeTypes, t) } + var set typeutil.Map // for de-duping identical types + for t := range prog.makeInterfaceTypes { + typesinternal.ForEachElement(&set, &prog.MethodSets, t, add) } - visit(T, false) + + return runtimeTypes } diff --git a/go/ssa/print.go b/go/ssa/print.go index c890d7ee531..ef32672a26a 100644 --- a/go/ssa/print.go +++ b/go/ssa/print.go @@ -39,16 +39,8 @@ func relName(v Value, i Instruction) string { return v.Name() } -// normalizeAnyForTesting controls whether we replace occurrences of -// interface{} with any. It is only used for normalizing test output. -var normalizeAnyForTesting bool - func relType(t types.Type, from *types.Package) string { - s := types.TypeString(t, types.RelativeTo(from)) - if normalizeAnyForTesting { - s = strings.ReplaceAll(s, "interface{}", "any") - } - return s + return types.TypeString(t, types.RelativeTo(from)) } func relTerm(term *types.Term, from *types.Package) string { diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 285cba04a9f..ef2928e3b74 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -407,14 +407,87 @@ func (s *sanity) checkReferrerList(v Value) { } } +func (s *sanity) checkFunctionParams() { + signature := s.fn.Signature + params := s.fn.Params + + // startSigParams is the start of signature.Params() within params. + startSigParams := 0 + if signature.Recv() != nil { + startSigParams = 1 + } + + if startSigParams+signature.Params().Len() != len(params) { + s.errorf("function has %d parameters in signature but has %d after building", + startSigParams+signature.Params().Len(), len(params)) + return + } + + for i, param := range params { + var sigType types.Type + si := i - startSigParams + if si < 0 { + sigType = signature.Recv().Type() + } else { + sigType = signature.Params().At(si).Type() + } + + if !types.Identical(sigType, param.Type()) { + s.errorf("expect type %s in signature but got type %s in param %d", param.Type(), sigType, i) + } + } +} + +// checkTransientFields checks whether all transient fields of Function are cleared. +func (s *sanity) checkTransientFields() { + fn := s.fn + if fn.build != nil { + s.errorf("function transient field 'build' is not nil") + } + if fn.currentBlock != nil { + s.errorf("function transient field 'currentBlock' is not nil") + } + if fn.vars != nil { + s.errorf("function transient field 'vars' is not nil") + } + if fn.results != nil { + s.errorf("function transient field 'results' is not nil") + } + if fn.returnVars != nil { + s.errorf("function transient field 'returnVars' is not nil") + } + if fn.targets != nil { + s.errorf("function transient field 'targets' is not nil") + } + if fn.lblocks != nil { + s.errorf("function transient field 'lblocks' is not nil") + } + if fn.subst != nil { + s.errorf("function transient field 'subst' is not nil") + } + if fn.jump != nil { + s.errorf("function transient field 'jump' is not nil") + } + if fn.deferstack != nil { + s.errorf("function transient field 'deferstack' is not nil") + } + if fn.source != nil { + s.errorf("function transient field 'source' is not nil") + } + if fn.exits != nil { + s.errorf("function transient field 'exits' is not nil") + } + if fn.uniq != 0 { + s.errorf("function transient field 'uniq' is not zero") + } +} + func (s *sanity) checkFunction(fn *Function) bool { - // TODO(adonovan): check Function invariants: - // - check params match signature - // - check transient fields are nil - // - warn if any fn.Locals do not appear among block instructions. + s.fn = fn + s.checkFunctionParams() + s.checkTransientFields() // TODO(taking): Sanity check origin, typeparams, and typeargs. - s.fn = fn if fn.Prog == nil { s.errorf("nil Prog") } @@ -452,7 +525,23 @@ func (s *sanity) checkFunction(fn *Function) bool { s.errorf("got fromSource=%t, hasSyntax=%t; want same values", src, syn) } } + + // Build the set of valid referrers. + s.instrs = make(map[Instruction]unit) + + // TODO: switch to range-over-func when x/tools updates to 1.23. + // instrs are the instructions that are present in the function. + fn.instrs()(func(instr Instruction) bool { + s.instrs[instr] = unit{} + return true + }) + + // Check all Locals allocations appear in the function instruction. for i, l := range fn.Locals { + if _, present := s.instrs[l]; !present { + s.warnf("function doesn't contain Local alloc %s", l.Name()) + } + if l.Parent() != fn { s.errorf("Local %s at index %d has wrong parent", l.Name(), i) } @@ -460,13 +549,6 @@ func (s *sanity) checkFunction(fn *Function) bool { s.errorf("Local %s at index %d has Heap flag set", l.Name(), i) } } - // Build the set of valid referrers. - s.instrs = make(map[Instruction]unit) - for _, b := range fn.Blocks { - for _, instr := range b.Instrs { - s.instrs[instr] = unit{} - } - } for i, p := range fn.Params { if p.Parent() != fn { s.errorf("Param %s at index %d has wrong parent", p.Name(), i) @@ -527,6 +609,19 @@ func sanityCheckPackage(pkg *Package) { if pkg.Pkg == nil { panic(fmt.Sprintf("Package %s has no Object", pkg)) } + if pkg.info != nil { + panic(fmt.Sprintf("package %s field 'info' is not cleared", pkg)) + } + if pkg.files != nil { + panic(fmt.Sprintf("package %s field 'files' is not cleared", pkg)) + } + if pkg.created != nil { + panic(fmt.Sprintf("package %s field 'created' is not cleared", pkg)) + } + if pkg.initVersion != nil { + panic(fmt.Sprintf("package %s field 'initVersion' is not cleared", pkg)) + } + _ = pkg.String() // must not crash for name, mem := range pkg.Members { diff --git a/go/ssa/source_test.go b/go/ssa/source_test.go index 112581bb55b..bd156cbc5e8 100644 --- a/go/ssa/source_test.go +++ b/go/ssa/source_test.go @@ -10,7 +10,6 @@ import ( "fmt" "go/ast" "go/constant" - "go/parser" "go/token" "go/types" "os" @@ -20,9 +19,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/expect" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" - "golang.org/x/tools/go/ssa/ssautil" ) func TestObjValueLookup(t *testing.T) { @@ -30,17 +27,15 @@ func TestObjValueLookup(t *testing.T) { t.Skipf("no testdata directory on %s", runtime.GOOS) } - conf := loader.Config{ParserMode: parser.ParseComments} src, err := os.ReadFile("testdata/objlookup.go") if err != nil { t.Fatal(err) } readFile := func(filename string) ([]byte, error) { return src, nil } - f, err := conf.ParseFile("testdata/objlookup.go", src) - if err != nil { - t.Fatal(err) - } - conf.CreateFromFiles("main", f) + + mode := ssa.GlobalDebug /*|ssa.PrintFunctions*/ + mainPkg, ppkg := buildPackage(t, string(src), mode) + fset := ppkg.Fset // Maps each var Ident (represented "name:linenum") to the // kind of ssa.Value we expect (represented "Constant", "&Alloc"). @@ -49,61 +44,49 @@ func TestObjValueLookup(t *testing.T) { // Each note of the form @ssa(x, "BinOp") in testdata/objlookup.go // specifies an expectation that an object named x declared on the // same line is associated with an ssa.Value of type *ssa.BinOp. - notes, err := expect.ExtractGo(conf.Fset, f) + notes, err := expect.ExtractGo(fset, ppkg.Syntax[0]) if err != nil { t.Fatal(err) } for _, n := range notes { if n.Name != "ssa" { - t.Errorf("%v: unexpected note type %q, want \"ssa\"", conf.Fset.Position(n.Pos), n.Name) + t.Errorf("%v: unexpected note type %q, want \"ssa\"", fset.Position(n.Pos), n.Name) continue } if len(n.Args) != 2 { - t.Errorf("%v: ssa has %d args, want 2", conf.Fset.Position(n.Pos), len(n.Args)) + t.Errorf("%v: ssa has %d args, want 2", fset.Position(n.Pos), len(n.Args)) continue } ident, ok := n.Args[0].(expect.Identifier) if !ok { - t.Errorf("%v: got %v for arg 1, want identifier", conf.Fset.Position(n.Pos), n.Args[0]) + t.Errorf("%v: got %v for arg 1, want identifier", fset.Position(n.Pos), n.Args[0]) continue } exp, ok := n.Args[1].(string) if !ok { - t.Errorf("%v: got %v for arg 2, want string", conf.Fset.Position(n.Pos), n.Args[1]) + t.Errorf("%v: got %v for arg 2, want string", fset.Position(n.Pos), n.Args[1]) continue } - p, _, err := expect.MatchBefore(conf.Fset, readFile, n.Pos, string(ident)) + p, _, err := expect.MatchBefore(fset, readFile, n.Pos, string(ident)) if err != nil { t.Error(err) continue } - pos := conf.Fset.Position(p) + pos := fset.Position(p) key := fmt.Sprintf("%s:%d", ident, pos.Line) expectations[key] = exp } - iprog, err := conf.Load() - if err != nil { - t.Error(err) - return - } - - prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0) /*|ssa.PrintFunctions*/) - mainInfo := iprog.Created[0] - mainPkg := prog.Package(mainInfo.Pkg) - mainPkg.SetDebugMode(true) - mainPkg.Build() - var varIds []*ast.Ident var varObjs []*types.Var - for id, obj := range mainInfo.Defs { + for id, obj := range ppkg.TypesInfo.Defs { // Check invariants for func and const objects. switch obj := obj.(type) { case *types.Func: - checkFuncValue(t, prog, obj) + checkFuncValue(t, mainPkg.Prog, obj) case *types.Const: - checkConstValue(t, prog, obj) + checkConstValue(t, mainPkg.Prog, obj) case *types.Var: if id.Name == "_" { @@ -113,7 +96,7 @@ func TestObjValueLookup(t *testing.T) { varObjs = append(varObjs, obj) } } - for id, obj := range mainInfo.Uses { + for id, obj := range ppkg.TypesInfo.Uses { if obj, ok := obj.(*types.Var); ok { varIds = append(varIds, id) varObjs = append(varObjs, obj) @@ -124,8 +107,8 @@ func TestObjValueLookup(t *testing.T) { // The result varies based on the specific Ident. for i, id := range varIds { obj := varObjs[i] - ref, _ := astutil.PathEnclosingInterval(f, id.Pos(), id.Pos()) - pos := prog.Fset.Position(id.Pos()) + ref, _ := astutil.PathEnclosingInterval(ppkg.Syntax[0], id.Pos(), id.Pos()) + pos := fset.Position(id.Pos()) exp := expectations[fmt.Sprintf("%s:%d", id.Name, pos.Line)] if exp == "" { t.Errorf("%s: no expectation for var ident %s ", pos, id.Name) @@ -136,7 +119,7 @@ func TestObjValueLookup(t *testing.T) { wantAddr = true exp = exp[1:] } - checkVarValue(t, prog, mainPkg, ref, obj, exp, wantAddr) + checkVarValue(t, mainPkg, ref, obj, exp, wantAddr) } } @@ -180,12 +163,12 @@ func checkConstValue(t *testing.T, prog *ssa.Program, obj *types.Const) { } } -func checkVarValue(t *testing.T, prog *ssa.Program, pkg *ssa.Package, ref []ast.Node, obj *types.Var, expKind string, wantAddr bool) { +func checkVarValue(t *testing.T, pkg *ssa.Package, ref []ast.Node, obj *types.Var, expKind string, wantAddr bool) { // The prefix of all assertions messages. prefix := fmt.Sprintf("VarValue(%s @ L%d)", - obj, prog.Fset.Position(ref[0].Pos()).Line) + obj, pkg.Prog.Fset.Position(ref[0].Pos()).Line) - v, gotAddr := prog.VarValue(obj, pkg, ref) + v, gotAddr := pkg.Prog.VarValue(obj, pkg, ref) // Kind is the concrete type of the ssa Value. gotKind := "nil" @@ -234,26 +217,14 @@ func testValueForExpr(t *testing.T, testfile string) { t.Skipf("no testdata dir on %s", runtime.GOOS) } - conf := loader.Config{ParserMode: parser.ParseComments} - f, err := conf.ParseFile(testfile, nil) - if err != nil { - t.Error(err) - return - } - conf.CreateFromFiles("main", f) - - iprog, err := conf.Load() + src, err := os.ReadFile(testfile) if err != nil { - t.Error(err) - return + t.Fatal(err) } - mainInfo := iprog.Created[0] - - prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0)) - mainPkg := prog.Package(mainInfo.Pkg) - mainPkg.SetDebugMode(true) - mainPkg.Build() + mode := ssa.GlobalDebug /*|ssa.PrintFunctions*/ + mainPkg, ppkg := buildPackage(t, string(src), mode) + fset, file := ppkg.Fset, ppkg.Syntax[0] if false { // debugging @@ -265,7 +236,7 @@ func testValueForExpr(t *testing.T, testfile string) { } var parenExprs []*ast.ParenExpr - ast.Inspect(f, func(n ast.Node) bool { + ast.Inspect(file, func(n ast.Node) bool { if n != nil { if e, ok := n.(*ast.ParenExpr); ok { parenExprs = append(parenExprs, e) @@ -274,7 +245,7 @@ func testValueForExpr(t *testing.T, testfile string) { return true }) - notes, err := expect.ExtractGo(prog.Fset, f) + notes, err := expect.ExtractGo(fset, file) if err != nil { t.Fatal(err) } @@ -283,7 +254,7 @@ func testValueForExpr(t *testing.T, testfile string) { if want == "nil" { want = "" } - position := prog.Fset.Position(n.Pos) + position := fset.Position(n.Pos) var e ast.Expr for _, paren := range parenExprs { if paren.Pos() > n.Pos { @@ -296,7 +267,7 @@ func testValueForExpr(t *testing.T, testfile string) { continue } - path, _ := astutil.PathEnclosingInterval(f, n.Pos, n.Pos) + path, _ := astutil.PathEnclosingInterval(file, n.Pos, n.Pos) if path == nil { t.Errorf("%s: can't find AST path from root to comment: %s", position, want) continue @@ -318,76 +289,54 @@ func testValueForExpr(t *testing.T, testfile string) { if gotAddr { T = T.Underlying().(*types.Pointer).Elem() // deref } - if !types.Identical(T, mainInfo.TypeOf(e)) { - t.Errorf("%s: got type %s, want %s", position, mainInfo.TypeOf(e), T) + if etyp := ppkg.TypesInfo.TypeOf(e); !types.Identical(T, etyp) { + t.Errorf("%s: got type %s, want %s", position, etyp, T) } } } } -// findInterval parses input and returns the [start, end) positions of -// the first occurrence of substr in input. f==nil indicates failure; -// an error has already been reported in that case. -func findInterval(t *testing.T, fset *token.FileSet, input, substr string) (f *ast.File, start, end token.Pos) { - f, err := parser.ParseFile(fset, "", input, 0) - if err != nil { - t.Errorf("parse error: %s", err) - return - } - - i := strings.Index(input, substr) - if i < 0 { - t.Errorf("%q is not a substring of input", substr) - f = nil - return - } - - filePos := fset.File(f.Package) - return f, filePos.Pos(i), filePos.Pos(i + len(substr)) -} - func TestEnclosingFunction(t *testing.T) { tests := []struct { + desc string input string // the input file substr string // first occurrence of this string denotes interval fn string // name of expected containing function }{ // We use distinctive numbers as syntactic landmarks. - - // Ordinary function: - {`package main + {"Ordinary function", ` + package main func f() { println(1003) }`, "100", "main.f"}, - // Methods: - {`package main - type T int + {"Methods", ` + package main + type T int func (t T) f() { println(200) }`, "200", "(main.T).f"}, - // Function literal: - {`package main + {"Function literal", ` + package main func f() { println(func() { print(300) }) }`, "300", "main.f$1"}, - // Doubly nested - {`package main + {"Doubly nested", ` + package main func f() { println(func() { print(func() { print(350) })})}`, "350", "main.f$1$1"}, - // Implicit init for package-level var initializer. - {"package main; var a = 400", "400", "main.init"}, - // No code for constants: - {"package main; const a = 500", "500", "(none)"}, - // Explicit init() - {"package main; func init() { println(600) }", "600", "main.init#1"}, - // Multiple explicit init functions: - {`package main + {"Implicit init for package-level var initializer", ` + package main; var a = 400`, + "400", "main.init"}, + {"No code for constants", "package main; const a = 500", "500", "(none)"}, + {" Explicit init", "package main; func init() { println(600) }", "600", "main.init#1"}, + {"Multiple explicit init functions", ` + package main func init() { println("foo") } func init() { println(800) }`, "800", "main.init#2"}, - // init() containing FuncLit. - {`package main + {"init containing FuncLit", ` + package main func init() { println(func(){print(900)}) }`, "900", "main.init#1$1"}, - // generics - {`package main + {"generic", ` + package main type S[T any] struct{} func (*S[T]) Foo() { println(1000) } type P[T any] struct{ *S[T] }`, @@ -395,45 +344,39 @@ func TestEnclosingFunction(t *testing.T) { }, } for _, test := range tests { - conf := loader.Config{Fset: token.NewFileSet()} - f, start, end := findInterval(t, conf.Fset, test.input, test.substr) - if f == nil { - continue - } - path, exact := astutil.PathEnclosingInterval(f, start, end) - if !exact { - t.Errorf("EnclosingFunction(%q) not exact", test.substr) - continue - } + t.Run(test.desc, func(t *testing.T) { + pkg, ppkg := buildPackage(t, test.input, ssa.BuilderMode(0)) + fset, file := ppkg.Fset, ppkg.Syntax[0] + + // Find [start,end) positions of the first occurrence of substr in file. + index := strings.Index(test.input, test.substr) + if index < 0 { + t.Fatalf("%q is not a substring of input", test.substr) + } + filePos := fset.File(file.Package) + start, end := filePos.Pos(index), filePos.Pos(index+len(test.substr)) - conf.CreateFromFiles("main", f) + path, exact := astutil.PathEnclosingInterval(file, start, end) + if !exact { + t.Fatalf("PathEnclosingInterval(%q) not exact", test.substr) + } - iprog, err := conf.Load() - if err != nil { - t.Error(err) - continue - } - prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0)) - pkg := prog.Package(iprog.Created[0].Pkg) - pkg.Build() - - name := "(none)" - fn := ssa.EnclosingFunction(pkg, path) - if fn != nil { - name = fn.String() - } + name := "(none)" + fn := ssa.EnclosingFunction(pkg, path) + if fn != nil { + name = fn.String() + } - if name != test.fn { - t.Errorf("EnclosingFunction(%q in %q) got %s, want %s", - test.substr, test.input, name, test.fn) - continue - } + if name != test.fn { + t.Errorf("EnclosingFunction(%q in %q) got %s, want %s", + test.substr, test.input, name, test.fn) + } - // While we're here: test HasEnclosingFunction. - if has := ssa.HasEnclosingFunction(pkg, path); has != (fn != nil) { - t.Errorf("HasEnclosingFunction(%q in %q) got %v, want %v", - test.substr, test.input, has, fn != nil) - continue - } + // While we're here: test HasEnclosingFunction. + if has := ssa.HasEnclosingFunction(pkg, path); has != (fn != nil) { + t.Errorf("HasEnclosingFunction(%q in %q) got %v, want %v", + test.substr, test.input, has, fn != nil) + } + }) } } diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 1231afd9e0c..4fa9831079c 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -37,8 +37,9 @@ type Program struct { hasParamsMu sync.Mutex hasParams typeparams.Free - runtimeTypesMu sync.Mutex - runtimeTypes typeutil.Map // set of runtime types (from MakeInterface) + // set of concrete types used as MakeInterface operands + makeInterfaceTypesMu sync.Mutex + makeInterfaceTypes map[types.Type]unit // (may contain redundant identical types) // objectMethods is a memoization of objectMethod // to avoid creation of duplicate methods from type information. @@ -341,7 +342,7 @@ type Function struct { // source information Synthetic string // provenance of synthetic function; "" for true source functions syntax ast.Node // *ast.Func{Decl,Lit}, if from syntax (incl. generic instances) or (*ast.RangeStmt if a yield function) - info *types.Info // type annotations (iff syntax != nil) + info *types.Info // type annotations (if syntax != nil) goversion string // Go version of syntax (NB: init is special) parent *Function // enclosing function if anon; nil if global diff --git a/go/ssa/ssautil/deprecated.go b/go/ssa/ssautil/deprecated.go new file mode 100644 index 00000000000..4feff7131ac --- /dev/null +++ b/go/ssa/ssautil/deprecated.go @@ -0,0 +1,36 @@ +// Copyright 2015 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 ssautil + +// This file contains deprecated public APIs. +// We discourage their use. + +import ( + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/ssa" +) + +// CreateProgram returns a new program in SSA form, given a program +// loaded from source. An SSA package is created for each transitively +// error-free package of lprog. +// +// Code for bodies of functions is not built until Build is called +// on the result. +// +// The mode parameter controls diagnostics and checking during SSA construction. +// +// Deprecated: Use [golang.org/x/tools/go/packages] and the [Packages] +// function instead; see ssa.Example_loadPackages. +func CreateProgram(lprog *loader.Program, mode ssa.BuilderMode) *ssa.Program { + prog := ssa.NewProgram(lprog.Fset, mode) + + for _, info := range lprog.AllPackages { + if info.TransitivelyErrorFree { + prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) + } + } + + return prog +} diff --git a/go/ssa/ssautil/deprecated_test.go b/go/ssa/ssautil/deprecated_test.go new file mode 100644 index 00000000000..9bc39e7eebd --- /dev/null +++ b/go/ssa/ssautil/deprecated_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ssautil_test + +// Tests of deprecated public APIs. +// We are keeping some tests around to have some test of the public API. + +import ( + "go/parser" + "os" + "testing" + + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +// TestCreateProgram tests CreateProgram which has an x/tools/go/loader.Program. +func TestCreateProgram(t *testing.T) { + conf := loader.Config{ParserMode: parser.ParseComments} + f, err := conf.ParseFile("hello.go", hello) + if err != nil { + t.Fatal(err) + } + + conf.CreateFromFiles("main", f) + iprog, err := conf.Load() + if err != nil { + t.Fatal(err) + } + if len(iprog.Created) != 1 { + t.Fatalf("Expected 1 Created package. got %d", len(iprog.Created)) + } + pkg := iprog.Created[0].Pkg + + prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0)) + ssapkg := prog.Package(pkg) + ssapkg.Build() + + if pkg.Name() != "main" { + t.Errorf("pkg.Name() = %s, want main", pkg.Name()) + } + if ssapkg.Func("main") == nil { + ssapkg.WriteTo(os.Stderr) + t.Errorf("ssapkg has no main function") + } +} diff --git a/go/ssa/ssautil/load.go b/go/ssa/ssautil/load.go index 3daa67a07e4..51fba054541 100644 --- a/go/ssa/ssautil/load.go +++ b/go/ssa/ssautil/load.go @@ -11,7 +11,6 @@ import ( "go/token" "go/types" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/internal/versions" @@ -111,29 +110,6 @@ func doPackages(initial []*packages.Package, mode ssa.BuilderMode, deps bool) (* return prog, ssapkgs } -// CreateProgram returns a new program in SSA form, given a program -// loaded from source. An SSA package is created for each transitively -// error-free package of lprog. -// -// Code for bodies of functions is not built until Build is called -// on the result. -// -// The mode parameter controls diagnostics and checking during SSA construction. -// -// Deprecated: Use [golang.org/x/tools/go/packages] and the [Packages] -// function instead; see ssa.Example_loadPackages. -func CreateProgram(lprog *loader.Program, mode ssa.BuilderMode) *ssa.Program { - prog := ssa.NewProgram(lprog.Fset, mode) - - for _, info := range lprog.AllPackages { - if info.TransitivelyErrorFree { - prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) - } - } - - return prog -} - // BuildPackage builds an SSA program with SSA intermediate // representation (IR) for all functions of a single package. // diff --git a/go/ssa/ssautil/switch_test.go b/go/ssa/ssautil/switch_test.go index 6db41052454..081b09010ee 100644 --- a/go/ssa/ssautil/switch_test.go +++ b/go/ssa/ssautil/switch_test.go @@ -10,32 +10,28 @@ package ssautil_test import ( - "go/parser" "strings" "testing" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/txtar" ) func TestSwitches(t *testing.T) { - conf := loader.Config{ParserMode: parser.ParseComments} - f, err := conf.ParseFile("testdata/switches.go", nil) + archive, err := txtar.ParseFile("testdata/switches.txtar") if err != nil { - t.Error(err) - return + t.Fatal(err) } - - conf.CreateFromFiles("main", f) - iprog, err := conf.Load() - if err != nil { - t.Error(err) - return + ppkgs := testfiles.LoadPackages(t, archive, ".") + if len(ppkgs) != 1 { + t.Fatalf("Expected to load one package but got %d", len(ppkgs)) } + f := ppkgs[0].Syntax[0] - prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0)) - mainPkg := prog.Package(iprog.Created[0].Pkg) + prog, _ := ssautil.Packages(ppkgs, ssa.BuilderMode(0)) + mainPkg := prog.Package(ppkgs[0].Types) mainPkg.Build() for _, mem := range mainPkg.Members { diff --git a/go/ssa/ssautil/testdata/switches.go b/go/ssa/ssautil/testdata/switches.txtar similarity index 99% rename from go/ssa/ssautil/testdata/switches.go rename to go/ssa/ssautil/testdata/switches.txtar index 8ab4c118f16..1f0d96c58d9 100644 --- a/go/ssa/ssautil/testdata/switches.go +++ b/go/ssa/ssautil/testdata/switches.txtar @@ -1,5 +1,8 @@ -// +build ignore +-- go.mod -- +module example.com +go 1.22 +-- switches.go -- package main // This file is the input to TestSwitches in switch_test.go. diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go index e56d6a98156..9b78cfbf839 100644 --- a/go/ssa/stdlib_test.go +++ b/go/ssa/stdlib_test.go @@ -35,13 +35,18 @@ func bytesAllocated() uint64 { return stats.Alloc } -// TestStdlib loads the entire standard library and its tools. +// TestStdlib loads the entire standard library and its tools and all +// their dependencies. +// +// (As of go1.23, std is transitively closed, so adding the -deps flag +// doesn't increase its result set. The cmd pseudomodule of course +// depends on a good chunk of std, but the std+cmd set is also +// transitively closed, so long as -pgo=off.) // // Apart from a small number of internal packages that are not // returned by the 'std' query, the set is essentially transitively // closed, so marginal per-dependency costs are invisible. func TestStdlib(t *testing.T) { - t.Skip("broken; see https://go.dev/issues/69287") testLoad(t, 500, "std", "cmd") } @@ -58,6 +63,7 @@ func TestNetHTTP(t *testing.T) { // This can under some schedules create a cycle of dependencies // where both need to wait on the other to finish building. func TestCycles(t *testing.T) { + testenv.NeedsGo1Point(t, 23) // internal/trace/testtrace was added in 1.23. testLoad(t, 120, "net/http", "internal/trace/testtrace") } @@ -78,6 +84,9 @@ func testLoad(t *testing.T, minPkgs int, patterns ...string) { if err != nil { t.Fatal(err) } + if packages.PrintErrors(pkgs) > 0 { + t.Fatal("there were errors loading the packages") + } t1 := time.Now() alloc1 := bytesAllocated() @@ -195,9 +204,13 @@ func srcFunctions(prog *ssa.Program, pkgs []*packages.Package) (res []*ssa.Funct if decl, ok := decl.(*ast.FuncDecl); ok { obj := pkg.TypesInfo.Defs[decl.Name].(*types.Func) if obj == nil { - panic("nil *Func") + panic("nil *types.Func: " + decl.Name.Name) + } + fn := prog.FuncValue(obj) + if fn == nil { + panic("nil *ssa.Function: " + obj.String()) } - addSrcFunc(prog.FuncValue(obj)) + addSrcFunc(fn) } } } diff --git a/go/ssa/subst.go b/go/ssa/subst.go index 631515882d3..fc870235c42 100644 --- a/go/ssa/subst.go +++ b/go/ssa/subst.go @@ -144,7 +144,7 @@ func (subst *subster) typ(t types.Type) (res types.Type) { case *types.Interface: return subst.interface_(t) - case *aliases.Alias: + case *types.Alias: return subst.alias(t) case *types.Named: @@ -317,7 +317,7 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { return types.NewInterfaceType(methods, embeds).Complete() } -func (subst *subster) alias(t *aliases.Alias) types.Type { +func (subst *subster) alias(t *types.Alias) types.Type { // See subster.named. This follows the same strategy. tparams := aliases.TypeParams(t) targs := aliases.TypeArgs(t) @@ -633,7 +633,7 @@ func reaches(t types.Type, c map[types.Type]bool) (res bool) { return true } } - case *types.Named, *aliases.Alias: + case *types.Named, *types.Alias: return reaches(t.Underlying(), c) default: panic("unreachable") diff --git a/go/ssa/testdata/objlookup.go b/go/ssa/testdata/objlookup.go index b040d747333..7c79f0cd5e9 100644 --- a/go/ssa/testdata/objlookup.go +++ b/go/ssa/testdata/objlookup.go @@ -1,5 +1,3 @@ -// +build ignore - package main // This file is the input to TestObjValueLookup in source_test.go, @@ -15,8 +13,10 @@ package main // are always values not addresses, so no annotations are needed. The // declaration is enough. -import "fmt" -import "os" +import ( + "fmt" + "os" +) type J int diff --git a/go/ssa/testdata/src/runtime/runtime.go b/go/ssa/testdata/src/runtime/runtime.go index 9feed5c995c..0363c85aaf1 100644 --- a/go/ssa/testdata/src/runtime/runtime.go +++ b/go/ssa/testdata/src/runtime/runtime.go @@ -3,3 +3,5 @@ package runtime func GC() func SetFinalizer(obj, finalizer any) + +func Caller(skip int) (pc uintptr, file string, line int, ok bool) diff --git a/go/ssa/testdata/structconv.go b/go/ssa/testdata/structconv.go index c0b4b840ee5..74661d1ed52 100644 --- a/go/ssa/testdata/structconv.go +++ b/go/ssa/testdata/structconv.go @@ -1,5 +1,3 @@ -// +build ignore - // This file is the input to TestValueForExprStructConv in identical_test.go, // which uses the same framework as TestValueForExpr does in source_test.go. // diff --git a/go/ssa/testdata/valueforexpr.go b/go/ssa/testdata/valueforexpr.go index 703c316a707..8c834ef7a05 100644 --- a/go/ssa/testdata/valueforexpr.go +++ b/go/ssa/testdata/valueforexpr.go @@ -1,6 +1,3 @@ -//go:build ignore -// +build ignore - package main // This file is the input to TestValueForExpr in source_test.go, which diff --git a/go/ssa/testhelper_test.go b/go/ssa/testhelper_test.go deleted file mode 100644 index 8d08bbb757c..00000000000 --- a/go/ssa/testhelper_test.go +++ /dev/null @@ -1,10 +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. - -package ssa - -// SetNormalizeAnyForTesting is exported here for external tests. -func SetNormalizeAnyForTesting(normalize bool) { - normalizeAnyForTesting = normalize -} diff --git a/go/ssa/testutil_test.go b/go/ssa/testutil_test.go new file mode 100644 index 00000000000..58680b282c6 --- /dev/null +++ b/go/ssa/testutil_test.go @@ -0,0 +1,146 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file defines helper functions for SSA tests. + +package ssa_test + +import ( + "fmt" + "go/parser" + "go/token" + "io/fs" + "os" + "testing" + "testing/fstest" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/txtar" +) + +// goMod returns a go.mod file containing a name and a go directive +// for the major version. If major < 0, use the current go toolchain +// version. +func goMod(name string, major int) []byte { + if major < 0 { + major = testenv.Go1Point() + } + return fmt.Appendf(nil, "module %s\ngo 1.%d", name, major) +} + +// overlayFS returns a simple in-memory filesystem. +func overlayFS(overlay map[string][]byte) fstest.MapFS { + // taking: Maybe loadPackages should take an overlay instead? + fs := make(fstest.MapFS) + for name, data := range overlay { + fs[name] = &fstest.MapFile{Data: data} + } + return fs +} + +// openTxtar opens a txtar file as a filesystem. +func openTxtar(t testing.TB, file string) fs.FS { + // TODO(taking): Move to testfiles? + t.Helper() + + ar, err := txtar.ParseFile(file) + if err != nil { + t.Fatal(err) + } + + fs, err := txtar.FS(ar) + if err != nil { + t.Fatal(err) + } + + return fs +} + +// loadPackages copies the files in a source file system to a unique temporary +// directory and loads packages matching the given patterns from the temporary directory. +// +// TODO(69556): Migrate loader tests to loadPackages. +func loadPackages(t testing.TB, src fs.FS, patterns ...string) []*packages.Package { + t.Helper() + testenv.NeedsGoBuild(t) // for go/packages + + // TODO(taking): src and overlays are very similar. Overlays could have nicer paths. + // Look into migrating src to overlays. + dir := testfiles.CopyToTmp(t, src) + + cfg := &packages.Config{ + Dir: dir, + Mode: packages.NeedSyntax | + packages.NeedTypesInfo | + packages.NeedDeps | + packages.NeedName | + packages.NeedFiles | + packages.NeedImports | + packages.NeedCompiledGoFiles | + packages.NeedTypes, + Env: append(os.Environ(), + "GO111MODULES=on", + "GOPATH=", + "GOWORK=off", + "GOPROXY=off"), + } + pkgs, err := packages.Load(cfg, patterns...) + if err != nil { + t.Fatal(err) + } + if packages.PrintErrors(pkgs) > 0 { + t.Fatal("there were errors") + } + return pkgs +} + +// buildPackage builds the content of a go file into: +// * a module with the same name as the package at the current go version, +// * loads the *package.Package, +// * checks that (*packages.Packages).Syntax contains one file, +// * builds the *ssa.Package (and not its dependencies), and +// * returns the built *ssa.Package and the loaded packages.Package. +// +// TODO(adonovan): factor with similar loadFile (2x) in cha/cha_test.go and vta/helpers_test.go. +func buildPackage(t testing.TB, content string, mode ssa.BuilderMode) (*ssa.Package, *packages.Package) { + name := parsePackageClause(t, content) + + fs := overlayFS(map[string][]byte{ + "go.mod": goMod(name, -1), + "input.go": []byte(content), + }) + ppkgs := loadPackages(t, fs, name) + if len(ppkgs) != 1 { + t.Fatalf("Expected to load 1 package from pattern %q. got %d", name, len(ppkgs)) + } + ppkg := ppkgs[0] + + if len(ppkg.Syntax) != 1 { + t.Fatalf("Expected 1 file in package %q. got %d", ppkg, len(ppkg.Syntax)) + } + + prog, _ := ssautil.Packages(ppkgs, mode) + + ssapkg := prog.Package(ppkg.Types) + if ssapkg == nil { + t.Fatalf("Failed to find ssa package for %q", ppkg.Types) + } + ssapkg.Build() + + return ssapkg, ppkg +} + +// parsePackageClause is a test helper to extract the package name from a string +// containing the content of a go file. +func parsePackageClause(t testing.TB, content string) string { + f, err := parser.ParseFile(token.NewFileSet(), "", content, parser.PackageClauseOnly) + if err != nil { + t.Fatalf("parsing the file %q failed with error: %s", content, err) + } + return f.Name.Name +} diff --git a/go/ssa/util.go b/go/ssa/util.go index 549c9c819ea..cdc46209e7c 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -15,9 +15,7 @@ import ( "os" "sync" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" ) @@ -36,7 +34,7 @@ func assert(p bool, msg string) { //// AST utilities -func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } +func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } // isBlankIdent returns true iff e is an Ident with name "_". // They have no associated types.Object, and thus no type. @@ -45,13 +43,6 @@ func isBlankIdent(e ast.Expr) bool { return ok && id.Name == "_" } -// rangePosition is the position to give for the `range` token in a RangeStmt. -var rangePosition = func(rng *ast.RangeStmt) token.Pos { - // Before 1.20, this is unreachable. - // rng.For is a close, but incorrect position. - return rng.For -} - //// Type utilities. Some of these belong in go/types. // isNonTypeParamInterface reports whether t is an interface type but not a type parameter. @@ -268,7 +259,7 @@ func instanceArgs(info *types.Info, id *ast.Ident) []types.Type { return targs } -// Mapping of a type T to a canonical instance C s.t. types.Indentical(T, C). +// Mapping of a type T to a canonical instance C s.t. types.Identical(T, C). // Thread-safe. type canonizer struct { mu sync.Mutex @@ -295,7 +286,7 @@ func (c *canonizer) List(ts []types.Type) *typeList { // Is there some top level alias? var found bool for _, t := range ts { - if _, ok := t.(*aliases.Alias); ok { + if _, ok := t.(*types.Alias); ok { found = true break } @@ -306,7 +297,7 @@ func (c *canonizer) List(ts []types.Type) *typeList { cp := make([]types.Type, len(ts)) // copy with top level aliases removed. for i, t := range ts { - cp[i] = aliases.Unalias(t) + cp[i] = types.Unalias(t) } return cp } @@ -323,7 +314,7 @@ func (c *canonizer) List(ts []types.Type) *typeList { // For performance, reasons the canonical instance is order-dependent, // and may contain deeply nested aliases. func (c *canonizer) Type(T types.Type) types.Type { - T = aliases.Unalias(T) // remove the top level alias. + T = types.Unalias(T) // remove the top level alias. c.mu.Lock() defer c.mu.Unlock() @@ -403,10 +394,10 @@ func (m *typeListMap) hash(ts []types.Type) uint32 { // instantiateMethod instantiates m with targs and returns a canonical representative for this method. func (canon *canonizer) instantiateMethod(m *types.Func, targs []types.Type, ctxt *types.Context) *types.Func { recv := recvType(m) - if p, ok := aliases.Unalias(recv).(*types.Pointer); ok { + if p, ok := types.Unalias(recv).(*types.Pointer); ok { recv = p.Elem() } - named := aliases.Unalias(recv).(*types.Named) + named := types.Unalias(recv).(*types.Named) inst, err := types.Instantiate(ctxt, named.Origin(), targs, false) if err != nil { panic(err) diff --git a/go/ssa/util_go120.go b/go/ssa/util_go120.go deleted file mode 100644 index 9e8ea874e14..00000000000 --- a/go/ssa/util_go120.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.20 -// +build go1.20 - -package ssa - -import ( - "go/ast" - "go/token" -) - -func init() { - rangePosition = func(rng *ast.RangeStmt) token.Pos { return rng.Range } -} diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go index c88bba5069a..e8b8cb9bbbe 100644 --- a/go/types/internal/play/play.go +++ b/go/types/internal/play/play.go @@ -9,6 +9,9 @@ // It is intended for convenient exploration and debugging of // go/types. The command and its web interface are not officially // supported and they may be changed arbitrarily in the future. + +//go:debug gotypesalias=0 + package main import ( @@ -30,7 +33,6 @@ 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/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -284,7 +286,7 @@ func formatObj(out *strings.Builder, fset *token.FileSet, ref string, obj types. if obj.IsAlias() { kind = "type alias" } - if named, ok := aliases.Unalias(obj.Type()).(*types.Named); ok { + if named, ok := types.Unalias(obj.Type()).(*types.Named); ok { origin = named.Obj() } } diff --git a/go/types/objectpath/objectpath.go b/go/types/objectpath/objectpath.go index 9ada177758f..a70b727f2c6 100644 --- a/go/types/objectpath/objectpath.go +++ b/go/types/objectpath/objectpath.go @@ -228,7 +228,7 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { // Reject obviously non-viable cases. switch obj := obj.(type) { case *types.TypeName: - if _, ok := aliases.Unalias(obj.Type()).(*types.TypeParam); !ok { + if _, ok := types.Unalias(obj.Type()).(*types.TypeParam); !ok { // With the exception of type parameters, only package-level type names // have a path. return "", fmt.Errorf("no path for %v", obj) @@ -280,7 +280,7 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { path = append(path, opType) T := o.Type() - if alias, ok := T.(*aliases.Alias); ok { + if alias, ok := T.(*types.Alias); ok { if r := findTypeParam(obj, aliases.TypeParams(alias), path, opTypeParam, nil); r != nil { return Path(r), nil } @@ -320,7 +320,7 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { } // Inspect declared methods of defined types. - if T, ok := aliases.Unalias(o.Type()).(*types.Named); ok { + if T, ok := types.Unalias(o.Type()).(*types.Named); ok { path = append(path, opType) // The method index here is always with respect // to the underlying go/types data structures, @@ -449,8 +449,8 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) { // nil, it will be allocated as necessary. func find(obj types.Object, T types.Type, path []byte, seen map[*types.TypeName]bool) []byte { switch T := T.(type) { - case *aliases.Alias: - return find(obj, aliases.Unalias(T), path, seen) + case *types.Alias: + return find(obj, types.Unalias(T), path, seen) case *types.Basic, *types.Named: // Named types belonging to pkg were handled already, // so T must belong to another package. No path. @@ -626,7 +626,7 @@ func Object(pkg *types.Package, p Path) (types.Object, error) { // Inv: t != nil, obj == nil - t = aliases.Unalias(t) + t = types.Unalias(t) switch code { case opElem: hasElem, ok := t.(hasElem) // Pointer, Slice, Array, Chan, Map @@ -664,7 +664,7 @@ func Object(pkg *types.Package, p Path) (types.Object, error) { t = named.Underlying() case opRhs: - if alias, ok := t.(*aliases.Alias); ok { + if alias, ok := t.(*types.Alias); ok { t = aliases.Rhs(alias) } else if false && aliases.Enabled() { // The Enabled check is too expensive, so for now we diff --git a/go/types/objectpath/objectpath_go118_test.go b/go/types/objectpath/objectpath_go118_test.go index 37bb0b18b32..0eb2f024f88 100644 --- a/go/types/objectpath/objectpath_go118_test.go +++ b/go/types/objectpath/objectpath_go118_test.go @@ -8,15 +8,17 @@ import ( "go/types" "testing" - "golang.org/x/tools/go/buildutil" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/types/objectpath" ) // TODO(adonovan): merge this back into objectpath_test.go. func TestGenericPaths(t *testing.T) { - pkgs := map[string]map[string]string{ - "b": {"b.go": ` + const src = ` +-- go.mod -- +module x.io +go 1.18 + +-- b/b.go -- package b const C int = 1 @@ -33,8 +35,10 @@ func (N) M1() type A = T[int, N] func F[FP0 any, FP1 interface{ M() }](FP0, FP1) {} -`}, - } +` + + pkgmap := loadPackages(t, src, "./b") + paths := []pathTest{ // Good paths {"b", "T", "type b.T[TP0 any, TP1 interface{M0(); M1()}] struct{}", ""}, @@ -60,16 +64,8 @@ func F[FP0 any, FP1 interface{ M() }](FP0, FP1) {} {"b", "T.T1M0", "", "cannot apply 'M' to TP1 (got *types.TypeParam, want interface or named)"}, {"b", "C.T0", "", "cannot apply 'T' to int (got *types.Basic, want named or signature)"}, } - - conf := loader.Config{Build: buildutil.FakeContext(pkgs)} - conf.Import("b") - prog, err := conf.Load() - if err != nil { - t.Fatal(err) - } - for _, test := range paths { - if err := testPath(prog, test); err != nil { + if err := testPath(pkgmap, test); err != nil { t.Error(err) } } @@ -95,8 +91,12 @@ func F[FP0 any, FP1 interface{ M() }](FP0, FP1) {} } func TestGenericPaths_Issue51717(t *testing.T) { - pkgs := map[string]map[string]string{ - "p": {"p.go": ` + const src = ` +-- go.mod -- +module x.io +go 1.18 + +-- p/p.go -- package p type S struct{} @@ -110,8 +110,9 @@ func F[WL interface{ N(item W) WL }, W any]() { } func main() {} -`}, - } +` + pkgmap := loadPackages(t, src, "./p") + paths := []pathTest{ {"p", "F.T0CM0.RA0", "var WL", ""}, {"p", "F.T0CM0.RA0.CM0", "func (interface).N(item W) WL", ""}, @@ -120,16 +121,8 @@ func main() {} // because F is searched before S. {"p", "S.M0", "func (p.S).M()", ""}, } - - conf := loader.Config{Build: buildutil.FakeContext(pkgs)} - conf.Import("p") - prog, err := conf.Load() - if err != nil { - t.Fatal(err) - } - for _, test := range paths { - if err := testPath(prog, test); err != nil { + if err := testPath(pkgmap, test); err != nil { t.Error(err) } } diff --git a/go/types/objectpath/objectpath_test.go b/go/types/objectpath/objectpath_test.go index 5b3c6159d1b..838f1be44df 100644 --- a/go/types/objectpath/objectpath_test.go +++ b/go/types/objectpath/objectpath_test.go @@ -13,14 +13,16 @@ import ( "go/parser" "go/token" "go/types" + "slices" "strings" "testing" - "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/gcexportdata" - "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/txtar" ) func TestPaths(t *testing.T) { @@ -32,14 +34,17 @@ func TestPaths(t *testing.T) { } func testPaths(t *testing.T, gotypesalias int) { - // override default set by go1.19 in go.mod + // override default set by go1.22 in go.mod t.Setenv("GODEBUG", fmt.Sprintf("gotypesalias=%d", gotypesalias)) - pkgs := map[string]map[string]string{ - "b": {"b.go": ` + const src = ` +-- go.mod -- +module x.io + +-- b/b.go -- package b -import "a" +import "x.io/a" const C = a.Int(0) @@ -70,16 +75,17 @@ func (unexportedType) F() {} // not reachable from package's public API (export type S struct{t struct{x int}} type R []struct{y int} type Q [2]struct{z int} -`}, - "a": {"a.go": ` + +-- a/a.go -- package a type Int int type T struct{x, y int} +` + + pkgmap := loadPackages(t, src, "./a", "./b") -`}, - } paths := []pathTest{ // Good paths {"b", "C", "const b.C a.Int", ""}, @@ -143,20 +149,12 @@ type T struct{x, y int} {"b", "F.PA4", "", "tuple index 4 out of range [0-4)"}, {"b", "F.XO", "", "invalid path: unknown code 'X'"}, } - conf := loader.Config{Build: buildutil.FakeContext(pkgs)} - conf.Import("a") - conf.Import("b") - prog, err := conf.Load() - if err != nil { - t.Fatal(err) - } - for _, test := range paths { // go1.22 gotypesalias=1 prints aliases wrong: "type A = A". // (Fixed by https://go.dev/cl/574716.) // Work around it here by updating the expectation. - if slicesContains(build.Default.ReleaseTags, "go1.22") && - !slicesContains(build.Default.ReleaseTags, "go1.23") && + if slices.Contains(build.Default.ReleaseTags, "go1.22") && + !slices.Contains(build.Default.ReleaseTags, "go1.23") && aliases.Enabled() { if test.pkg == "b" && test.path == "A" { test.wantobj = "type b.A = b.A" @@ -166,13 +164,13 @@ type T struct{x, y int} } } - if err := testPath(prog, test); err != nil { + if err := testPath(pkgmap, test); err != nil { t.Error(err) } } // bad objects - bInfo := prog.Imported["b"] + b := pkgmap["x.io/b"] for _, test := range []struct { obj types.Object wantErr string @@ -180,29 +178,42 @@ type T struct{x, y int} {types.Universe.Lookup("nil"), "predeclared nil has no path"}, {types.Universe.Lookup("len"), "predeclared builtin len has no path"}, {types.Universe.Lookup("int"), "predeclared type int has no path"}, - {bInfo.Implicits[bInfo.Files[0].Imports[0]], "no path for package a"}, // import "a" - {bInfo.Pkg.Scope().Lookup("unexportedFunc"), "no path for non-exported func b.unexportedFunc()"}, + {b.TypesInfo.Implicits[b.Syntax[0].Imports[0]], "no path for package a (\"a\")"}, // import "a" + {b.Types.Scope().Lookup("unexportedFunc"), "no path for non-exported func b.unexportedFunc()"}, } { path, err := objectpath.For(test.obj) if err == nil { t.Errorf("Object(%s) = %q, want error", test.obj, path) continue } - if err.Error() != test.wantErr { - t.Errorf("Object(%s) error was %q, want %q", test.obj, err, test.wantErr) + gotErr := strings.ReplaceAll(err.Error(), "x.io/", "") + if gotErr != test.wantErr { + t.Errorf("Object(%s) error was %q, want %q", test.obj, gotErr, test.wantErr) continue } } } +// loadPackages expands the archive and loads the package patterns relative to its root. +func loadPackages(t *testing.T, archive string, patterns ...string) map[string]*packages.Package { + // TODO(adonovan): ExtractTxtarToTmp (sans File) would be useful. + ar := txtar.Parse([]byte(archive)) + pkgs := testfiles.LoadPackages(t, ar, patterns...) + m := make(map[string]*packages.Package) + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + m[pkg.Types.Path()] = pkg + }) + return m +} + type pathTest struct { - pkg string + pkg string // sans "x.io/" module prefix path objectpath.Path wantobj string - wantErr string + wantErr string // after each "x.io/" replaced with "" } -func testPath(prog *loader.Program, test pathTest) error { +func testPath(pkgmap map[string]*packages.Package, test pathTest) error { // We test objectpath by enumerating a set of paths // and ensuring that Path(pkg, Object(pkg, path)) == path. // @@ -217,23 +228,24 @@ func testPath(prog *loader.Program, test pathTest) error { // // The downside is that the test depends on the path encoding. // The upside is that the test exercises the encoding. + pkg := pkgmap["x.io/"+test.pkg].Types - pkg := prog.Imported[test.pkg].Pkg // check path -> object obj, err := objectpath.Object(pkg, test.path) if (test.wantErr != "") != (err != nil) { return fmt.Errorf("Object(%s, %q) returned error %q, want %q", pkg.Path(), test.path, err, test.wantErr) } if test.wantErr != "" { - if got := err.Error(); got != test.wantErr { + gotErr := strings.ReplaceAll(err.Error(), "x.io/", "") + if gotErr != test.wantErr { return fmt.Errorf("Object(%s, %q) error was %q, want %q", - pkg.Path(), test.path, got, test.wantErr) + pkg.Path(), test.path, gotErr, test.wantErr) } return nil } // Inv: err == nil - if objString := obj.String(); objString != test.wantobj { + if objString := types.ObjectString(obj, (*types.Package).Name); objString != test.wantobj { return fmt.Errorf("Object(%s, %q) = %s, want %s", pkg.Path(), test.path, objString, test.wantobj) } if obj.Pkg() != pkg { @@ -370,8 +382,11 @@ func objectString(obj types.Object) string { // names but in a different source order and checks that objectpath is the // same for methods with the same name. func TestOrdering(t *testing.T) { - pkgs := map[string]map[string]string{ - "p": {"p.go": ` + const src = ` +-- go.mod -- +module x.io + +-- p/p.go -- package p type T struct{ A int } @@ -380,8 +395,8 @@ func (T) M() { } func (T) N() { } func (T) X() { } func (T) Y() { } -`}, - "q": {"q.go": ` + +-- q/q.go -- package q type T struct{ A int } @@ -390,16 +405,11 @@ func (T) N() { } func (T) M() { } func (T) Y() { } func (T) X() { } -`}} - conf := loader.Config{Build: buildutil.FakeContext(pkgs)} - conf.Import("p") - conf.Import("q") - prog, err := conf.Load() - if err != nil { - t.Fatal(err) - } - p := prog.Imported["p"].Pkg - q := prog.Imported["q"].Pkg +` + + pkgmap := loadPackages(t, src, "./p", "./q") + p := pkgmap["x.io/p"].Types + q := pkgmap["x.io/q"].Types // From here, the objectpaths generated for p and q should be the // same. If they are not, then we are generating an ordering that is @@ -427,13 +437,3 @@ func (T) X() { } } } } - -// TODO(adonovan): use go1.21 slices.Contains. -func slicesContains[S ~[]E, E comparable](slice S, x E) bool { - for _, elem := range slice { - if elem == x { - return true - } - } - return false -} diff --git a/go/types/typeutil/callee.go b/go/types/typeutil/callee.go index 90dc541adfe..754380351e8 100644 --- a/go/types/typeutil/callee.go +++ b/go/types/typeutil/callee.go @@ -8,7 +8,6 @@ import ( "go/ast" "go/types" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/typeparams" ) @@ -17,7 +16,7 @@ import ( // // Functions and methods may potentially have type parameters. func Callee(info *types.Info, call *ast.CallExpr) types.Object { - fun := astutil.Unparen(call.Fun) + fun := ast.Unparen(call.Fun) // Look through type instantiation if necessary. isInstance := false diff --git a/go/types/typeutil/map.go b/go/types/typeutil/map.go index a92f80dd2da..8d824f7140f 100644 --- a/go/types/typeutil/map.go +++ b/go/types/typeutil/map.go @@ -12,7 +12,6 @@ import ( "go/types" "reflect" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -260,8 +259,8 @@ func (h Hasher) hashFor(t types.Type) uint32 { case *types.Basic: return uint32(t.Kind()) - case *aliases.Alias: - return h.Hash(aliases.Unalias(t)) + case *types.Alias: + return h.Hash(types.Unalias(t)) case *types.Array: return 9043 + 2*uint32(t.Len()) + 3*h.Hash(t.Elem()) @@ -461,8 +460,8 @@ func (h Hasher) shallowHash(t types.Type) uint32 { // elements (mostly Slice, Pointer, Basic, Named), // so there's no need to optimize anything else. switch t := t.(type) { - case *aliases.Alias: - return h.shallowHash(aliases.Unalias(t)) + case *types.Alias: + return h.shallowHash(types.Unalias(t)) case *types.Signature: var hash uint32 = 604171 diff --git a/go/types/typeutil/methodsetcache.go b/go/types/typeutil/methodsetcache.go index bd71aafaaa1..f7666028fe5 100644 --- a/go/types/typeutil/methodsetcache.go +++ b/go/types/typeutil/methodsetcache.go @@ -9,8 +9,6 @@ package typeutil import ( "go/types" "sync" - - "golang.org/x/tools/internal/aliases" ) // A MethodSetCache records the method set of each type T for which @@ -34,12 +32,12 @@ func (cache *MethodSetCache) MethodSet(T types.Type) *types.MethodSet { cache.mu.Lock() defer cache.mu.Unlock() - switch T := aliases.Unalias(T).(type) { + switch T := types.Unalias(T).(type) { case *types.Named: return cache.lookupNamed(T).value case *types.Pointer: - if N, ok := aliases.Unalias(T.Elem()).(*types.Named); ok { + if N, ok := types.Unalias(T.Elem()).(*types.Named); ok { return cache.lookupNamed(N).pointer } } diff --git a/go/types/typeutil/ui.go b/go/types/typeutil/ui.go index a0c1a60ac02..9dda6a25df7 100644 --- a/go/types/typeutil/ui.go +++ b/go/types/typeutil/ui.go @@ -8,8 +8,6 @@ package typeutil import ( "go/types" - - "golang.org/x/tools/internal/aliases" ) // IntuitiveMethodSet returns the intuitive method set of a type T, @@ -28,7 +26,7 @@ import ( // The order of the result is as for types.MethodSet(T). func IntuitiveMethodSet(T types.Type, msets *MethodSetCache) []*types.Selection { isPointerToConcrete := func(T types.Type) bool { - ptr, ok := aliases.Unalias(T).(*types.Pointer) + ptr, ok := types.Unalias(T).(*types.Pointer) return ok && !types.IsInterface(ptr.Elem()) } diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index f78f1bdf732..ec2b6316374 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -796,42 +796,6 @@ Default: on. Package documentation: [structtag](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/structtag) - -## `stubmethods`: detect missing methods and fix with stub implementations - - -This analyzer detects type-checking errors due to missing methods -in assignments from concrete types to interface types, and offers -a suggested fix that will create a set of stub methods so that -the concrete type satisfies the interface. - -For example, this function will not compile because the value -NegativeErr{} does not implement the "error" interface: - - func sqrt(x float64) (float64, error) { - if x < 0 { - return 0, NegativeErr{} // error: missing method - } - ... - } - - type NegativeErr struct{} - -This analyzer will suggest a fix to declare this method: - - // Error implements error.Error. - func (NegativeErr) Error() string { - panic("unimplemented") - } - -(At least, it appears to behave that way, but technically it -doesn't use the SuggestedFix mechanism and the stub is created by -logic in gopls's golang.stub function.) - -Default: on. - -Package documentation: [stubmethods](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods) - ## `testinggoroutine`: report calls to (*testing.T).Fatal from goroutines started by a test diff --git a/gopls/doc/design/design.md b/gopls/doc/design/design.md index 6e6e7c3bb15..6e03914ee03 100644 --- a/gopls/doc/design/design.md +++ b/gopls/doc/design/design.md @@ -377,7 +377,7 @@ Rename | Rename an identifier Requires | AST and type information for the **reverse** transitive closure LSP | [`textDocument/rename`] | | [`textDocument/prepareRename`] -Previous | [gorename] +Previous | golang.org/x/tools/cmd/gorename | | This uses the same information that find references does, with all the same problems and limitations. It is slightly worse because the changes it suggests make it intolerant of incorrect results. It is also dangerous using it to change the public API of a package. --- @@ -402,7 +402,6 @@ Previous | N/A [gofmt]: https://golang.org/cmd/gofmt [gogetdoc]: https://github.com/zmb3/gogetdoc [goimports]: https://pkg.go.dev/golang.org/x/tools/cmd/goimports -[gorename]: https://pkg.go.dev/golang.org/x/tools/cmd/gorename [goreturns]: https://github.com/sqs/goreturns [gotags]: https://github.com/jstemmer/gotags [guru]: https://pkg.go.dev/golang.org/x/tools/cmd/guru diff --git a/gopls/doc/features/diagnostics.md b/gopls/doc/features/diagnostics.md index f58a6465d1d..b667f69a080 100644 --- a/gopls/doc/features/diagnostics.md +++ b/gopls/doc/features/diagnostics.md @@ -119,6 +119,52 @@ Client support: - **Vim + coc.nvim**: ?? - **CLI**: `gopls check file.go` + + + +### `stubMethods`: Declare missing methods of type + +When a value of a concrete type is assigned to a variable of an +interface type, but the concrete type does not possess all the +necessary methods, the type checker will report a "missing method" +error. + +In this situation, gopls offers a quick fix to add stub declarations +of all the missing methods to the concrete type so that it implements +the interface. + +For example, this function will not compile because the value +`NegativeErr{}` does not implement the "error" interface: + +```go +func sqrt(x float64) (float64, error) { + if x < 0 { + return 0, NegativeErr{} // error: missing method + } + ... +} + +type NegativeErr struct{} +``` + +Gopls will offer a quick fix to declare this method: + +```go + +// Error implements error.Error. +func (NegativeErr) Error() string { + panic("unimplemented") +} +``` + +Beware that the new declarations appear alongside the concrete type, +which may be in a different file or even package from the cursor +position. +(Perhaps gopls should send a `showDocument` request to navigate the +client there, or a progress notification indicating that something +happened.) + - -- **Extract function** replaces one or more complete statements by a +- **`refactor.extract.function`** replaces one or more complete statements by a call to a new function named `newFunction` whose body contains the statements. The selection must enclose fewer statements than the entire body of the existing function. @@ -286,11 +283,11 @@ newly created declaration that contains the selected code: ![Before extracting a function](../assets/extract-function-before.png) ![After extracting a function](../assets/extract-function-after.png) -- **Extract method** is a variant of "Extract function" offered when +- **`refactor.extract.method`** is a variant of "Extract function" offered when the selected statements belong to a method. The newly created function will be a method of the same receiver type. -- **Extract variable** replaces an expression by a reference to a new +- **`refactor.extract.variable`** replaces an expression by a reference to a new local variable named `x` initialized by the expression: ![Before extracting a var](../assets/extract-var-before.png) @@ -330,7 +327,7 @@ The following Extract features are planned for 2024 but not yet supported: -## Extract declarations to new file +## `refactor.extract.toNewFile`: Extract declarations to new file (Available from gopls/v0.17.0) @@ -347,11 +344,11 @@ first token of the declaration, such as `func` or `type`. -## Inline call to function +## `refactor.inline.call`: Inline call to function For a `codeActions` request where the selection is (or is within) a call of a function or method, gopls will return a command of kind -`refactor.inline`, whose effect is to inline the function call. +`refactor.inline.call`, whose effect is to inline the function call. The screenshots below show a call to `sum` before and after inlining: +code actions whose kinds are children of `refactor.rewrite`. -### Remove unused parameter +### `refactor.rewrite.removeUnusedParam`: Remove unused parameter The [`unusedparams` analyzer](../analyzers.md#unusedparams) reports a diagnostic for each parameter that is not used within the function body. @@ -544,7 +538,7 @@ Observe that in the first call, the argument `chargeCreditCard()` was not deleted because of potential side effects, whereas in the second call, the argument 2, a constant, was safely deleted. -### Convert string literal between raw and interpreted +### `refactor.rewrite.changeQuote`: Convert string literal between raw and interpreted When the selection is a string literal, gopls offers a code action to convert the string between raw form (`` `abc` ``) and interpreted @@ -556,7 +550,7 @@ form (`"abc"`) where this is possible: Applying the code action a second time reverts back to the original form. -### Invert 'if' condition +### `refactor.rewrite.invertIf`: Invert 'if' condition When the selection is within an `if`/`else` statement that is not followed by `else if`, gopls offers a code action to invert the @@ -571,7 +565,7 @@ blocks. if the else block ends with a return statement; and thus applying the operation twice does not get you back to where you started. --> -### Split elements into separate lines +### `refactor.rewrite.{split,join}Lines`: Split elements into separate lines When the selection is within a bracketed list of items such as: @@ -619,7 +613,7 @@ comments, which run to the end of the line. -### Fill struct literal +### `refactor.rewrite.fillStruct`: Fill struct literal When the cursor is within a struct literal `S{}`, gopls offers the "Fill S" code action, which populates each missing field of the @@ -651,7 +645,7 @@ Caveats: or in other files in the package, are not considered; see golang/go#68224. -### Fill switch +### `refactor.rewrite.fillSwitch`: Fill switch When the cursor is within a switch statement whose operand type is an _enum_ (a finite set of named constants), or within a type switch, diff --git a/gopls/doc/features/web.md b/gopls/doc/features/web.md index 698cd837f69..46a9f91477b 100644 --- a/gopls/doc/features/web.md +++ b/gopls/doc/features/web.md @@ -49,7 +49,7 @@ your source code has been modified but not saved. like your editor to raise its window when handling this event.) -## Browse package documentation +## `source.doc`: Browse package documentation In any Go source file, a code action request returns a command to "Browse package documentation". This command opens a browser window @@ -75,7 +75,7 @@ Client support: -## Browse free symbols +## `source.freesymbols`: Browse free symbols When studying code, either to understand it or to evaluate a different organization or factoring, it is common to need to know what the @@ -108,7 +108,7 @@ Client support: -## Browse assembly +## `source.assembly`: Browse assembly When you're optimizing the performance of your code or investigating an unexpected crash, it may sometimes be helpful to inspect the diff --git a/gopls/doc/generate/generate.go b/gopls/doc/generate/generate.go index 3fd3e58e6ed..994933a3681 100644 --- a/gopls/doc/generate/generate.go +++ b/gopls/doc/generate/generate.go @@ -23,11 +23,13 @@ import ( "go/ast" "go/token" "go/types" + "maps" "os" "os/exec" "path/filepath" "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -41,7 +43,6 @@ import ( "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/mod" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/maps" "golang.org/x/tools/gopls/internal/util/safetoken" ) @@ -56,14 +57,6 @@ func main() { // - if write, it updates them; // - if !write, it reports whether they would change. func doMain(write bool) (bool, error) { - // TODO(adonovan): when we can rely on go1.23, - // switch to gotypesalias=1 behavior. - // - // (Since this program is run by 'go run', - // the gopls/go.mod file's go 1.19 directive doesn't - // have its usual effect of setting gotypesalias=0.) - os.Setenv("GODEBUG", "gotypesalias=0") - api, err := loadAPI() if err != nil { return false, err @@ -472,9 +465,7 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou // Build list of Lens descriptors. var lenses []*doc.Lens addAll := func(sources map[settings.CodeLensSource]cache.CodeLensSourceFunc, fileType string) error { - slice := maps.Keys(sources) - sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] }) - for _, source := range slice { + for _, source := range slices.Sorted(maps.Keys(sources)) { docText, ok := enumDoc[string(source)] if !ok { return fmt.Errorf("missing CodeLensSource declaration for %s", source) diff --git a/gopls/doc/release/v0.17.0.md b/gopls/doc/release/v0.17.0.md index dba85fef46c..c57522973db 100644 --- a/gopls/doc/release/v0.17.0.md +++ b/gopls/doc/release/v0.17.0.md @@ -6,11 +6,18 @@ The `fieldalignment` analyzer, previously disabled by default, has been removed: it is redundant with the hover size/offset information displayed by v0.16.0 and its diagnostics were confusing. +The kind (identifiers) of all of gopls' code actions have changed +to use more specific hierarchical names. For example, "Inline call" +has changed from `refactor.inline` to `refactor.inline.call`. +This allows clients to request particular code actions more precisely. +The user manual now includes the identifier in the documentation for each code action. # New features ## Extract declarations to new file -Gopls now offers another code action, "Extract declarations to new file", + +Gopls now offers another code action, +"Extract declarations to new file" (`refactor.extract.toNewFile`), which moves selected code sections to a newly created file within the same package. The created filename is chosen as the first {function, type, const, var} name encountered. In addition, import declarations are added or @@ -35,3 +42,15 @@ constructor of the type of each symbol: `interface`, `struct`, `signature`, `pointer`, `array`, `map`, `slice`, `chan`, `string`, `number`, `bool`, and `invalid`. Editors may use this for syntax coloring. +## SignatureHelp for ident and values. + +Now, function signature help can be used on any identifier with a function +signature, not just within the parentheses of a function being called. + +## Jump to assembly definition + +A Definition query on a reference to a function jumps to the +function's Go `func` declaration. If the function is implemented in C +or assembly, the function has no body. Executing a second Definition +query (while already at the Go declaration) will navigate you to the +assembly implementation. diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index d64be6370ef..db6092a980c 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -88,7 +88,7 @@ Default: `["-**/node_modules"]`. ### `templateExtensions []string` -templateExtensions gives the extensions of file names that are treateed +templateExtensions gives the extensions of file names that are treated as template files. (The extension is the part of the file name after the final dot.) diff --git a/gopls/go.mod b/gopls/go.mod index a6128333050..a7bc7404a65 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -9,13 +9,14 @@ require ( github.com/jba/templatecheck v0.7.0 golang.org/x/mod v0.21.0 golang.org/x/sync v0.8.0 - golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98 - golang.org/x/text v0.18.0 + golang.org/x/sys v0.26.0 + golang.org/x/telemetry v0.0.0-20240927184629-19675431963b + golang.org/x/text v0.19.0 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d golang.org/x/vuln v1.0.4 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.4.7 - mvdan.cc/gofumpt v0.6.0 + mvdan.cc/gofumpt v0.7.0 mvdan.cc/xurls/v2 v2.5.0 ) @@ -23,7 +24,6 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/google/safehtml v0.1.0 // indirect golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect - golang.org/x/sys v0.25.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/gopls/go.sum b/gopls/go.sum index c380cc75d5d..2b92ae83594 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -1,7 +1,7 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= @@ -16,7 +16,7 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -25,7 +25,7 @@ golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -33,19 +33,19 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= -golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98 h1:Wm3cG5X6sZ0RSVRc/H1/sciC4AT6HAKgLCSH2lbpR/c= -golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98/go.mod h1:m7R/r+o5h7UvF2JD9n2iLSGY4v8v+zNSyTJ6xynLrqs= +golang.org/x/telemetry v0.0.0-20240927184629-19675431963b h1:PfPrmVDHfPgLVpiYnf2R1uL8SCXBjkqT51+f/fQHR6Q= +golang.org/x/telemetry v0.0.0-20240927184629-19675431963b/go.mod h1:PsFMgI0jiuY7j+qwXANuh9a/x5kQESTSnRow3gapUyk= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -55,7 +55,7 @@ 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.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= -mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= +mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= +mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff --git a/gopls/internal/analysis/embeddirective/embeddirective.go b/gopls/internal/analysis/embeddirective/embeddirective.go index 1b0b89711c2..e623587cc68 100644 --- a/gopls/internal/analysis/embeddirective/embeddirective.go +++ b/gopls/internal/analysis/embeddirective/embeddirective.go @@ -12,7 +12,6 @@ import ( "strings" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/analysisinternal" ) @@ -148,7 +147,7 @@ func embeddableType(o types.Object) bool { // For embed.FS the underlying type is an implementation detail. // As long as the named type resolves to embed.FS, it is OK. - if named, ok := aliases.Unalias(o.Type()).(*types.Named); ok { + if named, ok := types.Unalias(o.Type()).(*types.Named); ok { obj := named.Obj() if obj.Pkg() != nil && obj.Pkg().Path() == "embed" && obj.Name() == "FS" { return true diff --git a/gopls/internal/analysis/fillreturns/fillreturns_test.go b/gopls/internal/analysis/fillreturns/fillreturns_test.go index 45c7846a7dc..f7667660bf7 100644 --- a/gopls/internal/analysis/fillreturns/fillreturns_test.go +++ b/gopls/internal/analysis/fillreturns/fillreturns_test.go @@ -9,17 +9,9 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/gopls/internal/analysis/fillreturns" - "golang.org/x/tools/internal/aliases" ) func Test(t *testing.T) { - // TODO(golang/go#65294): update expectations and delete this - // check once we update go.mod to go1.23 so that - // gotypesalias=1 becomes the only supported mode. - if aliases.Enabled() { - t.Skip("expectations need updating for materialized aliases") - } - testdata := analysistest.TestData() analysistest.RunWithSuggestedFixes(t, testdata, fillreturns.Analyzer, "a", "typeparams") } diff --git a/gopls/internal/analysis/fillreturns/testdata/src/a/a.go.golden b/gopls/internal/analysis/fillreturns/testdata/src/a/a.go.golden index f007a5f374b..27353f5fbab 100644 --- a/gopls/internal/analysis/fillreturns/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/fillreturns/testdata/src/a/a.go.golden @@ -71,7 +71,7 @@ func complex() (*int, []int, [2]int, map[int]int) { } func structsAndInterfaces() (T, url.URL, T1, I, I1, io.Reader, Client, ast2.Stmt) { - return T{}, url.URL{}, T{}, nil, nil, nil, Client{}, nil // want "return values" + return T{}, url.URL{}, T1{}, nil, nil, nil, Client{}, nil // want "return values" } func m() (int, error) { diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go index b42b8232f22..629dfdfc797 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/analysis/fillstruct/fillstruct.go @@ -26,7 +26,6 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/fuzzy" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" @@ -453,7 +452,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { } case *types.Pointer: - switch aliases.Unalias(u.Elem()).(type) { + switch types.Unalias(u.Elem()).(type) { case *types.Basic: return &ast.CallExpr{ Fun: &ast.Ident{ @@ -477,7 +476,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { } case *types.Interface: - if param, ok := aliases.Unalias(typ).(*types.TypeParam); ok { + if param, ok := types.Unalias(typ).(*types.TypeParam); ok { // *new(T) is the zero value of a type parameter T. // TODO(adonovan): one could give a more specific zero // value if the type has a core type that is, say, diff --git a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go index 1bdce1d658c..6511477d254 100644 --- a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go +++ b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go @@ -19,7 +19,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/internal/analysisinternal" ) @@ -38,7 +37,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { - if astutil.IsGenerated(file) { + if ast.IsGenerated(file) { generated[pass.Fset.File(file.Pos())] = true } } diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange.go b/gopls/internal/analysis/simplifyrange/simplifyrange.go index ce9d450582b..4071d1b6e8a 100644 --- a/gopls/internal/analysis/simplifyrange/simplifyrange.go +++ b/gopls/internal/analysis/simplifyrange/simplifyrange.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/internal/analysisinternal" ) @@ -33,7 +32,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { - if astutil.IsGenerated(file) { + if ast.IsGenerated(file) { generated[pass.Fset.File(file.Pos())] = true } } diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go index 973144c30e8..50a600e03bf 100644 --- a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go +++ b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go @@ -6,11 +6,11 @@ package simplifyrange_test import ( "go/build" + "slices" "testing" "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/gopls/internal/analysis/simplifyrange" - "golang.org/x/tools/gopls/internal/util/slices" ) func Test(t *testing.T) { diff --git a/gopls/internal/analysis/simplifyslice/simplifyslice.go b/gopls/internal/analysis/simplifyslice/simplifyslice.go index 343fca8b185..dc99580b07e 100644 --- a/gopls/internal/analysis/simplifyslice/simplifyslice.go +++ b/gopls/internal/analysis/simplifyslice/simplifyslice.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/internal/analysisinternal" ) @@ -42,7 +41,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { - if astutil.IsGenerated(file) { + if ast.IsGenerated(file) { generated[pass.Fset.File(file.Pos())] = true } } diff --git a/gopls/internal/analysis/stubmethods/doc.go b/gopls/internal/analysis/stubmethods/doc.go deleted file mode 100644 index e1383cfc7e7..00000000000 --- a/gopls/internal/analysis/stubmethods/doc.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package stubmethods defines a code action for missing interface methods. -// -// # Analyzer stubmethods -// -// stubmethods: detect missing methods and fix with stub implementations -// -// This analyzer detects type-checking errors due to missing methods -// in assignments from concrete types to interface types, and offers -// a suggested fix that will create a set of stub methods so that -// the concrete type satisfies the interface. -// -// For example, this function will not compile because the value -// NegativeErr{} does not implement the "error" interface: -// -// func sqrt(x float64) (float64, error) { -// if x < 0 { -// return 0, NegativeErr{} // error: missing method -// } -// ... -// } -// -// type NegativeErr struct{} -// -// This analyzer will suggest a fix to declare this method: -// -// // Error implements error.Error. -// func (NegativeErr) Error() string { -// panic("unimplemented") -// } -// -// (At least, it appears to behave that way, but technically it -// doesn't use the SuggestedFix mechanism and the stub is created by -// logic in gopls's golang.stub function.) -package stubmethods diff --git a/gopls/internal/analysis/stubmethods/stubmethods_test.go b/gopls/internal/analysis/stubmethods/stubmethods_test.go deleted file mode 100644 index 9c744c9b7a3..00000000000 --- a/gopls/internal/analysis/stubmethods/stubmethods_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package stubmethods_test - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/gopls/internal/analysis/stubmethods" -) - -func Test(t *testing.T) { - testdata := analysistest.TestData() - analysistest.Run(t, testdata, stubmethods.Analyzer, "typeparams") -} diff --git a/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go b/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go deleted file mode 100644 index 7b6f2911ea9..00000000000 --- a/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package stubmethods - -var _ I = Y{} // want "does not implement I" - -type I interface{ F() } - -type X struct{} - -func (X) F(string) {} - -type Y struct{ X } diff --git a/gopls/internal/analysis/undeclaredname/undeclared.go b/gopls/internal/analysis/undeclaredname/undeclared.go index afd9b652b97..70b22881700 100644 --- a/gopls/internal/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/analysis/undeclaredname/undeclared.go @@ -18,7 +18,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/analysisinternal" ) @@ -318,7 +317,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, func typeToArgName(ty types.Type) string { s := types.Default(ty).String() - switch t := aliases.Unalias(ty).(type) { + switch t := types.Unalias(ty).(type) { case *types.Basic: // use first letter in type name for basic types return s[0:1] diff --git a/gopls/internal/analysis/unusedparams/unusedparams.go b/gopls/internal/analysis/unusedparams/unusedparams.go index df54293b37f..ca808a740d3 100644 --- a/gopls/internal/analysis/unusedparams/unusedparams.go +++ b/gopls/internal/analysis/unusedparams/unusedparams.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/slices" + "golang.org/x/tools/gopls/internal/util/moreslices" "golang.org/x/tools/internal/analysisinternal" ) @@ -194,7 +194,7 @@ func run(pass *analysis.Pass) (any, error) { // Edge case: f = func() {...} // should not count as a use. if pass.TypesInfo.Uses[id] != nil { - usesOutsideCall[fn] = slices.Remove(usesOutsideCall[fn], id) + usesOutsideCall[fn] = moreslices.Remove(usesOutsideCall[fn], id) } if fn == nil && id.Name == "_" { diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index 4730830cb4f..9debc609048 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go @@ -24,6 +24,7 @@ import ( "reflect" "runtime" "runtime/debug" + "slices" "sort" "strings" "sync" @@ -43,8 +44,7 @@ import ( "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/frob" - "golang.org/x/tools/gopls/internal/util/maps" - "golang.org/x/tools/gopls/internal/util/slices" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/facts" @@ -539,6 +539,9 @@ type analysisNode struct { typesOnce sync.Once // guards lazy population of types and typesErr fields types *types.Package // type information lazily imported from summary typesErr error // an error producing type information + + depHashOnce sync.Once + _depHash file.Hash // memoized hash of data affecting dependents } func (an *analysisNode) String() string { return string(an.mp.ID) } @@ -597,6 +600,40 @@ func (an *analysisNode) _import() (*types.Package, error) { return an.types, an.typesErr } +// depHash computes the hash of node information that may affect other nodes +// depending on this node: the package path, export hash, and action results. +// +// The result is memoized to avoid redundant work when analysing multiple +// dependents. +func (an *analysisNode) depHash() file.Hash { + an.depHashOnce.Do(func() { + hasher := sha256.New() + fmt.Fprintf(hasher, "dep: %s\n", an.mp.PkgPath) + fmt.Fprintf(hasher, "export: %s\n", an.summary.DeepExportHash) + + // action results: errors and facts + actions := an.summary.Actions + names := make([]string, 0, len(actions)) + for name := range actions { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + summary := actions[name] + fmt.Fprintf(hasher, "action %s\n", name) + if summary.Err != "" { + fmt.Fprintf(hasher, "error %s\n", summary.Err) + } else { + fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash) + // We can safely omit summary.diagnostics + // from the key since they have no downstream effect. + } + } + hasher.Sum(an._depHash[:0]) + }) + return an._depHash +} + // analyzeSummary is a gob-serializable summary of successfully // applying a list of analyzers to a package. type analyzeSummary struct { @@ -769,32 +806,9 @@ func (an *analysisNode) cacheKey() [sha256.Size]byte { } // vdeps, in PackageID order - depIDs := maps.Keys(an.succs) - // TODO(adonovan): use go1.2x slices.Sort(depIDs). - sort.Slice(depIDs, func(i, j int) bool { return depIDs[i] < depIDs[j] }) - for _, depID := range depIDs { - vdep := an.succs[depID] - fmt.Fprintf(hasher, "dep: %s\n", vdep.mp.PkgPath) - fmt.Fprintf(hasher, "export: %s\n", vdep.summary.DeepExportHash) - - // action results: errors and facts - actions := vdep.summary.Actions - names := make([]string, 0, len(actions)) - for name := range actions { - names = append(names, name) - } - sort.Strings(names) - for _, name := range names { - summary := actions[name] - fmt.Fprintf(hasher, "action %s\n", name) - if summary.Err != "" { - fmt.Fprintf(hasher, "error %s\n", summary.Err) - } else { - fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash) - // We can safely omit summary.diagnostics - // from the key since they have no downstream effect. - } - } + for _, vdep := range moremaps.Sorted(an.succs) { + hash := vdep.depHash() + hasher.Write(hash[:]) } var hash [sha256.Size]byte diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index d9c75100443..08d57f4e657 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -15,6 +15,7 @@ import ( "go/types" "regexp" "runtime" + "slices" "sort" "strings" "sync" @@ -32,7 +33,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gcimporter" @@ -50,22 +50,50 @@ const ( type unit = struct{} // A typeCheckBatch holds data for a logical type-checking operation, which may -// type-check many unrelated packages. +// type check many unrelated packages. // // It shares state such as parsed files and imports, to optimize type-checking // for packages with overlapping dependency graphs. type typeCheckBatch struct { - syntaxIndex map[PackageID]int // requested ID -> index in ids - pre preTypeCheck - post postTypeCheck - handles map[PackageID]*packageHandle // (immutable) - parseCache *parseCache - fset *token.FileSet // describes all parsed or imported files - cpulimit chan unit // concurrency limiter for CPU-bound operations - - mu sync.Mutex - syntaxPackages map[PackageID]*futurePackage // results of processing a requested package; may hold (nil, nil) - importPackages map[PackageID]*futurePackage // package results to use for importing + // handleMu guards _handles, which must only be accessed via addHandles or + // getHandle. + // + // TODO(rfindley): refactor such that we can simply prepare the type checking + // pass by ensuring that handles are present on the Snapshot, and access them + // directly, rather than copying maps for each caller. + handleMu sync.Mutex + _handles map[PackageID]*packageHandle + + parseCache *parseCache + fset *token.FileSet // describes all parsed or imported files + cpulimit chan unit // concurrency limiter for CPU-bound operations + syntaxPackages *futureCache[PackageID, *Package] // transient cache of in-progress syntax futures + importPackages *futureCache[PackageID, *types.Package] // persistent cache of imports +} + +// addHandles is called by each goroutine joining the type check batch, to +// ensure that the batch has all inputs necessary for type checking. +func (b *typeCheckBatch) addHandles(handles map[PackageID]*packageHandle) { + b.handleMu.Lock() + defer b.handleMu.Unlock() + for id, ph := range handles { + assert(ph.state == validKey, "invalid handle") + if alt, ok := b._handles[id]; ok { + // Once handles have been reevaluated, they should not change. Therefore, + // we should only ever encounter exactly one handle instance for a given + // ID. + assert(alt == ph, "mismatching handle") + } else { + b._handles[id] = ph + } + } +} + +// getHandle retrieves the packageHandle for the given id. +func (b *typeCheckBatch) getHandle(id PackageID) *packageHandle { + b.handleMu.Lock() + defer b.handleMu.Unlock() + return b._handles[id] } // A futurePackage is a future result of type checking or importing a package, @@ -102,30 +130,10 @@ type pkgOrErr struct { // of the potentially type-checking methods below. func (s *Snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]*Package, error) { pkgs := make([]*Package, len(ids)) - - var ( - needIDs []PackageID // ids to type-check - indexes []int // original index of requested ids - ) - - // Check for existing active packages, as any package will do. - // - // This is also done inside forEachPackage, but doing it here avoids - // unnecessary set up for type checking (e.g. assembling the package handle - // graph). - for i, id := range ids { - if pkg := s.getActivePackage(id); pkg != nil { - pkgs[i] = pkg - } else { - needIDs = append(needIDs, id) - indexes = append(indexes, i) - } - } - post := func(i int, pkg *Package) { - pkgs[indexes[i]] = pkg + pkgs[i] = pkg } - return pkgs, s.forEachPackage(ctx, needIDs, nil, post) + return pkgs, s.forEachPackage(ctx, ids, nil, post) } // getImportGraph returns a shared import graph use for this snapshot, or nil. @@ -195,6 +203,10 @@ func (s *Snapshot) getImportGraph(ctx context.Context) *importGraph { // import graph. // // resolveImportGraph should only be called from getImportGraph. +// +// TODO(rfindley): resolveImportGraph can be eliminated (greatly simplifying +// things) by instead holding on to imports of open packages after each type +// checking pass. func (s *Snapshot) resolveImportGraph() (*importGraph, error) { ctx := s.backgroundCtx ctx, done := event.Start(event.Detach(ctx), "cache.resolveImportGraph") @@ -202,6 +214,7 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { s.mu.Lock() lastImportGraph := s.importGraph + g := s.meta s.mu.Unlock() openPackages := make(map[PackageID]bool) @@ -213,7 +226,6 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { // In the past, a call to MetadataForFile here caused a bunch of // unnecessary loads in multi-root workspaces (and as a result, spurious // diagnostics). - g := s.MetadataGraph() var mps []*metadata.Package for _, id := range g.IDs[fh.URI()] { mps = append(mps, g.Packages[id]) @@ -229,11 +241,6 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { openPackageIDs = append(openPackageIDs, id) } - handles, err := s.getPackageHandles(ctx, openPackageIDs) - if err != nil { - return nil, err - } - // Subtlety: we erase the upward cone of open packages from the shared import // graph, to increase reusability. // @@ -249,43 +256,51 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { // reachability. // // TODO(rfindley): this logic could use a unit test. - volatileDeps := make(map[PackageID]bool) - var isVolatile func(*packageHandle) bool - isVolatile = func(ph *packageHandle) (volatile bool) { - if v, ok := volatileDeps[ph.mp.ID]; ok { - return v - } - defer func() { - volatileDeps[ph.mp.ID] = volatile - }() - if openPackages[ph.mp.ID] { - return true - } - for _, dep := range ph.mp.DepsByPkgPath { - if isVolatile(handles[dep]) { - return true + volatile := make(map[PackageID]bool) + var isVolatile func(PackageID) bool + isVolatile = func(id PackageID) (v bool) { + v, ok := volatile[id] + if !ok { + volatile[id] = false // defensive: break cycles + for _, dep := range g.Packages[id].DepsByPkgPath { + if isVolatile(dep) { + v = true + // Keep going, to ensure that we traverse all dependencies. + } + } + if openPackages[id] { + v = true } + volatile[id] = v } - return false + return v } - for _, dep := range handles { - isVolatile(dep) + for _, id := range openPackageIDs { + if ctx.Err() != nil { + return nil, ctx.Err() + } + _ = isVolatile(id) // populate volatile map } - for id, volatile := range volatileDeps { - if volatile { - delete(handles, id) + + var ids []PackageID + for id, v := range volatile { + if !v { + ids = append(ids, id) } } + handles, err := s.getPackageHandles(ctx, ids) + if err != nil { + return nil, err + } + // We reuse the last import graph if and only if none of the dependencies // have changed. Doing better would involve analyzing dependencies to find // subgraphs that are still valid. Not worth it, especially when in the // common case nothing has changed. - unchanged := lastImportGraph != nil && len(handles) == len(lastImportGraph.depKeys) - var ids []PackageID + unchanged := lastImportGraph != nil && len(ids) == len(lastImportGraph.depKeys) depKeys := make(map[PackageID]file.Hash) for id, ph := range handles { - ids = append(ids, id) depKeys[id] = ph.key if unchanged { prevKey, ok := lastImportGraph.depKeys[id] @@ -297,8 +312,8 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { return lastImportGraph, nil } - b, err := s.forEachPackageInternal(ctx, nil, ids, nil, nil, nil, handles) - if err != nil { + b := newTypeCheckBatch(s.view.parseCache, nil) + if err := b.query(ctx, ids, nil, nil, nil, handles); err != nil { return nil, err } @@ -307,11 +322,11 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { depKeys: depKeys, imports: make(map[PackageID]pkgOrErr), } - for id, fut := range b.importPackages { - if fut.v.pkg == nil && fut.v.err == nil { + for id, fut := range b.importPackages.cache { + if fut.v == nil && fut.err == nil { panic(fmt.Sprintf("internal error: import node %s is not evaluated", id)) } - next.imports[id] = fut.v + next.imports[id] = pkgOrErr{fut.v, fut.err} } return next, nil } @@ -385,31 +400,55 @@ func (s *Snapshot) forEachPackage(ctx context.Context, ids []PackageID, pre preT post(indexes[i], pkg) } + b, release := s.acquireTypeChecking(ctx) + defer release() + handles, err := s.getPackageHandles(ctx, needIDs) if err != nil { return err } + return b.query(ctx, nil, needIDs, pre2, post2, handles) +} + +// acquireTypeChecking joins or starts a concurrent type checking batch. +// +// The batch may be queried for package information using [typeCheckBatch.query]. +// The second result must be called when the batch is no longer needed, to +// release the resource. +func (s *Snapshot) acquireTypeChecking(ctx context.Context) (*typeCheckBatch, func()) { + s.typeCheckMu.Lock() + defer s.typeCheckMu.Unlock() + + if s.batch == nil { + assert(s.batchRef == 0, "miscounted type checking") + impGraph := s.getImportGraph(ctx) + s.batch = newTypeCheckBatch(s.view.parseCache, impGraph) + } + s.batchRef++ - impGraph := s.getImportGraph(ctx) - _, err = s.forEachPackageInternal(ctx, impGraph, nil, needIDs, pre2, post2, handles) - return err + return s.batch, func() { + s.typeCheckMu.Lock() + defer s.typeCheckMu.Unlock() + assert(s.batchRef > 0, "miscounted type checking 2") + s.batchRef-- + if s.batchRef == 0 { + s.batch = nil + } + } } -// forEachPackageInternal is used by both forEachPackage and loadImportGraph to -// type-check a graph of packages. +// newTypeCheckBatch creates a new type checking batch using the provided +// shared parseCache. // // If a non-nil importGraph is provided, imports in this graph will be reused. -func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *importGraph, importIDs, syntaxIDs []PackageID, pre preTypeCheck, post postTypeCheck, handles map[PackageID]*packageHandle) (*typeCheckBatch, error) { +func newTypeCheckBatch(parseCache *parseCache, importGraph *importGraph) *typeCheckBatch { b := &typeCheckBatch{ - pre: pre, - post: post, - handles: handles, - parseCache: s.view.parseCache, + _handles: make(map[PackageID]*packageHandle), + parseCache: parseCache, fset: fileSetWithBase(reservedForParsing), - syntaxIndex: make(map[PackageID]int), cpulimit: make(chan unit, runtime.GOMAXPROCS(0)), - syntaxPackages: make(map[PackageID]*futurePackage), - importPackages: make(map[PackageID]*futurePackage), + syntaxPackages: newFutureCache[PackageID, *Package](false), // don't persist syntax packages + importPackages: newFutureCache[PackageID, *types.Package](true), // ...but DO persist imports } if importGraph != nil { @@ -419,15 +458,28 @@ func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *impo done := make(chan unit) close(done) for id, res := range importGraph.imports { - b.importPackages[id] = &futurePackage{done, res} + b.importPackages.cache[id] = &future[*types.Package]{done: done, v: res.pkg, err: res.err} } } else { b.fset = fileSetWithBase(reservedForParsing) } + return b +} - for i, id := range syntaxIDs { - b.syntaxIndex[id] = i - } +// query executes a traversal of package information in the given typeCheckBatch. +// For each package in importIDs, the package will be loaded "for import" (sans +// syntax). +// +// For each package in syntaxIDs, the package will be handled following the +// pre- and post- traversal logic of [Snapshot.forEachPackage]. +// +// Package handles must be provided for each package in the forward transitive +// closure of either importIDs or syntaxIDs. +// +// TODO(rfindley): simplify this API by clarifying shared import graph and +// package handle logic. +func (b *typeCheckBatch) query(ctx context.Context, importIDs, syntaxIDs []PackageID, pre preTypeCheck, post postTypeCheck, handles map[PackageID]*packageHandle) error { + b.addHandles(handles) // Start a single goroutine for each requested package. // @@ -435,21 +487,23 @@ func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *impo // are not needed. var g errgroup.Group for _, id := range importIDs { - id := id g.Go(func() error { + if ctx.Err() != nil { + return ctx.Err() + } _, err := b.getImportPackage(ctx, id) return err }) } for i, id := range syntaxIDs { - i := i - id := id g.Go(func() error { - _, err := b.handleSyntaxPackage(ctx, i, id) - return err + if ctx.Err() != nil { + return ctx.Err() + } + return b.handleSyntaxPackage(ctx, i, id, pre, post) }) } - return b, g.Wait() + return g.Wait() } // TODO(rfindley): re-order the declarations below to read better from top-to-bottom. @@ -461,62 +515,31 @@ func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *impo // where id is in the set of requested IDs), a package loaded from export data, // or a package type-checked for import only. func (b *typeCheckBatch) getImportPackage(ctx context.Context, id PackageID) (pkg *types.Package, err error) { - b.mu.Lock() - f, ok := b.importPackages[id] - if ok { - b.mu.Unlock() + return b.importPackages.get(ctx, id, func(ctx context.Context) (*types.Package, error) { + ph := b.getHandle(id) - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-f.done: - return f.v.pkg, f.v.err + // "unsafe" cannot be imported or type-checked. + // + // We check PkgPath, not id, as the structure of the ID + // depends on the build system (in particular, + // Bazel+gopackagesdriver appears to use something other than + // "unsafe", though we aren't sure what; even 'go list' can + // use "p [q.test]" for testing or if PGO is enabled. + // See golang/go#60890. + if ph.mp.PkgPath == "unsafe" { + return types.Unsafe, nil } - } - - f = &futurePackage{done: make(chan unit)} - b.importPackages[id] = f - b.mu.Unlock() - defer func() { - f.v = pkgOrErr{pkg, err} - close(f.done) - }() - - if index, ok := b.syntaxIndex[id]; ok { - pkg, err := b.handleSyntaxPackage(ctx, index, id) - if err != nil { - return nil, err + data, err := filecache.Get(exportDataKind, ph.key) + if err == filecache.ErrNotFound { + // No cached export data: type-check as fast as possible. + return b.checkPackageForImport(ctx, ph) } - if pkg != nil { - return pkg, nil + if err != nil { + return nil, fmt.Errorf("failed to read cache data for %s: %v", ph.mp.ID, err) } - // type-checking was short-circuited by the pre- func. - } - - ph := b.handles[id] - - // "unsafe" cannot be imported or type-checked. - // - // We check PkgPath, not id, as the structure of the ID - // depends on the build system (in particular, - // Bazel+gopackagesdriver appears to use something other than - // "unsafe", though we aren't sure what; even 'go list' can - // use "p [q.test]" for testing or if PGO is enabled. - // See golang/go#60890. - if ph.mp.PkgPath == "unsafe" { - return types.Unsafe, nil - } - - data, err := filecache.Get(exportDataKind, ph.key) - if err == filecache.ErrNotFound { - // No cached export data: type-check as fast as possible. - return b.checkPackageForImport(ctx, ph) - } - if err != nil { - return nil, fmt.Errorf("failed to read cache data for %s: %v", ph.mp.ID, err) - } - return b.importPackage(ctx, ph.mp, data) + return b.importPackage(ctx, ph.mp, data) + }) } // handleSyntaxPackage handles one package from the ids slice. @@ -525,73 +548,61 @@ func (b *typeCheckBatch) getImportPackage(ctx context.Context, id PackageID) (pk // resulting types.Package so that it may be used for importing. // // handleSyntaxPackage returns (nil, nil) if pre returned false. -func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id PackageID) (pkg *types.Package, err error) { - b.mu.Lock() - f, ok := b.syntaxPackages[id] - if ok { - b.mu.Unlock() - <-f.done - return f.v.pkg, f.v.err - } - - f = &futurePackage{done: make(chan unit)} - b.syntaxPackages[id] = f - b.mu.Unlock() - defer func() { - f.v = pkgOrErr{pkg, err} - close(f.done) - }() - - ph := b.handles[id] - if b.pre != nil && !b.pre(i, ph) { - return nil, nil // skip: export data only - } - - // Wait for predecessors. - { - var g errgroup.Group - for _, depID := range ph.mp.DepsByPkgPath { - depID := depID - g.Go(func() error { - _, err := b.getImportPackage(ctx, depID) - return err - }) - } - if err := g.Wait(); err != nil { - // Failure to import a package should not abort the whole operation. - // Stop only if the context was cancelled, a likely cause. - // Import errors will be reported as type diagnostics. - if ctx.Err() != nil { - return nil, ctx.Err() +func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id PackageID, pre preTypeCheck, post postTypeCheck) error { + ph := b.getHandle(id) + if pre != nil && !pre(i, ph) { + return nil // skip: not needed + } + + pkg, err := b.syntaxPackages.get(ctx, id, func(ctx context.Context) (*Package, error) { + // Wait for predecessors. + { + var g errgroup.Group + for _, depID := range ph.mp.DepsByPkgPath { + g.Go(func() error { + _, err := b.getImportPackage(ctx, depID) + return err + }) + } + if err := g.Wait(); err != nil { + // Failure to import a package should not abort the whole operation. + // Stop only if the context was cancelled, a likely cause. + // Import errors will be reported as type diagnostics. + if ctx.Err() != nil { + return nil, ctx.Err() + } } } - } - // Wait to acquire a CPU token. - // - // Note: it is important to acquire this token only after awaiting - // predecessors, to avoid starvation. - select { - case <-ctx.Done(): - return nil, ctx.Err() - case b.cpulimit <- unit{}: - defer func() { - <-b.cpulimit // release CPU token - }() - } + // Wait to acquire a CPU token. + // + // Note: it is important to acquire this token only after awaiting + // predecessors, to avoid starvation. + select { + case <-ctx.Done(): + return nil, ctx.Err() + case b.cpulimit <- unit{}: + defer func() { + <-b.cpulimit // release CPU token + }() + } + + // Compute the syntax package. + p, err := b.checkPackage(ctx, ph) + if err != nil { + return nil, err // e.g. I/O error, cancelled + } - // Compute the syntax package. - p, err := b.checkPackage(ctx, ph) + // Update caches. + go storePackageResults(ctx, ph, p) // ...and write all packages to disk + return p, nil + }) if err != nil { - return nil, err + return err } - // Update caches. - go storePackageResults(ctx, ph, p) // ...and write all packages to disk - - b.post(i, p) - - return p.pkg.types, nil + post(i, pkg) + return nil } // storePackageResults serializes and writes information derived from p to the @@ -601,6 +612,7 @@ func storePackageResults(ctx context.Context, ph *packageHandle, p *Package) { toCache := map[string][]byte{ xrefsKind: p.pkg.xrefs(), methodSetsKind: p.pkg.methodsets().Encode(), + testsKind: p.pkg.tests().Encode(), diagnosticsKind: encodeDiagnostics(p.pkg.diagnostics), } @@ -626,7 +638,7 @@ func (b *typeCheckBatch) importPackage(ctx context.Context, mp *metadata.Package ctx, done := event.Start(ctx, "cache.typeCheckBatch.importPackage", label.Package.Of(string(mp.ID))) defer done() - impMap := b.importMap(mp.ID) + importLookup := b.importLookup(mp) thisPackage := types.NewPackage(string(mp.PkgPath), string(mp.Name)) getPackages := func(items []gcimporter.GetPackagesItem) error { @@ -647,7 +659,7 @@ func (b *typeCheckBatch) importPackage(ctx context.Context, mp *metadata.Package pkg.Name(), item.Name, id, item.Path) } } else { - id = impMap[item.Path] + id = importLookup(PackagePath(item.Path)) var err error pkg, err = b.getImportPackage(ctx, id) if err != nil { @@ -760,64 +772,123 @@ func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageH return pkg, nil } -// importMap returns the map of package path -> package ID relative to the -// specified ID. -func (b *typeCheckBatch) importMap(id PackageID) map[string]PackageID { - impMap := make(map[string]PackageID) - var populateDeps func(*metadata.Package) - populateDeps = func(parent *metadata.Package) { - for _, id := range parent.DepsByPkgPath { - mp := b.handles[id].mp - if prevID, ok := impMap[string(mp.PkgPath)]; ok { +// importLookup returns a function that may be used to look up a package ID for +// a given package path, based on the forward transitive closure of the initial +// package (id). +// +// The resulting function is not concurrency safe. +func (b *typeCheckBatch) importLookup(mp *metadata.Package) func(PackagePath) PackageID { + // This function implements an incremental depth first scan through the + // package imports. Previous implementations of import mapping built the + // entire PackagePath->PackageID mapping eagerly, but that resulted in a + // large amount of unnecessary work: most imports are either directly + // imported, or found through a shallow scan. + + // impMap memoizes the lookup of package paths. + impMap := map[PackagePath]PackageID{ + mp.PkgPath: mp.ID, + } + // pending is a FIFO queue of package metadata that has yet to have its + // dependencies fully scanned. + // Invariant: all entries in pending are already mapped in impMap. + pending := []*metadata.Package{mp} + + // search scans children the next package in pending, looking for pkgPath. + // Invariant: whenever search is called, pkgPath is not yet mapped. + var search func(pkgPath PackagePath) (PackageID, bool) + search = func(pkgPath PackagePath) (id PackageID, found bool) { + pkg := pending[0] + pending = pending[1:] + for depPath, depID := range pkg.DepsByPkgPath { + if prevID, ok := impMap[depPath]; ok { // debugging #63822 - if prevID != mp.ID { + if prevID != depID { bug.Reportf("inconsistent view of dependencies") } continue } - impMap[string(mp.PkgPath)] = mp.ID - populateDeps(mp) + impMap[depPath] = depID + // TODO(rfindley): express this as an operation on the import graph + // itself, rather than the set of package handles. + pending = append(pending, b.getHandle(depID).mp) + if depPath == pkgPath { + // Don't return early; finish processing pkg's deps. + id = depID + found = true + } + } + return id, found + } + + return func(pkgPath PackagePath) PackageID { + if id, ok := impMap[pkgPath]; ok { + return id + } + for len(pending) > 0 { + if id, found := search(pkgPath); found { + return id + } } + return "" } - mp := b.handles[id].mp - populateDeps(mp) - return impMap } -// A packageHandle holds inputs required to compute a Package, including -// metadata, derived diagnostics, files, and settings. Additionally, -// packageHandles manage a key for these inputs, to use in looking up -// precomputed results. +// A packageState is the state of a [packageHandle]; see below for details. +type packageState uint8 + +const ( + validMetadata packageState = iota // the package has valid metadata (initial state) + validLocalData // local package files have been analyzed + validKey // dependencies have been analyzed, and key produced +) + +// A packageHandle holds information derived from a metadata.Package, and +// records its degree of validity as state changes occur: successful analysis +// causes the state to progress; invalidation due to changes causes it to +// regress. +// +// In the initial state (validMetadata), all we know is the metadata for the +// package itself. This is the lowest state, and it cannot become invalid +// because the metadata for a given snapshot never changes. (Each handle is +// implicitly associated with a Snapshot.) // -// packageHandles may be invalid following an invalidation via snapshot.clone, -// but the handles returned by getPackageHandles will always be valid. +// After the files of the package have been read (validLocalData), we can +// perform computations that are local to that package, such as parsing, or +// building the symbol reference graph (SRG). This information is invalidated +// by a change to any file in the package. The local information is thus +// sufficient to form a cache key for saved parsed trees or the SRG. // -// packageHandles are critical for implementing "precise pruning" in gopls: -// packageHandle.key is a hash of a precise set of inputs, such as package -// files and "reachable" syntax, that may affect type checking. +// Once all dependencies have been analyzed (validKey), we can type-check the +// package. This information is invalidated by any change to the package +// itself, or to any dependency that is transitively reachable through the SRG. +// The cache key for saved type information must thus incorporate information +// from all reachable dependencies. This reachability analysis implements what +// we sometimes refer to as "precise pruning", or fine-grained invalidation: +// https://go.dev/blog/gopls-scalability#invalidation // -// packageHandles also keep track of state that allows gopls to compute, and -// then quickly recompute, these keys. This state is split into two categories: -// - local state, which depends only on the package's local files and metadata -// - other state, which includes data derived from dependencies. +// Following a change, the packageHandle is cloned in the new snapshot with a +// new state set to its least known valid state, as described above: if package +// files changed, it is reset to validMetadata; if dependencies changed, it is +// reset to validLocalData. However, the derived data from its previous state +// is not yet removed, as keys may not have changed after they are reevaluated, +// in which case we can avoid recomputing the derived data. // -// Dividing the data in this way allows gopls to minimize invalidation when a -// package is modified. For example, any change to a package file fully -// invalidates the package handle. On the other hand, if that change was not -// metadata-affecting it may be the case that packages indirectly depending on -// the modified package are unaffected by the change. For that reason, we have -// two types of invalidation, corresponding to the two types of data above: -// - deletion of the handle, which occurs when the package itself changes -// - clearing of the validated field, which marks the package as possibly -// invalid. +// See [packageHandleBuilder.evaluatePackageHandle] for more details of the +// reevaluation algorithm. // -// With the second type of invalidation, packageHandles are re-evaluated from the -// bottom up. If this process encounters a packageHandle whose deps have not -// changed (as detected by the depkeys field), then the packageHandle in -// question must also not have changed, and we need not re-evaluate its key. +// packageHandles are immutable once they are stored in the Snapshot.packages +// map: any changes to packageHandle fields evaluatePackageHandle must be made +// to a cloned packageHandle, and inserted back into Snapshot.packages. Data +// referred to by the packageHandle may be shared by multiple clones, and so +// referents must not be mutated. type packageHandle struct { mp *metadata.Package + // state indicates which data below are still valid. + state packageState + + // Local data: + // loadDiagnostics memoizes the result of processing error messages from // go/packages (i.e. `go list`). // @@ -835,27 +906,19 @@ type packageHandle struct { // (Nevertheless, since the lifetime of load diagnostics matches that of the // Metadata, it is convenient to memoize them here.) loadDiagnostics []*Diagnostic - - // Local data: - // localInputs holds all local type-checking localInputs, excluding // dependencies. - localInputs typeCheckInputs + localInputs *typeCheckInputs // localKey is a hash of localInputs. localKey file.Hash // refs is the result of syntactic dependency analysis produced by the - // typerefs package. + // typerefs package. Derived from localInputs. refs map[string][]typerefs.Symbol - // Data derived from dependencies: + // Keys, computed through reachability analysis of dependencies. - // validated indicates whether the current packageHandle is known to have a - // valid key. Invalidated package handles are stored for packages whose - // type information may have changed. - validated bool // depKeys records the key of each dependency that was used to calculate the - // key above. If the handle becomes invalid, we must re-check that each still - // matches. + // key below. If state < validKey, we must re-check that each still matches. depKeys map[PackageID]file.Hash // key is the hashed key for the package. // @@ -864,36 +927,35 @@ type packageHandle struct { key file.Hash } -// clone returns a copy of the receiver with the validated bit set to the -// provided value. -func (ph *packageHandle) clone(validated bool) *packageHandle { - copy := *ph - copy.validated = validated - return © +// clone returns a shallow copy of the receiver. +func (ph *packageHandle) clone() *packageHandle { + clone := *ph + return &clone } // getPackageHandles gets package handles for all given ids and their -// dependencies, recursively. +// dependencies, recursively. The resulting [packageHandle] values are fully +// evaluated (their state will be at least validKey). func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[PackageID]*packageHandle, error) { // perform a two-pass traversal. // // On the first pass, build up a bidirectional graph of handle nodes, and collect leaves. // Then build package handles from bottom up. - - s.mu.Lock() // guard s.meta and s.packages below b := &packageHandleBuilder{ s: s, transitiveRefs: make(map[typerefs.IndexID]*partialRefs), nodes: make(map[typerefs.IndexID]*handleNode), } + meta := s.MetadataGraph() + var leaves []*handleNode var makeNode func(*handleNode, PackageID) *handleNode makeNode = func(from *handleNode, id PackageID) *handleNode { - idxID := b.s.pkgIndex.IndexID(id) + idxID := s.view.pkgIndex.IndexID(id) n, ok := b.nodes[idxID] if !ok { - mp := s.meta.Packages[id] + mp := meta.Packages[id] if mp == nil { panic(fmt.Sprintf("nil metadata for %q", id)) } @@ -902,9 +964,6 @@ func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[ idxID: idxID, unfinishedSuccs: int32(len(mp.DepsByPkgPath)), } - if entry, hit := b.s.packages.Get(mp.ID); hit { - n.ph = entry - } if n.unfinishedSuccs == 0 { leaves = append(leaves, n) } else { @@ -922,9 +981,11 @@ func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[ return n } for _, id := range ids { + if ctx.Err() != nil { + return nil, ctx.Err() + } makeNode(nil, id) } - s.mu.Unlock() g, ctx := errgroup.WithContext(ctx) @@ -945,15 +1006,16 @@ func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[ return ctx.Err() } - b.buildPackageHandle(ctx, n) + if err := b.evaluatePackageHandle(ctx, n); err != nil { + return err + } for _, pred := range n.preds { if atomic.AddInt32(&pred.unfinishedSuccs, -1) == 0 { enqueue(pred) } } - - return n.err + return nil }) } for _, leaf := range leaves { @@ -995,7 +1057,6 @@ type handleNode struct { mp *metadata.Package idxID typerefs.IndexID ph *packageHandle - err error preds []*handleNode succs map[PackageID]*handleNode unfinishedSuccs int32 @@ -1021,7 +1082,7 @@ func (b *packageHandleBuilder) getTransitiveRefs(pkgID PackageID) map[string]*ty b.transitiveRefsMu.Lock() defer b.transitiveRefsMu.Unlock() - idxID := b.s.pkgIndex.IndexID(pkgID) + idxID := b.s.view.pkgIndex.IndexID(pkgID) trefs, ok := b.transitiveRefs[idxID] if !ok { trefs = &partialRefs{ @@ -1032,12 +1093,12 @@ func (b *packageHandleBuilder) getTransitiveRefs(pkgID PackageID) map[string]*ty if !trefs.complete { trefs.complete = true - ph := b.nodes[idxID].ph - for name := range ph.refs { + node := b.nodes[idxID] + for name := range node.ph.refs { if ('A' <= name[0] && name[0] <= 'Z') || token.IsExported(name) { if _, ok := trefs.refs[name]; !ok { - pkgs := b.s.pkgIndex.NewSet() - for _, sym := range ph.refs[name] { + pkgs := b.s.view.pkgIndex.NewSet() + for _, sym := range node.ph.refs[name] { pkgs.Add(sym.Package) otherSet := b.getOneTransitiveRefLocked(sym) pkgs.Union(otherSet) @@ -1088,7 +1149,7 @@ func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *t // point release. // // TODO(rfindley): in the future, we should turn this into an assertion. - bug.Reportf("missing reference to package %s", b.s.pkgIndex.PackageID(sym.Package)) + bug.Reportf("missing reference to package %s", b.s.view.pkgIndex.PackageID(sym.Package)) return nil } @@ -1100,7 +1161,7 @@ func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *t // See the "cycle detected" bug report above. trefs.refs[sym.Name] = nil - pkgs := b.s.pkgIndex.NewSet() + pkgs := b.s.view.pkgIndex.NewSet() for _, sym2 := range n.ph.refs[sym.Name] { pkgs.Add(sym2.Package) otherSet := b.getOneTransitiveRefLocked(sym2) @@ -1112,153 +1173,152 @@ func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *t return pkgs } -// buildPackageHandle gets or builds a package handle for the given id, storing -// its result in the snapshot.packages map. +// evaluatePackageHandle recomputes the derived information in the package handle. +// On success, the handle's state is validKey. // -// buildPackageHandle must only be called from getPackageHandles. -func (b *packageHandleBuilder) buildPackageHandle(ctx context.Context, n *handleNode) { - var prevPH *packageHandle - if n.ph != nil { - // Existing package handle: if it is valid, return it. Otherwise, create a - // copy to update. - if n.ph.validated { - return - } - prevPH = n.ph - // Either prevPH is still valid, or we will update the key and depKeys of - // this copy. In either case, the result will be valid. - n.ph = prevPH.clone(true) +// evaluatePackageHandle must only be called from getPackageHandles. +func (b *packageHandleBuilder) evaluatePackageHandle(ctx context.Context, n *handleNode) (err error) { + // Initialize n.ph. + var hit bool + b.s.mu.Lock() + n.ph, hit = b.s.packages.Get(n.mp.ID) + b.s.mu.Unlock() + + if hit && n.ph.state >= validKey { + return nil // already valid } else { + // We'll need to update the package handle. Since this could happen + // concurrently, make a copy. + if hit { + n.ph = n.ph.clone() + } else { + n.ph = &packageHandle{ + mp: n.mp, + state: validMetadata, + } + } + } + + defer func() { + if err == nil { + assert(n.ph.state == validKey, "invalid handle") + // Record the now valid key in the snapshot. + // There may be a race, so avoid the write if the recorded handle is + // already valid. + b.s.mu.Lock() + if alt, ok := b.s.packages.Get(n.mp.ID); !ok || alt.state < n.ph.state { + b.s.packages.Set(n.mp.ID, n.ph, nil) + } else { + n.ph = alt + } + b.s.mu.Unlock() + } + }() + + // Invariant: n.ph is either + // - a new handle in state validMetadata, or + // - a clone of an existing handle in state validMetadata or validLocalData. + + // State transition: validMetadata -> validLocalInputs. + localKeyChanged := false + if n.ph.state < validLocalData { + prevLocalKey := n.ph.localKey // may be zero // No package handle: read and analyze the package syntax. inputs, err := b.s.typeCheckInputs(ctx, n.mp) if err != nil { - n.err = err - return + return err } refs, err := b.s.typerefs(ctx, n.mp, inputs.compiledGoFiles) if err != nil { - n.err = err - return - } - n.ph = &packageHandle{ - mp: n.mp, - loadDiagnostics: computeLoadDiagnostics(ctx, b.s, n.mp), - localInputs: inputs, - localKey: localPackageKey(inputs), - refs: refs, - validated: true, + return err } + n.ph.loadDiagnostics = computeLoadDiagnostics(ctx, b.s, n.mp) + n.ph.localInputs = inputs + n.ph.localKey = localPackageKey(inputs) + n.ph.refs = refs + n.ph.state = validLocalData + localKeyChanged = n.ph.localKey != prevLocalKey } - // ph either did not exist, or was invalid. We must re-evaluate deps and key. - if err := b.evaluatePackageHandle(prevPH, n); err != nil { - n.err = err - return - } - - assert(n.ph.validated, "unvalidated handle") - - // Ensure the result (or an equivalent) is recorded in the snapshot. - b.s.mu.Lock() - defer b.s.mu.Unlock() + assert(n.ph.state == validLocalData, "unexpected handle state") - // Check that the metadata has not changed - // (which should invalidate this handle). + // Optimization: if the local package information did not change, nor did any + // of the dependencies, we don't need to re-run the reachability algorithm. // - // TODO(rfindley): eventually promote this to an assert. - // TODO(rfindley): move this to after building the package handle graph? - if b.s.meta.Packages[n.mp.ID] != n.mp { - bug.Reportf("stale metadata for %s", n.mp.ID) - } - - // Check the packages map again in case another goroutine got there first. - if alt, ok := b.s.packages.Get(n.mp.ID); ok && alt.validated { - if alt.mp != n.mp { - bug.Reportf("existing package handle does not match for %s", n.mp.ID) - } - n.ph = alt - } else { - b.s.packages.Set(n.mp.ID, n.ph, nil) - } -} - -// evaluatePackageHandle validates and/or computes the key of ph, setting key, -// depKeys, and the validated flag on ph. -// -// It uses prevPH to avoid recomputing keys that can't have changed, since -// their depKeys did not change. -// -// See the documentation for packageHandle for more details about packageHandle -// state, and see the documentation for the typerefs package for more details -// about precise reachability analysis. -func (b *packageHandleBuilder) evaluatePackageHandle(prevPH *packageHandle, n *handleNode) error { - // Opt: if no dep keys have changed, we need not re-evaluate the key. - if prevPH != nil { - depsChanged := false - assert(len(prevPH.depKeys) == len(n.succs), "mismatching dep count") + // Concretely: suppose A -> B -> C -> D, where '->' means "imports". If I + // type in a function body of D, I will probably invalidate types in D that C + // uses, because positions change, and therefore the package key of C will + // change. But B probably doesn't reach any types in D, and therefore the + // package key of B will not change. We still need to re-run the reachability + // algorithm on B to confirm. But if the key of B did not change, we don't + // even need to run the reachability algorithm on A. + if !localKeyChanged && + n.ph.depKeys != nil && // n.ph was previously evaluated + len(n.ph.depKeys) == len(n.succs) { + + unchanged := true for id, succ := range n.succs { - oldKey, ok := prevPH.depKeys[id] + oldKey, ok := n.ph.depKeys[id] assert(ok, "missing dep") if oldKey != succ.ph.key { - depsChanged = true + unchanged = false break } } - if !depsChanged { - return nil // key cannot have changed + if unchanged { + n.ph.state = validKey + return nil } } - // Deps have changed, so we must re-evaluate the key. + // State transition: validLocalInputs -> validKey + // + // If we get here, it must be the case that deps have changed, so we must + // run the reachability algorithm. n.ph.depKeys = make(map[PackageID]file.Hash) // See the typerefs package: the reachable set of packages is defined to be // the set of packages containing syntax that is reachable through the // exported symbols in the dependencies of n.ph. - reachable := b.s.pkgIndex.NewSet() + reachable := b.s.view.pkgIndex.NewSet() for depID, succ := range n.succs { n.ph.depKeys[depID] = succ.ph.key reachable.Add(succ.idxID) trefs := b.getTransitiveRefs(succ.mp.ID) - if trefs == nil { - // A predecessor failed to build due to e.g. context cancellation. - return fmt.Errorf("missing transitive refs for %s", succ.mp.ID) - } + assert(trefs != nil, "nil trefs") for _, set := range trefs { reachable.Union(set) } } - // Collect reachable handles. - var reachableHandles []*packageHandle + // Collect reachable nodes. + var reachableNodes []*handleNode // In the presence of context cancellation, any package may be missing. // We need all dependencies to produce a valid key. - missingReachablePackage := false reachable.Elems(func(id typerefs.IndexID) { dh := b.nodes[id] if dh == nil { - missingReachablePackage = true + // Previous code reported an error (not a bug) here. + bug.Reportf("missing reachable node for %q", id) } else { - assert(dh.ph.validated, "unvalidated dependency") - reachableHandles = append(reachableHandles, dh.ph) + reachableNodes = append(reachableNodes, dh) } }) - if missingReachablePackage { - return fmt.Errorf("missing reachable package") - } + // Sort for stability. - sort.Slice(reachableHandles, func(i, j int) bool { - return reachableHandles[i].mp.ID < reachableHandles[j].mp.ID + sort.Slice(reachableNodes, func(i, j int) bool { + return reachableNodes[i].mp.ID < reachableNodes[j].mp.ID }) // Key is the hash of the local key, and the local key of all reachable // packages. depHasher := sha256.New() depHasher.Write(n.ph.localKey[:]) - for _, rph := range reachableHandles { - depHasher.Write(rph.localKey[:]) + for _, dh := range reachableNodes { + depHasher.Write(dh.ph.localKey[:]) } depHasher.Sum(n.ph.key[:0]) + n.ph.state = validKey return nil } @@ -1277,7 +1337,7 @@ func (s *Snapshot) typerefs(ctx context.Context, mp *metadata.Package, cgfs []fi if err != nil { return nil, err } - classes := typerefs.Decode(s.pkgIndex, data) + classes := typerefs.Decode(s.view.pkgIndex, data) refs := make(map[string][]typerefs.Symbol) for _, class := range classes { for _, decl := range class.Decls { @@ -1367,7 +1427,7 @@ type typeCheckInputs struct { viewType ViewType } -func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (typeCheckInputs, error) { +func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (*typeCheckInputs, error) { // Read both lists of files of this package. // // Parallelism is not necessary here as the files will have already been @@ -1380,11 +1440,11 @@ func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (t // The need should be rare. goFiles, err := readFiles(ctx, s, mp.GoFiles) if err != nil { - return typeCheckInputs{}, err + return nil, err } compiledGoFiles, err := readFiles(ctx, s, mp.CompiledGoFiles) if err != nil { - return typeCheckInputs{}, err + return nil, err } goVersion := "" @@ -1392,7 +1452,7 @@ func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (t goVersion = mp.Module.GoVersion } - return typeCheckInputs{ + return &typeCheckInputs{ id: mp.ID, pkgPath: mp.PkgPath, name: mp.Name, @@ -1423,7 +1483,7 @@ func readFiles(ctx context.Context, fs file.Source, uris []protocol.DocumentURI) // localPackageKey returns a key for local inputs into type-checking, excluding // dependency information: files, metadata, and configuration. -func localPackageKey(inputs typeCheckInputs) file.Hash { +func localPackageKey(inputs *typeCheckInputs) file.Hash { hasher := sha256.New() // In principle, a key must be the hash of an @@ -1617,7 +1677,7 @@ func (b *typeCheckBatch) checkPackage(ctx context.Context, ph *packageHandle) (* // e.g. "go1" or "go1.2" or "go1.2.3" var goVersionRx = regexp.MustCompile(`^go[1-9][0-9]*(?:\.(0|[1-9][0-9]*)){0,2}$`) -func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs, onError func(e error)) *types.Config { +func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs *typeCheckInputs, onError func(e error)) *types.Config { cfg := &types.Config{ Sizes: inputs.sizes, Error: onError, @@ -1633,7 +1693,7 @@ func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs // See TestFixImportDecl for an example. return nil, fmt.Errorf("missing metadata for import of %q", path) } - depPH := b.handles[id] + depPH := b.getHandle(id) if depPH == nil { // e.g. missing metadata for dependencies in buildPackageHandle return nil, missingPkgError(inputs.id, path, inputs.viewType) @@ -1862,7 +1922,7 @@ func missingPkgError(from PackageID, pkgPath string, viewType ViewType) error { // sequence to a terminal). // // Fields in typeCheckInputs may affect the resulting diagnostics. -func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs typeCheckInputs, errs []types.Error) []*Diagnostic { +func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs []types.Error) []*Diagnostic { var result []*Diagnostic // batch records diagnostics for a set of related types.Errors. diff --git a/gopls/internal/cache/diagnostics.go b/gopls/internal/cache/diagnostics.go index 329f7e7e718..797ce961cd8 100644 --- a/gopls/internal/cache/diagnostics.go +++ b/gopls/internal/cache/diagnostics.go @@ -103,10 +103,10 @@ type SuggestedFix struct { } // SuggestedFixFromCommand returns a suggested fix to run the given command. -func SuggestedFixFromCommand(cmd protocol.Command, kind protocol.CodeActionKind) SuggestedFix { +func SuggestedFixFromCommand(cmd *protocol.Command, kind protocol.CodeActionKind) SuggestedFix { return SuggestedFix{ Title: cmd.Title, - Command: &cmd, + Command: cmd, ActionKind: kind, } } diff --git a/gopls/internal/cache/errors.go b/gopls/internal/cache/errors.go index 26dc0c446ef..26747a63d33 100644 --- a/gopls/internal/cache/errors.go +++ b/gopls/internal/cache/errors.go @@ -14,7 +14,6 @@ import ( "go/parser" "go/scanner" "go/token" - "log" "path/filepath" "regexp" "strconv" @@ -135,15 +134,11 @@ func goGetQuickFixes(haveModule bool, uri protocol.DocumentURI, pkg string) []Su return nil } title := fmt.Sprintf("go get package %v", pkg) - cmd, err := command.NewGoGetPackageCommand(title, command.GoGetPackageArgs{ + cmd := command.NewGoGetPackageCommand(title, command.GoGetPackageArgs{ URI: uri, AddRequire: true, Pkg: pkg, }) - if err != nil { - bug.Reportf("internal error building 'go get package' fix: %v", err) - return nil - } return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)} } @@ -153,14 +148,10 @@ func editGoDirectiveQuickFix(haveModule bool, uri protocol.DocumentURI, version return nil } title := fmt.Sprintf("go mod edit -go=%s", version) - cmd, err := command.NewEditGoDirectiveCommand(title, command.EditGoDirectiveArgs{ + cmd := command.NewEditGoDirectiveCommand(title, command.EditGoDirectiveArgs{ URI: uri, Version: version, }) - if err != nil { - bug.Reportf("internal error constructing 'edit go directive' fix: %v", err) - return nil - } return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)} } @@ -332,15 +323,10 @@ func toSourceDiagnostic(srcAnalyzer *settings.Analyzer, gobDiag *gobDiagnostic) // by logic "adjacent to" the analyzer. // // The analysis.Diagnostic.Category is used as the fix name. - cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ - Fix: diag.Code, - URI: gobDiag.Location.URI, - Range: gobDiag.Location.Range, + cmd := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ + Fix: diag.Code, + Location: gobDiag.Location, }) - if err != nil { - // JSON marshalling of these argument values cannot fail. - log.Fatalf("internal error in NewApplyFixCommand: %v", err) - } for _, kind := range kinds { fixes = append(fixes, SuggestedFixFromCommand(cmd, kind)) } diff --git a/gopls/internal/cache/future.go b/gopls/internal/cache/future.go new file mode 100644 index 00000000000..8aa69e11fc6 --- /dev/null +++ b/gopls/internal/cache/future.go @@ -0,0 +1,136 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "context" + "sync" +) + +// A futureCache is a key-value store of "futures", which are values that might +// not yet be processed. By accessing values using [futureCache.get], the +// caller may share work with other goroutines that require the same key. +// +// This is a relatively common pattern, though this implementation includes the +// following two non-standard additions: +// +// 1. futures are cancellable and retryable. If the context being used to +// compute the future is cancelled, it will abort the computation. If other +// goroutes are awaiting the future, they will acquire the right to compute +// it, and start anew. +// 2. futures may be either persistent or transient. Persistent futures are +// the standard pattern: the results of the computation are preserved for +// the lifetime of the cache. However, if the cache is transient +// (persistent=false), the futures will be discarded once their value has +// been passed to all awaiting goroutines. +// +// These specific extensions are used to implement the concurrency model of the +// [typeCheckBatch], which allows multiple operations to piggy-back on top of +// an ongoing type checking operation, requesting new packages asynchronously +// without unduly increasing the in-use memory required by the type checking +// pass. +type futureCache[K comparable, V any] struct { + persistent bool + + mu sync.Mutex + cache map[K]*future[V] +} + +// newFutureCache returns a futureCache that is ready to coordinate +// computations via [futureCache.get]. +// +// If persistent is true, the results of these computations are stored for the +// lifecycle of cache. Otherwise, results are discarded after they have been +// passed to all awaiting goroutines. +func newFutureCache[K comparable, V any](persistent bool) *futureCache[K, V] { + return &futureCache[K, V]{ + persistent: persistent, + cache: make(map[K]*future[V]), + } +} + +type future[V any] struct { + // refs is the number of goroutines awaiting this future, to be used for + // cleaning up transient cache entries. + // + // Guarded by futureCache.mu. + refs int + + // done is closed when the future has been fully computed. + done chan unit + + // acquire used to select an awaiting goroutine to run the computation. + // acquire is 1-buffered, and initialized with one unit, so that the first + // requester starts a computation. If that computation is cancelled, the + // requester pushes the unit back to acquire, so that another goroutine may + // execute the computation. + acquire chan unit + + // v and err store the result of the computation, guarded by done. + v V + err error +} + +// cacheFunc is the type of a future computation function. +type cacheFunc[V any] func(context.Context) (V, error) + +// get retrieves or computes the value corresponding to k. +// +// If the cache if persistent and the value has already been computed, get +// returns the result of the previous computation. Otherwise, get either starts +// a computation or joins an ongoing computation. If that computation is +// cancelled, get will reassign the computation to a new goroutine as long as +// there are awaiters. +// +// Once the computation completes, the result is passed to all awaiting +// goroutines. If the cache is transient (persistent=false), the corresponding +// cache entry is removed, and the next call to get will execute a new +// computation. +// +// It is therefore the responsibility of the caller to ensure that the given +// compute function is safely retryable, and always returns the same value. +func (c *futureCache[K, V]) get(ctx context.Context, k K, compute cacheFunc[V]) (V, error) { + c.mu.Lock() + f, ok := c.cache[k] + if !ok { + f = &future[V]{ + done: make(chan unit), + acquire: make(chan unit, 1), + } + f.acquire <- unit{} // make available for computation + c.cache[k] = f + } + f.refs++ + c.mu.Unlock() + + defer func() { + c.mu.Lock() + defer c.mu.Unlock() + f.refs-- + if f.refs == 0 && !c.persistent { + delete(c.cache, k) + } + }() + + var zero V + select { + case <-ctx.Done(): + return zero, ctx.Err() + case <-f.done: + return f.v, f.err + case <-f.acquire: + } + + v, err := compute(ctx) + if err := ctx.Err(); err != nil { + f.acquire <- unit{} // hand off work to the next requester + return zero, err + } + + f.v = v + f.err = err + close(f.done) + return v, err +} diff --git a/gopls/internal/cache/future_test.go b/gopls/internal/cache/future_test.go new file mode 100644 index 00000000000..d96dc0f5317 --- /dev/null +++ b/gopls/internal/cache/future_test.go @@ -0,0 +1,156 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + "time" + + "golang.org/x/sync/errgroup" +) + +func TestFutureCache_Persistent(t *testing.T) { + c := newFutureCache[int, int](true) + ctx := context.Background() + + var computed atomic.Int32 + compute := func(i int) cacheFunc[int] { + return func(context.Context) (int, error) { + computed.Add(1) + return i, ctx.Err() + } + } + + testFutureCache(t, ctx, c, compute) + + // Since this cache is persistent, we should get exactly 10 computations, + // since there are 10 distinct keys in [testFutureCache]. + if got := computed.Load(); got != 10 { + t.Errorf("computed %d times, want 10", got) + } +} + +func TestFutureCache_Ephemeral(t *testing.T) { + c := newFutureCache[int, int](false) + ctx := context.Background() + + var computed atomic.Int32 + compute := func(i int) cacheFunc[int] { + return func(context.Context) (int, error) { + time.Sleep(1 * time.Millisecond) + computed.Add(1) + return i, ctx.Err() + } + } + + testFutureCache(t, ctx, c, compute) + + // Since this cache is ephemeral, we should get at least 30 computations, + // since there are 10 distinct keys and three synchronous passes in + // [testFutureCache]. + if got := computed.Load(); got < 30 { + t.Errorf("computed %d times, want at least 30", got) + } else { + t.Logf("compute ran %d times", got) + } +} + +// testFutureCache starts 100 goroutines concurrently, indexed by j, each +// getting key j%10 from the cache. It repeats this three times, synchronizing +// after each. +// +// This is designed to exercise both concurrent and synchronous access to the +// cache. +func testFutureCache(t *testing.T, ctx context.Context, c *futureCache[int, int], compute func(int) cacheFunc[int]) { + for range 3 { + var g errgroup.Group + for j := range 100 { + mod := j % 10 + compute := compute(mod) + g.Go(func() error { + got, err := c.get(ctx, mod, compute) + if err == nil && got != mod { + t.Errorf("get() = %d, want %d", got, mod) + } + return err + }) + } + if err := g.Wait(); err != nil { + t.Fatal(err) + } + } +} + +func TestFutureCache_Retrying(t *testing.T) { + // This test verifies the retry behavior of cache entries, + // by checking that cancelled work is handed off to the next awaiter. + // + // The setup is a little tricky: 10 goroutines are started, and the first 9 + // are cancelled whereas the 10th is allowed to finish. As a result, the + // computation should always succeed with value 9. + + ctx := context.Background() + + for _, persistent := range []bool{true, false} { + t.Run(fmt.Sprintf("persistent=%t", persistent), func(t *testing.T) { + c := newFutureCache[int, int](persistent) + + var started atomic.Int32 + + // compute returns a new cacheFunc that produces the value i, after the + // provided done channel is closed. + compute := func(i int, done <-chan struct{}) cacheFunc[int] { + return func(ctx context.Context) (int, error) { + started.Add(1) + select { + case <-ctx.Done(): + return 0, ctx.Err() + case <-done: + return i, nil + } + } + } + + // goroutines are either cancelled, or allowed to complete, + // as controlled by cancels and dones. + var ( + cancels = make([]func(), 10) + dones = make([]chan struct{}, 10) + ) + + var g errgroup.Group + var lastValue atomic.Int32 // keep track of the last successfully computed value + for i := range 10 { + ctx, cancel := context.WithCancel(ctx) + done := make(chan struct{}) + cancels[i] = cancel + dones[i] = done + compute := compute(i, done) + g.Go(func() error { + v, err := c.get(ctx, 0, compute) + if err == nil { + lastValue.Store(int32(v)) + } + return nil + }) + } + for _, cancel := range cancels[:9] { + cancel() + } + defer cancels[9]() + + dones[9] <- struct{}{} + g.Wait() + + t.Logf("started %d computations", started.Load()) + if got := lastValue.Load(); got != 9 { + t.Errorf("after cancelling computation 0-8, got %d, want 9", got) + } + }) + } +} diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go index 36aeddcd9e0..9373766b413 100644 --- a/gopls/internal/cache/load.go +++ b/gopls/internal/cache/load.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "sort" "strings" "sync/atomic" @@ -23,7 +24,6 @@ import ( "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/immutable" "golang.org/x/tools/gopls/internal/util/pathutil" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/packagesinternal" @@ -412,18 +412,15 @@ func buildMetadata(updates map[PackageID]*metadata.Package, pkg *packages.Packag updates[id] = mp - for _, filename := range pkg.CompiledGoFiles { - uri := protocol.URIFromPath(filename) - mp.CompiledGoFiles = append(mp.CompiledGoFiles, uri) - } - for _, filename := range pkg.GoFiles { - uri := protocol.URIFromPath(filename) - mp.GoFiles = append(mp.GoFiles, uri) - } - for _, filename := range pkg.IgnoredFiles { - uri := protocol.URIFromPath(filename) - mp.IgnoredFiles = append(mp.IgnoredFiles, uri) + copyURIs := func(dst *[]protocol.DocumentURI, src []string) { + for _, filename := range src { + *dst = append(*dst, protocol.URIFromPath(filename)) + } } + copyURIs(&mp.CompiledGoFiles, pkg.CompiledGoFiles) + copyURIs(&mp.GoFiles, pkg.GoFiles) + copyURIs(&mp.IgnoredFiles, pkg.IgnoredFiles) + copyURIs(&mp.OtherFiles, pkg.OtherFiles) depsByImpPath := make(map[ImportPath]PackageID) depsByPkgPath := make(map[PackagePath]PackageID) diff --git a/gopls/internal/cache/metadata/metadata.go b/gopls/internal/cache/metadata/metadata.go index 7860f336954..e42aac304f6 100644 --- a/gopls/internal/cache/metadata/metadata.go +++ b/gopls/internal/cache/metadata/metadata.go @@ -45,10 +45,11 @@ type Package struct { PkgPath PackagePath Name PackageName - // these three fields are as defined by go/packages.Package + // These fields are as defined by go/packages.Package GoFiles []protocol.DocumentURI CompiledGoFiles []protocol.DocumentURI IgnoredFiles []protocol.DocumentURI + OtherFiles []protocol.DocumentURI ForTest PackagePath // q in a "p [q.test]" package, else "" TypesSizes types.Sizes diff --git a/gopls/internal/cache/methodsets/methodsets.go b/gopls/internal/cache/methodsets/methodsets.go index ed7ead7a747..98b0563ceeb 100644 --- a/gopls/internal/cache/methodsets/methodsets.go +++ b/gopls/internal/cache/methodsets/methodsets.go @@ -54,7 +54,6 @@ import ( "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/util/frob" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/internal/aliases" ) // An Index records the non-empty method sets of all package-level @@ -305,7 +304,7 @@ func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) gob // EnsurePointer wraps T in a types.Pointer if T is a named, non-interface type. // This is useful to make sure you consider a named type's full method set. func EnsurePointer(T types.Type) types.Type { - if _, ok := aliases.Unalias(T).(*types.Named); ok && !types.IsInterface(T) { + if _, ok := types.Unalias(T).(*types.Named); ok && !types.IsInterface(T) { return types.NewPointer(T) } @@ -330,8 +329,8 @@ func fingerprint(method *types.Func) (string, bool) { var fprint func(t types.Type) fprint = func(t types.Type) { switch t := t.(type) { - case *aliases.Alias: - fprint(aliases.Unalias(t)) + case *types.Alias: + fprint(types.Unalias(t)) case *types.Named: tname := t.Obj() @@ -449,7 +448,7 @@ func fingerprint(method *types.Func) (string, bool) { } buf.WriteString(method.Id()) // e.g. "pkg.Type" - sig := method.Type().(*types.Signature) + sig := method.Signature() fprint(sig.Params()) fprint(sig.Results()) return buf.String(), tricky diff --git a/gopls/internal/cache/mod.go b/gopls/internal/cache/mod.go index c1d69a45038..6837ec3257c 100644 --- a/gopls/internal/cache/mod.go +++ b/gopls/internal/cache/mod.go @@ -418,10 +418,7 @@ func (s *Snapshot) goCommandDiagnostic(pm *ParsedModule, loc protocol.Location, switch { case strings.Contains(goCmdError, "inconsistent vendoring"): - cmd, err := command.NewVendorCommand("Run go mod vendor", command.URIArg{URI: pm.URI}) - if err != nil { - return nil, err - } + cmd := command.NewVendorCommand("Run go mod vendor", command.URIArg{URI: pm.URI}) return &Diagnostic{ URI: pm.URI, Range: loc.Range, @@ -435,14 +432,8 @@ See https://github.com/golang/go/issues/39164 for more detail on this issue.`, case strings.Contains(goCmdError, "updates to go.sum needed"), strings.Contains(goCmdError, "missing go.sum entry"): var args []protocol.DocumentURI args = append(args, s.View().ModFiles()...) - tidyCmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: args}) - if err != nil { - return nil, err - } - updateCmd, err := command.NewUpdateGoSumCommand("Update go.sum", command.URIArgs{URIs: args}) - if err != nil { - return nil, err - } + tidyCmd := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: args}) + updateCmd := command.NewUpdateGoSumCommand("Update go.sum", command.URIArgs{URIs: args}) msg := "go.sum is out of sync with go.mod. Please update it by applying the quick fix." if innermost != nil { msg = fmt.Sprintf("go.sum is out of sync with go.mod: entry for %v is missing. Please updating it by applying the quick fix.", innermost) @@ -460,14 +451,11 @@ See https://github.com/golang/go/issues/39164 for more detail on this issue.`, }, nil case strings.Contains(goCmdError, "disabled by GOPROXY=off") && innermost != nil: title := fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version) - cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{ + cmd := command.NewAddDependencyCommand(title, command.DependencyArgs{ URI: pm.URI, AddRequire: false, GoCmdArgs: []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)}, }) - if err != nil { - return nil, err - } return &Diagnostic{ URI: pm.URI, Range: loc.Range, diff --git a/gopls/internal/cache/mod_tidy.go b/gopls/internal/cache/mod_tidy.go index 90448d62cc5..8532d1c7497 100644 --- a/gopls/internal/cache/mod_tidy.go +++ b/gopls/internal/cache/mod_tidy.go @@ -333,14 +333,11 @@ func unusedDiagnostic(m *protocol.Mapper, req *modfile.Require, onlyDiagnostic b return nil, err } title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path) - cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{ + cmd := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{ URI: m.URI, OnlyDiagnostic: onlyDiagnostic, ModulePath: req.Mod.Path, }) - if err != nil { - return nil, err - } return &Diagnostic{ URI: m.URI, Range: rng, @@ -406,14 +403,11 @@ func missingModuleDiagnostic(pm *ParsedModule, req *modfile.Require) (*Diagnosti } } title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path) - cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{ + cmd := command.NewAddDependencyCommand(title, command.DependencyArgs{ URI: pm.Mapper.URI, AddRequire: !req.Indirect, GoCmdArgs: []string{req.Mod.Path + "@" + req.Mod.Version}, }) - if err != nil { - return nil, err - } return &Diagnostic{ URI: pm.Mapper.URI, Range: rng, diff --git a/gopls/internal/cache/package.go b/gopls/internal/cache/package.go index e2555c8ed98..5c0da7e6af0 100644 --- a/gopls/internal/cache/package.go +++ b/gopls/internal/cache/package.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/testfuncs" "golang.org/x/tools/gopls/internal/cache/xrefs" "golang.org/x/tools/gopls/internal/protocol" ) @@ -60,6 +61,9 @@ type syntaxPackage struct { methodsetsOnce sync.Once _methodsets *methodsets.Index // only used by the methodsets method + + testsOnce sync.Once + _tests *testfuncs.Index // only used by the tests method } func (p *syntaxPackage) xrefs() []byte { @@ -76,6 +80,13 @@ func (p *syntaxPackage) methodsets() *methodsets.Index { return p._methodsets } +func (p *syntaxPackage) tests() *testfuncs.Index { + p.testsOnce.Do(func() { + p._tests = testfuncs.NewIndex(p.compiledGoFiles, p.typesInfo) + }) + return p._tests +} + func (p *Package) String() string { return string(p.metadata.ID) } func (p *Package) Metadata() *metadata.Package { return p.metadata } diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index 0143a36ab71..82f3eeebeec 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -824,7 +824,7 @@ func parseStmt(tok *token.File, pos token.Pos, src []byte) (ast.Stmt, error) { // Use ParseFile instead of ParseExpr because ParseFile has // best-effort behavior, whereas ParseExpr fails hard on any error. - fakeFile, err := parser.ParseFile(token.NewFileSet(), "", fileSrc, 0) + fakeFile, err := parser.ParseFile(token.NewFileSet(), "", fileSrc, parser.SkipObjectResolution) if fakeFile == nil { return nil, fmt.Errorf("error reading fake file source: %v", err) } diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go index 23aebdb078a..65ba7e69d0a 100644 --- a/gopls/internal/cache/session.go +++ b/gopls/internal/cache/session.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -24,7 +25,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/persistent" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" @@ -230,6 +230,7 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, * initialWorkspaceLoad: make(chan struct{}), initializationSema: make(chan struct{}, 1), baseCtx: baseCtx, + pkgIndex: typerefs.NewPackageIndex(), parseCache: s.parseCache, ignoreFilter: ignoreFilter, fs: s.overlayFS, @@ -257,7 +258,6 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, * modTidyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), modVulnHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), modWhyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), - pkgIndex: typerefs.NewPackageIndex(), moduleUpgrades: new(persistent.Map[protocol.DocumentURI, map[string]string]), vulns: new(persistent.Map[protocol.DocumentURI, *vulncheck.Result]), } diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 9014817bdff..004dc5279c0 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -19,6 +19,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "sort" "strconv" "strings" @@ -30,7 +31,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" "golang.org/x/tools/gopls/internal/cache/parsego" - "golang.org/x/tools/gopls/internal/cache/typerefs" + "golang.org/x/tools/gopls/internal/cache/testfuncs" "golang.org/x/tools/gopls/internal/cache/xrefs" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/filecache" @@ -43,7 +44,6 @@ import ( "golang.org/x/tools/gopls/internal/util/immutable" "golang.org/x/tools/gopls/internal/util/pathutil" "golang.org/x/tools/gopls/internal/util/persistent" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/label" @@ -190,9 +190,6 @@ type Snapshot struct { importGraphDone chan struct{} // closed when importGraph is set; may be nil importGraph *importGraph // copied from preceding snapshot and re-evaluated - // pkgIndex is an index of package IDs, for efficient storage of typerefs. - pkgIndex *typerefs.PackageIndex - // moduleUpgrades tracks known upgrades for module paths in each modfile. // Each modfile has a map of module name to upgrade version. moduleUpgrades *persistent.Map[protocol.DocumentURI, map[string]string] @@ -203,6 +200,14 @@ type Snapshot struct { // gcOptimizationDetails describes the packages for which we want // optimization details to be included in the diagnostics. gcOptimizationDetails map[metadata.PackageID]unit + + // Concurrent type checking: + // typeCheckMu guards the ongoing type checking batch, and reference count of + // ongoing type checking operations. + // When the batch is no longer needed (batchRef=0), it is discarded. + typeCheckMu sync.Mutex + batchRef int + batch *typeCheckBatch } var _ memoize.RefCounted = (*Snapshot)(nil) // snapshots are reference-counted @@ -572,6 +577,7 @@ func (s *Snapshot) Overlays() []*overlay { const ( xrefsKind = "xrefs" methodSetsKind = "methodsets" + testsKind = "tests" exportDataKind = "export" diagnosticsKind = "diagnostics" typerefsKind = "typerefs" @@ -673,6 +679,32 @@ func (s *Snapshot) MethodSets(ctx context.Context, ids ...PackageID) ([]*methods return indexes, s.forEachPackage(ctx, ids, pre, post) } +// Tests returns test-set indexes for the specified packages. There is a +// one-to-one correspondence between ID and Index. +// +// If these indexes cannot be loaded from cache, the requested packages may be +// type-checked. +func (s *Snapshot) Tests(ctx context.Context, ids ...PackageID) ([]*testfuncs.Index, error) { + ctx, done := event.Start(ctx, "cache.snapshot.Tests") + defer done() + + indexes := make([]*testfuncs.Index, len(ids)) + pre := func(i int, ph *packageHandle) bool { + data, err := filecache.Get(testsKind, ph.key) + if err == nil { // hit + indexes[i] = testfuncs.Decode(data) + return false + } else if err != filecache.ErrNotFound { + event.Error(ctx, "reading tests from filecache", err) + } + return true + } + post := func(i int, pkg *Package) { + indexes[i] = pkg.pkg.tests() + } + return indexes, s.forEachPackage(ctx, ids, pre, post) +} + // MetadataForFile returns a new slice containing metadata for each // package containing the Go file identified by uri, ordered by the // number of CompiledGoFiles (i.e. "narrowest" to "widest" package), @@ -1451,58 +1483,54 @@ searchOverlays: if s.view.folder.Env.GoVersion >= 18 { if s.view.gowork != "" { fix = fmt.Sprintf("To fix this problem, you can add this module to your go.work file (%s)", s.view.gowork) - if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use`", command.RunGoWorkArgs{ + cmd := command.NewRunGoWorkCommandCommand("Run `go work use`", command.RunGoWorkArgs{ ViewID: s.view.ID(), Args: []string{"use", modDir}, - }); err == nil { - suggestedFixes = append(suggestedFixes, SuggestedFix{ - Title: "Use this module in your go.work file", - Command: &cmd, - ActionKind: protocol.QuickFix, - }) - } + }) + suggestedFixes = append(suggestedFixes, SuggestedFix{ + Title: "Use this module in your go.work file", + Command: cmd, + ActionKind: protocol.QuickFix, + }) if inDir { - if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use -r`", command.RunGoWorkArgs{ + cmd := command.NewRunGoWorkCommandCommand("Run `go work use -r`", command.RunGoWorkArgs{ ViewID: s.view.ID(), Args: []string{"use", "-r", "."}, - }); err == nil { - suggestedFixes = append(suggestedFixes, SuggestedFix{ - Title: "Use all modules in your workspace", - Command: &cmd, - ActionKind: protocol.QuickFix, - }) - } + }) + suggestedFixes = append(suggestedFixes, SuggestedFix{ + Title: "Use all modules in your workspace", + Command: cmd, + ActionKind: protocol.QuickFix, + }) } } else { fix = "To fix this problem, you can add a go.work file that uses this directory." - if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use`", command.RunGoWorkArgs{ + cmd := command.NewRunGoWorkCommandCommand("Run `go work init && go work use`", command.RunGoWorkArgs{ ViewID: s.view.ID(), InitFirst: true, Args: []string{"use", modDir}, - }); err == nil { - suggestedFixes = []SuggestedFix{ - { - Title: "Add a go.work file using this module", - Command: &cmd, - ActionKind: protocol.QuickFix, - }, - } + }) + suggestedFixes = []SuggestedFix{ + { + Title: "Add a go.work file using this module", + Command: cmd, + ActionKind: protocol.QuickFix, + }, } if inDir { - if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use -r`", command.RunGoWorkArgs{ + cmd := command.NewRunGoWorkCommandCommand("Run `go work init && go work use -r`", command.RunGoWorkArgs{ ViewID: s.view.ID(), InitFirst: true, Args: []string{"use", "-r", "."}, - }); err == nil { - suggestedFixes = append(suggestedFixes, SuggestedFix{ - Title: "Add a go.work file using all modules in your workspace", - Command: &cmd, - ActionKind: protocol.QuickFix, - }) - } + }) + suggestedFixes = append(suggestedFixes, SuggestedFix{ + Title: "Add a go.work file using all modules in your workspace", + Command: cmd, + ActionKind: protocol.QuickFix, + }) } } } else { @@ -1654,7 +1682,6 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f modWhyHandles: cloneWithout(s.modWhyHandles, changedFiles, &needsDiagnosis), modVulnHandles: cloneWithout(s.modVulnHandles, changedFiles, &needsDiagnosis), importGraph: s.importGraph, - pkgIndex: s.pkgIndex, moduleUpgrades: cloneWith(s.moduleUpgrades, changed.ModuleUpgrades), vulns: cloneWith(s.vulns, changed.Vulns), } @@ -1918,14 +1945,21 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f // Invalidated package information. for id, invalidateMetadata := range idsToInvalidate { - if _, ok := directIDs[id]; ok || invalidateMetadata { - if result.packages.Delete(id) { - needsDiagnosis = true - } - } else { - if entry, hit := result.packages.Get(id); hit { - needsDiagnosis = true - ph := entry.clone(false) + // See the [packageHandle] documentation for more details about this + // invalidation. + if ph, ok := result.packages.Get(id); ok { + needsDiagnosis = true + if invalidateMetadata { + result.packages.Delete(id) + } else { + // If the package was just invalidated by a dependency, its local + // inputs are still valid. + ph = ph.clone() + if _, ok := directIDs[id]; ok { + ph.state = validMetadata // local inputs changed + } else { + ph.state = min(ph.state, validLocalData) // a dependency changed + } result.packages.Set(id, ph, nil) } } @@ -2246,7 +2280,8 @@ func extractMagicComments(f *ast.File) []string { return results } -// BuiltinFile returns information about the special builtin package. +// BuiltinFile returns the pseudo-source file builtins.go, +// parsed with legacy ast.Object resolution. func (s *Snapshot) BuiltinFile(ctx context.Context) (*parsego.File, error) { s.AwaitInitialized(ctx) diff --git a/gopls/internal/cache/testfuncs/match.go b/gopls/internal/cache/testfuncs/match.go new file mode 100644 index 00000000000..a7b5cb7dd58 --- /dev/null +++ b/gopls/internal/cache/testfuncs/match.go @@ -0,0 +1,116 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testfuncs + +import ( + "fmt" + "strconv" + "strings" +) + +// The functions in this file are copies of those from the testing package. +// +// https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/testing/match.go + +// uniqueName creates a unique name for the given parent and subname by affixing +// it with one or more counts, if necessary. +func (b *indexBuilder) uniqueName(parent, subname string) string { + base := parent + "/" + subname + + for { + n := b.subNames[base] + if n < 0 { + panic("subtest count overflow") + } + b.subNames[base] = n + 1 + + if n == 0 && subname != "" { + prefix, nn := parseSubtestNumber(base) + if len(prefix) < len(base) && nn < b.subNames[prefix] { + // This test is explicitly named like "parent/subname#NN", + // and #NN was already used for the NNth occurrence of "parent/subname". + // Loop to add a disambiguating suffix. + continue + } + return base + } + + name := fmt.Sprintf("%s#%02d", base, n) + if b.subNames[name] != 0 { + // This is the nth occurrence of base, but the name "parent/subname#NN" + // collides with the first occurrence of a subtest *explicitly* named + // "parent/subname#NN". Try the next number. + continue + } + + return name + } +} + +// parseSubtestNumber splits a subtest name into a "#%02d"-formatted int +// suffix (if present), and a prefix preceding that suffix (always). +func parseSubtestNumber(s string) (prefix string, nn int) { + i := strings.LastIndex(s, "#") + if i < 0 { + return s, 0 + } + + prefix, suffix := s[:i], s[i+1:] + if len(suffix) < 2 || (len(suffix) > 2 && suffix[0] == '0') { + // Even if suffix is numeric, it is not a possible output of a "%02" format + // string: it has either too few digits or too many leading zeroes. + return s, 0 + } + if suffix == "00" { + if !strings.HasSuffix(prefix, "/") { + // We only use "#00" as a suffix for subtests named with the empty + // string — it isn't a valid suffix if the subtest name is non-empty. + return s, 0 + } + } + + n, err := strconv.ParseInt(suffix, 10, 32) + if err != nil || n < 0 { + return s, 0 + } + return prefix, int(n) +} + +// rewrite rewrites a subname to having only printable characters and no white +// space. +func rewrite(s string) string { + b := []byte{} + for _, r := range s { + switch { + case isSpace(r): + b = append(b, '_') + case !strconv.IsPrint(r): + s := strconv.QuoteRune(r) + b = append(b, s[1:len(s)-1]...) + default: + b = append(b, string(r)...) + } + } + return string(b) +} + +func isSpace(r rune) bool { + if r < 0x2000 { + switch r { + // Note: not the same as Unicode Z class. + case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0, 0x1680: + return true + } + } else { + if r <= 0x200a { + return true + } + switch r { + case 0x2028, 0x2029, 0x202f, 0x205f, 0x3000: + return true + } + } + return false +} diff --git a/gopls/internal/cache/testfuncs/tests.go b/gopls/internal/cache/testfuncs/tests.go new file mode 100644 index 00000000000..cfef3c54164 --- /dev/null +++ b/gopls/internal/cache/testfuncs/tests.go @@ -0,0 +1,324 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testfuncs + +import ( + "go/ast" + "go/constant" + "go/types" + "regexp" + "strings" + + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/frob" + "golang.org/x/tools/gopls/internal/util/safetoken" +) + +// An Index records the test set of a package. +type Index struct { + pkg gobPackage +} + +// Decode decodes the given gob-encoded data as an Index. +func Decode(data []byte) *Index { + var pkg gobPackage + packageCodec.Decode(data, &pkg) + return &Index{pkg} +} + +// Encode encodes the receiver as gob-encoded data. +func (index *Index) Encode() []byte { + return packageCodec.Encode(index.pkg) +} + +func (index *Index) All() []Result { + var results []Result + for _, file := range index.pkg.Files { + for _, test := range file.Tests { + results = append(results, test.result()) + } + } + return results +} + +// A Result reports a test function +type Result struct { + Location protocol.Location // location of the test + Name string // name of the test +} + +// NewIndex returns a new index of method-set information for all +// package-level types in the specified package. +func NewIndex(files []*parsego.File, info *types.Info) *Index { + b := &indexBuilder{ + fileIndex: make(map[protocol.DocumentURI]int), + subNames: make(map[string]int), + } + return b.build(files, info) +} + +// build adds to the index all tests of the specified package. +func (b *indexBuilder) build(files []*parsego.File, info *types.Info) *Index { + for _, file := range files { + if !strings.HasSuffix(file.Tok.Name(), "_test.go") { + continue + } + + for _, decl := range file.File.Decls { + decl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + obj, ok := info.ObjectOf(decl.Name).(*types.Func) + if !ok || !obj.Exported() { + continue + } + + // error.Error has empty Position, PkgPath, and ObjectPath. + if obj.Pkg() == nil { + continue + } + + isTest, isExample := isTestOrExample(obj) + if !isTest && !isExample { + continue + } + + var t gobTest + t.Name = decl.Name.Name + t.Location.URI = file.URI + t.Location.Range, _ = file.NodeRange(decl) + + i, ok := b.fileIndex[t.Location.URI] + if !ok { + i = len(b.Files) + b.Files = append(b.Files, gobFile{}) + b.fileIndex[t.Location.URI] = i + } + + b.Files[i].Tests = append(b.Files[i].Tests, t) + + // Check for subtests + if isTest { + b.Files[i].Tests = append(b.Files[i].Tests, b.findSubtests(t, decl.Type, decl.Body, file, files, info)...) + } + } + } + + return &Index{pkg: b.gobPackage} +} + +func (b *indexBuilder) findSubtests(parent gobTest, typ *ast.FuncType, body *ast.BlockStmt, file *parsego.File, files []*parsego.File, info *types.Info) []gobTest { + if body == nil { + return nil + } + + // If the [testing.T] parameter is unnamed, the func cannot call + // [testing.T.Run] and thus cannot create any subtests + if len(typ.Params.List[0].Names) == 0 { + return nil + } + + // This "can't fail" because testKind should guarantee that the function has + // one parameter and the check above guarantees that parameter is named + param := info.ObjectOf(typ.Params.List[0].Names[0]) + + // Find statements of form t.Run(name, func(...) {...}) where t is the + // parameter of the enclosing test function. + var tests []gobTest + for _, stmt := range body.List { + expr, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + + call, ok := expr.X.(*ast.CallExpr) + if !ok || len(call.Args) != 2 { + continue + } + fun, ok := call.Fun.(*ast.SelectorExpr) + if !ok || fun.Sel.Name != "Run" { + continue + } + recv, ok := fun.X.(*ast.Ident) + if !ok || info.ObjectOf(recv) != param { + continue + } + + sig, ok := info.TypeOf(call.Args[1]).(*types.Signature) + if !ok { + continue + } + if _, ok := testKind(sig); !ok { + continue // subtest has wrong signature + } + + val := info.Types[call.Args[0]].Value + if val == nil || val.Kind() != constant.String { + continue + } + + var t gobTest + t.Name = b.uniqueName(parent.Name, rewrite(constant.StringVal(val))) + t.Location.URI = file.URI + t.Location.Range, _ = file.NodeRange(call) + tests = append(tests, t) + + if typ, body := findFunc(files, info, body, call.Args[1]); typ != nil { + tests = append(tests, b.findSubtests(t, typ, body, file, files, info)...) + } + } + return tests +} + +// findFunc finds the type and body of the given expr, which may be a function +// literal or reference to a declared function. +// +// If no function is found, findFunc returns (nil, nil). +func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr ast.Expr) (*ast.FuncType, *ast.BlockStmt) { + var obj types.Object + switch arg := expr.(type) { + case *ast.FuncLit: + return arg.Type, arg.Body + + case *ast.Ident: + obj = info.ObjectOf(arg) + if obj == nil { + return nil, nil + } + + case *ast.SelectorExpr: + // Look for methods within the current package. We will not handle + // imported functions and methods for now, as that would require access + // to the source of other packages and would be substantially more + // complex. However, those cases should be rare. + sel, ok := info.Selections[arg] + if !ok { + return nil, nil + } + obj = sel.Obj() + + default: + return nil, nil + } + + if v, ok := obj.(*types.Var); ok { + // TODO: Handle vars. This could handled by walking over the body (and + // the file), but that doesn't account for assignment. If the variable + // is assigned multiple times, we could easily get the wrong one. + _, _ = v, body + return nil, nil + } + + for _, file := range files { + // Skip files that don't contain the object (there should only be a + // single file that _does_ contain it) + if _, err := safetoken.Offset(file.Tok, obj.Pos()); err != nil { + continue + } + + for _, decl := range file.File.Decls { + decl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + + if info.ObjectOf(decl.Name) == obj { + return decl.Type, decl.Body + } + } + } + return nil, nil +} + +var ( + reTest = regexp.MustCompile(`^Test([A-Z]|$)`) + reBenchmark = regexp.MustCompile(`^Benchmark([A-Z]|$)`) + reFuzz = regexp.MustCompile(`^Fuzz([A-Z]|$)`) + reExample = regexp.MustCompile(`^Example([A-Z]|$)`) +) + +// isTestOrExample reports whether the given func is a testing func or an +// example func (or neither). isTestOrExample returns (true, false) for testing +// funcs, (false, true) for example funcs, and (false, false) otherwise. +func isTestOrExample(fn *types.Func) (isTest, isExample bool) { + sig := fn.Type().(*types.Signature) + if sig.Params().Len() == 0 && + sig.Results().Len() == 0 { + return false, reExample.MatchString(fn.Name()) + } + + kind, ok := testKind(sig) + if !ok { + return false, false + } + switch kind.Name() { + case "T": + return reTest.MatchString(fn.Name()), false + case "B": + return reBenchmark.MatchString(fn.Name()), false + case "F": + return reFuzz.MatchString(fn.Name()), false + default: + return false, false // "can't happen" (see testKind) + } +} + +// testKind returns the parameter type TypeName of a test, benchmark, or fuzz +// function (one of testing.[TBF]). +func testKind(sig *types.Signature) (*types.TypeName, bool) { + if sig.Params().Len() != 1 || + sig.Results().Len() != 0 { + return nil, false + } + + ptr, ok := sig.Params().At(0).Type().(*types.Pointer) + if !ok { + return nil, false + } + + named, ok := ptr.Elem().(*types.Named) + if !ok || named.Obj().Pkg().Path() != "testing" { + return nil, false + } + + switch named.Obj().Name() { + case "T", "B", "F": + return named.Obj(), true + } + return nil, false +} + +// An indexBuilder builds an index for a single package. +type indexBuilder struct { + gobPackage + fileIndex map[protocol.DocumentURI]int + subNames map[string]int +} + +// -- serial format of index -- + +// (The name says gob but in fact we use frob.) +var packageCodec = frob.CodecFor[gobPackage]() + +// A gobPackage records the test set of each package-level type for a single package. +type gobPackage struct { + Files []gobFile +} + +type gobFile struct { + Tests []gobTest +} + +// A gobTest records the name, type, and position of a single test. +type gobTest struct { + Location protocol.Location // location of the test + Name string // name of the test +} + +func (t *gobTest) result() Result { + return Result(*t) +} diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go index 7ff3e7b0c8b..93612a763fb 100644 --- a/gopls/internal/cache/view.go +++ b/gopls/internal/cache/view.go @@ -20,18 +20,19 @@ import ( "path" "path/filepath" "regexp" + "slices" "sort" "strings" "sync" "time" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/typerefs" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/pathutil" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" @@ -106,6 +107,9 @@ type View struct { importsState *importsState + // pkgIndex is an index of package IDs, for efficient storage of typerefs. + pkgIndex *typerefs.PackageIndex + // parseCache holds an LRU cache of recently parsed files. parseCache *parseCache @@ -253,7 +257,7 @@ func viewDefinitionsEqual(x, y *viewDefinition) bool { if x.workspaceModFilesErr.Error() != y.workspaceModFilesErr.Error() { return false } - } else if !maps.SameKeys(x.workspaceModFiles, y.workspaceModFiles) { + } else if !moremaps.SameKeys(x.workspaceModFiles, y.workspaceModFiles) { return false } if len(x.envOverlay) != len(y.envOverlay) { @@ -698,7 +702,7 @@ func (s *Snapshot) initialize(ctx context.Context, firstAttempt bool) { extractedDiags := s.extractGoCommandErrors(ctx, loadErr) initialErr = &InitializationError{ MainError: loadErr, - Diagnostics: maps.Group(extractedDiags, byURI), + Diagnostics: moremaps.Group(extractedDiags, byURI), } case s.view.workspaceModFilesErr != nil: initialErr = &InitializationError{ diff --git a/gopls/internal/cache/xrefs/xrefs.go b/gopls/internal/cache/xrefs/xrefs.go index b29b80aebf2..4113e08716e 100644 --- a/gopls/internal/cache/xrefs/xrefs.go +++ b/gopls/internal/cache/xrefs/xrefs.go @@ -18,7 +18,6 @@ import ( "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/frob" - "golang.org/x/tools/gopls/internal/util/typesutil" ) // Index constructs a serializable index of outbound cross-references @@ -93,8 +92,8 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { case *ast.ImportSpec: // Report a reference from each import path // string to the imported package. - pkgname, ok := typesutil.ImportedPkgName(info, n) - if !ok { + pkgname := info.PkgNameOf(n) + if pkgname == nil { return true // missing import } objects := getObjects(pkgname.Imported()) diff --git a/gopls/internal/cmd/capabilities_test.go b/gopls/internal/cmd/capabilities_test.go index 97eb49652d0..e1cc11bf408 100644 --- a/gopls/internal/cmd/capabilities_test.go +++ b/gopls/internal/cmd/capabilities_test.go @@ -104,6 +104,9 @@ func TestCapabilities(t *testing.T) { TextDocument: protocol.TextDocumentIdentifier{ URI: uri, }, + Context: protocol.CodeActionContext{ + Only: []protocol.CodeActionKind{protocol.SourceOrganizeImports}, + }, }) if err != nil { t.Fatal(err) diff --git a/gopls/internal/cmd/check.go b/gopls/internal/cmd/check.go index a859ed2c708..d256fa9de2a 100644 --- a/gopls/internal/cmd/check.go +++ b/gopls/internal/cmd/check.go @@ -8,10 +8,10 @@ import ( "context" "flag" "fmt" + "slices" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/slices" ) // check implements the check verb for gopls. diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index 80149f435ea..4afac6a7aff 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go @@ -365,6 +365,13 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti params.Capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true} params.Capabilities.TextDocument.SemanticTokens.TokenTypes = protocol.SemanticTypes() params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = protocol.SemanticModifiers() + params.Capabilities.TextDocument.CodeAction = protocol.CodeActionClientCapabilities{ + CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{ + CodeActionKind: protocol.ClientCodeActionKindOptions{ + ValueSet: []protocol.CodeActionKind{protocol.Empty}, // => all + }, + }, + } params.Capabilities.Window.WorkDoneProgress = true params.InitializationOptions = map[string]interface{}{ @@ -823,13 +830,10 @@ func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTok } func (c *connection) diagnoseFiles(ctx context.Context, files []protocol.DocumentURI) error { - cmd, err := command.NewDiagnoseFilesCommand("Diagnose files", command.DiagnoseFilesArgs{ + cmd := command.NewDiagnoseFilesCommand("Diagnose files", command.DiagnoseFilesArgs{ Files: files, }) - if err != nil { - return err - } - _, err = c.executeCommand(ctx, &cmd) + _, err := c.executeCommand(ctx, cmd) return err } diff --git a/gopls/internal/cmd/codeaction.go b/gopls/internal/cmd/codeaction.go index cb82e951b41..c349c7ab653 100644 --- a/gopls/internal/cmd/codeaction.go +++ b/gopls/internal/cmd/codeaction.go @@ -9,10 +9,10 @@ import ( "flag" "fmt" "regexp" + "slices" "strings" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/tool" ) @@ -47,21 +47,34 @@ The -kind flag specifies a comma-separated list of LSP CodeAction kinds. Only actions of these kinds will be requested from the server. Valid kinds include: + gopls.doc.features quickfix refactor refactor.extract + refactor.extract.function + refactor.extract.method + refactor.extract.toNewFile + refactor.extract.variable refactor.inline + refactor.inline.call refactor.rewrite - source.organizeImports - source.fixAll + refactor.rewrite.changeQuote + refactor.rewrite.fillStruct + refactor.rewrite.fillSwitch + refactor.rewrite.invertIf + refactor.rewrite.joinLines + refactor.rewrite.removeUnusedParam + refactor.rewrite.splitLines + source source.assembly source.doc + source.fixAll source.freesymbols - goTest - gopls.doc.features + source.organizeImports + source.test Kinds are hierarchical, so "refactor" includes "refactor.inline". -(Note: actions of kind "goTest" are not returned unless explicitly +(Note: actions of kind "source.test" are not returned unless explicitly requested.) The -title flag specifies a regular expression that must match the @@ -131,6 +144,8 @@ func (cmd *codeaction) Run(ctx context.Context, args ...string) error { for _, kind := range strings.Split(cmd.Kind, ",") { kinds = append(kinds, protocol.CodeActionKind(kind)) } + } else { + kinds = append(kinds, protocol.Empty) // => all } actions, err := conn.CodeAction(ctx, &protocol.CodeActionParams{ TextDocument: protocol.TextDocumentIdentifier{URI: uri}, diff --git a/gopls/internal/cmd/execute.go b/gopls/internal/cmd/execute.go index e2b3650a6e6..96b3cf3b81d 100644 --- a/gopls/internal/cmd/execute.go +++ b/gopls/internal/cmd/execute.go @@ -11,11 +11,11 @@ import ( "fmt" "log" "os" + "slices" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/server" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/imports.go b/gopls/internal/cmd/imports.go index c64c871e390..b0f67590748 100644 --- a/gopls/internal/cmd/imports.go +++ b/gopls/internal/cmd/imports.go @@ -59,15 +59,15 @@ func (t *imports) Run(ctx context.Context, args ...string) error { TextDocument: protocol.TextDocumentIdentifier{ URI: uri, }, + Context: protocol.CodeActionContext{ + Only: []protocol.CodeActionKind{protocol.SourceOrganizeImports}, + }, }) if err != nil { return fmt.Errorf("%v: %v", from, err) } var edits []protocol.TextEdit for _, a := range actions { - if a.Title != "Organize Imports" { - continue - } for _, c := range a.Edit.DocumentChanges { // This code action should affect only the specified file; // it is safe to ignore others. diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index 0bc066b02e0..39698f37334 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -48,7 +48,7 @@ import ( "golang.org/x/tools/txtar" ) -// TestVersion tests the 'version' subcommand (../info.go). +// TestVersion tests the 'version' subcommand (info.go). func TestVersion(t *testing.T) { t.Parallel() @@ -84,7 +84,7 @@ func TestVersion(t *testing.T) { } } -// TestCheck tests the 'check' subcommand (../check.go). +// TestCheck tests the 'check' subcommand (check.go). func TestCheck(t *testing.T) { t.Parallel() @@ -143,7 +143,7 @@ var C int } } -// TestCallHierarchy tests the 'call_hierarchy' subcommand (../call_hierarchy.go). +// TestCallHierarchy tests the 'call_hierarchy' subcommand (call_hierarchy.go). func TestCallHierarchy(t *testing.T) { t.Parallel() @@ -186,7 +186,7 @@ func h() { } } -// TestCodeLens tests the 'codelens' subcommand (../codelens.go). +// TestCodeLens tests the 'codelens' subcommand (codelens.go). func TestCodeLens(t *testing.T) { t.Parallel() @@ -238,7 +238,7 @@ func TestFail(t *testing.T) { t.Fatal("fail") } } } -// TestDefinition tests the 'definition' subcommand (../definition.go). +// TestDefinition tests the 'definition' subcommand (definition.go). func TestDefinition(t *testing.T) { t.Parallel() @@ -289,7 +289,7 @@ func g() { } } -// TestExecute tests the 'execute' subcommand (../execute.go). +// TestExecute tests the 'execute' subcommand (execute.go). func TestExecute(t *testing.T) { t.Parallel() @@ -363,7 +363,7 @@ func TestHello(t *testing.T) { } } -// TestFoldingRanges tests the 'folding_ranges' subcommand (../folding_range.go). +// TestFoldingRanges tests the 'folding_ranges' subcommand (folding_range.go). func TestFoldingRanges(t *testing.T) { t.Parallel() @@ -393,7 +393,7 @@ func f(x int) { } } -// TestFormat tests the 'format' subcommand (../format.go). +// TestFormat tests the 'format' subcommand (format.go). func TestFormat(t *testing.T) { t.Parallel() @@ -453,7 +453,7 @@ func f() {} } } -// TestHighlight tests the 'highlight' subcommand (../highlight.go). +// TestHighlight tests the 'highlight' subcommand (highlight.go). func TestHighlight(t *testing.T) { t.Parallel() @@ -482,7 +482,7 @@ func f() { } } -// TestImplementations tests the 'implementation' subcommand (../implementation.go). +// TestImplementations tests the 'implementation' subcommand (implementation.go). func TestImplementations(t *testing.T) { t.Parallel() @@ -511,7 +511,7 @@ func (T) String() string { return "" } } } -// TestImports tests the 'imports' subcommand (../imports.go). +// TestImports tests the 'imports' subcommand (imports.go). func TestImports(t *testing.T) { t.Parallel() @@ -560,7 +560,7 @@ func _() { } } -// TestLinks tests the 'links' subcommand (../links.go). +// TestLinks tests the 'links' subcommand (links.go). func TestLinks(t *testing.T) { t.Parallel() @@ -605,7 +605,7 @@ func f() {} } } -// TestReferences tests the 'references' subcommand (../references.go). +// TestReferences tests the 'references' subcommand (references.go). func TestReferences(t *testing.T) { t.Parallel() @@ -643,7 +643,7 @@ func g() { } } -// TestSignature tests the 'signature' subcommand (../signature.go). +// TestSignature tests the 'signature' subcommand (signature.go). func TestSignature(t *testing.T) { t.Parallel() @@ -674,7 +674,7 @@ func f() { } } -// TestPrepareRename tests the 'prepare_rename' subcommand (../prepare_rename.go). +// TestPrepareRename tests the 'prepare_rename' subcommand (prepare_rename.go). func TestPrepareRename(t *testing.T) { t.Parallel() @@ -713,7 +713,7 @@ func oldname() {} } } -// TestRename tests the 'rename' subcommand (../rename.go). +// TestRename tests the 'rename' subcommand (rename.go). func TestRename(t *testing.T) { t.Parallel() @@ -759,7 +759,7 @@ func oldname() {} } } -// TestSymbols tests the 'symbols' subcommand (../symbols.go). +// TestSymbols tests the 'symbols' subcommand (symbols.go). func TestSymbols(t *testing.T) { t.Parallel() @@ -790,7 +790,7 @@ const c = 0 } } -// TestSemtok tests the 'semtok' subcommand (../semantictokens.go). +// TestSemtok tests the 'semtok' subcommand (semantictokens.go). func TestSemtok(t *testing.T) { t.Parallel() @@ -941,7 +941,7 @@ package foo } } -// TestCodeAction tests the 'codeaction' subcommand (../codeaction.go). +// TestCodeAction tests the 'codeaction' subcommand (codeaction.go). func TestCodeAction(t *testing.T) { t.Parallel() @@ -988,6 +988,17 @@ type C struct{} t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) } } + // list code actions in file, filtering (hierarchically) by kind + { + res := gopls(t, tree, "codeaction", "-kind=source", "a.go") + res.checkExit(true) + got := res.stdout + want := `command "Browse documentation for package a" [source.doc]` + + "\n" + if got != want { + t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) + } + } // list code actions at position (of io.Reader) { res := gopls(t, tree, "codeaction", "b.go:#31") @@ -1029,7 +1040,7 @@ func (c C) Read(p []byte) (n int, err error) { } } -// TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (../workspace_symbol.go). +// TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (workspace_symbol.go). func TestWorkspaceSymbol(t *testing.T) { t.Parallel() diff --git a/gopls/internal/cmd/usage/codeaction.hlp b/gopls/internal/cmd/usage/codeaction.hlp index edc6a3e8f99..6d6923ef458 100644 --- a/gopls/internal/cmd/usage/codeaction.hlp +++ b/gopls/internal/cmd/usage/codeaction.hlp @@ -18,21 +18,34 @@ The -kind flag specifies a comma-separated list of LSP CodeAction kinds. Only actions of these kinds will be requested from the server. Valid kinds include: + gopls.doc.features quickfix refactor refactor.extract + refactor.extract.function + refactor.extract.method + refactor.extract.toNewFile + refactor.extract.variable refactor.inline + refactor.inline.call refactor.rewrite - source.organizeImports - source.fixAll + refactor.rewrite.changeQuote + refactor.rewrite.fillStruct + refactor.rewrite.fillSwitch + refactor.rewrite.invertIf + refactor.rewrite.joinLines + refactor.rewrite.removeUnusedParam + refactor.rewrite.splitLines + source source.assembly source.doc + source.fixAll source.freesymbols - goTest - gopls.doc.features + source.organizeImports + source.test Kinds are hierarchical, so "refactor" includes "refactor.inline". -(Note: actions of kind "goTest" are not returned unless explicitly +(Note: actions of kind "source.test" are not returned unless explicitly requested.) The -title flag specifies a regular expression that must match the diff --git a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp index 21c124eef97..64f99a3387e 100644 --- a/gopls/internal/cmd/usage/usage-v.hlp +++ b/gopls/internal/cmd/usage/usage-v.hlp @@ -67,6 +67,8 @@ flags: port on which to run gopls for debugging purposes -profile.alloc=string write alloc profile to this file + -profile.block=string + write block profile to this file -profile.cpu=string write CPU profile to this file -profile.mem=string diff --git a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp index 9ee0077ac95..c801a467626 100644 --- a/gopls/internal/cmd/usage/usage.hlp +++ b/gopls/internal/cmd/usage/usage.hlp @@ -64,6 +64,8 @@ flags: port on which to run gopls for debugging purposes -profile.alloc=string write alloc profile to this file + -profile.block=string + write block profile to this file -profile.cpu=string write CPU profile to this file -profile.mem=string diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 322707dd085..b076abd26b0 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -43,7 +43,7 @@ { "Name": "templateExtensions", "Type": "[]string", - "Doc": "templateExtensions gives the extensions of file names that are treateed\nas template files. (The extension\nis the part of the file name after the final dot.)\n", + "Doc": "templateExtensions gives the extensions of file names that are treated\nas template files. (The extension\nis the part of the file name after the final dot.)\n", "EnumKeys": { "ValueType": "", "Keys": null @@ -567,11 +567,6 @@ "Doc": "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.", "Default": "true" }, - { - "Name": "\"stubmethods\"", - "Doc": "detect missing methods and fix with stub implementations\n\nThis analyzer detects type-checking errors due to missing methods\nin assignments from concrete types to interface types, and offers\na suggested fix that will create a set of stub methods so that\nthe concrete type satisfies the interface.\n\nFor example, this function will not compile because the value\nNegativeErr{} does not implement the \"error\" interface:\n\n\tfunc sqrt(x float64) (float64, error) {\n\t\tif x \u003c 0 {\n\t\t\treturn 0, NegativeErr{} // error: missing method\n\t\t}\n\t\t...\n\t}\n\n\ttype NegativeErr struct{}\n\nThis analyzer will suggest a fix to declare this method:\n\n\t// Error implements error.Error.\n\tfunc (NegativeErr) Error() string {\n\t\tpanic(\"unimplemented\")\n\t}\n\n(At least, it appears to behave that way, but technically it\ndoesn't use the SuggestedFix mechanism and the stub is created by\nlogic in gopls's golang.stub function.)", - "Default": "true" - }, { "Name": "\"testinggoroutine\"", "Doc": "report calls to (*testing.T).Fatal from goroutines started by a test\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t go func() {\n\t t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t }()\n\t}", @@ -1238,12 +1233,6 @@ "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/structtag", "Default": true }, - { - "Name": "stubmethods", - "Doc": "detect missing methods and fix with stub implementations\n\nThis analyzer detects type-checking errors due to missing methods\nin assignments from concrete types to interface types, and offers\na suggested fix that will create a set of stub methods so that\nthe concrete type satisfies the interface.\n\nFor example, this function will not compile because the value\nNegativeErr{} does not implement the \"error\" interface:\n\n\tfunc sqrt(x float64) (float64, error) {\n\t\tif x \u003c 0 {\n\t\t\treturn 0, NegativeErr{} // error: missing method\n\t\t}\n\t\t...\n\t}\n\n\ttype NegativeErr struct{}\n\nThis analyzer will suggest a fix to declare this method:\n\n\t// Error implements error.Error.\n\tfunc (NegativeErr) Error() string {\n\t\tpanic(\"unimplemented\")\n\t}\n\n(At least, it appears to behave that way, but technically it\ndoesn't use the SuggestedFix mechanism and the stub is created by\nlogic in gopls's golang.stub function.)", - "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods", - "Default": true - }, { "Name": "testinggoroutine", "Doc": "report calls to (*testing.T).Fatal from goroutines started by a test\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t go func() {\n\t t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t }()\n\t}", diff --git a/gopls/internal/fuzzy/symbol_test.go b/gopls/internal/fuzzy/symbol_test.go index 7204aa6b9a9..99e2152cef3 100644 --- a/gopls/internal/fuzzy/symbol_test.go +++ b/gopls/internal/fuzzy/symbol_test.go @@ -100,7 +100,8 @@ func TestMatcherSimilarities(t *testing.T) { idents := collectIdentifiers(t) t.Logf("collected %d unique identifiers", len(idents)) - // TODO: use go1.21 slices.MaxFunc. + // We can't use slices.MaxFunc because we want a custom + // scoring (not equivalence) function. topMatch := func(score func(string) float64) string { top := "" topScore := 0.0 diff --git a/gopls/internal/golang/change_quote.go b/gopls/internal/golang/change_quote.go index e20b1ea88fb..67f29430700 100644 --- a/gopls/internal/golang/change_quote.go +++ b/gopls/internal/golang/change_quote.go @@ -11,8 +11,6 @@ import ( "strings" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/gopls/internal/cache/parsego" - "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" @@ -25,22 +23,22 @@ import ( // Only the following conditions are true, the action in result is valid // - [start, end) is enclosed by a string literal // - if the string is interpreted string, need check whether the convert is allowed -func convertStringLiteral(pgf *parsego.File, fh file.Handle, startPos, endPos token.Pos) (protocol.CodeAction, bool) { - path, _ := astutil.PathEnclosingInterval(pgf.File, startPos, endPos) +func convertStringLiteral(req *codeActionsRequest) { + path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end) lit, ok := path[0].(*ast.BasicLit) if !ok || lit.Kind != token.STRING { - return protocol.CodeAction{}, false + return } str, err := strconv.Unquote(lit.Value) if err != nil { - return protocol.CodeAction{}, false + return } interpreted := lit.Value[0] == '"' // Not all "..." strings can be represented as `...` strings. if interpreted && !strconv.CanBackquote(strings.ReplaceAll(str, "\n", "")) { - return protocol.CodeAction{}, false + return } var ( @@ -55,24 +53,20 @@ func convertStringLiteral(pgf *parsego.File, fh file.Handle, startPos, endPos to newText = strconv.Quote(str) } - start, end, err := safetoken.Offsets(pgf.Tok, lit.Pos(), lit.End()) + start, end, err := safetoken.Offsets(req.pgf.Tok, lit.Pos(), lit.End()) if err != nil { bug.Reportf("failed to get string literal offset by token.Pos:%v", err) - return protocol.CodeAction{}, false + return } edits := []diff.Edit{{ Start: start, End: end, New: newText, }} - textedits, err := protocol.EditsFromDiffEdits(pgf.Mapper, edits) + textedits, err := protocol.EditsFromDiffEdits(req.pgf.Mapper, edits) if err != nil { bug.Reportf("failed to convert diff.Edit to protocol.TextEdit:%v", err) - return protocol.CodeAction{}, false + return } - return protocol.CodeAction{ - Title: title, - Kind: protocol.RefactorRewrite, - Edit: protocol.NewWorkspaceEdit(protocol.DocumentChangeEdit(fh, textedits)), - }, true + req.addEditAction(title, nil, protocol.DocumentChangeEdit(req.fh, textedits)) } diff --git a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go index a4aab3e16b0..f0a5500b57f 100644 --- a/gopls/internal/golang/code_lens.go +++ b/gopls/internal/golang/code_lens.go @@ -48,21 +48,15 @@ func runTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand } puri := fh.URI() for _, fn := range testFuncs { - cmd, err := command.NewTestCommand("run test", puri, []string{fn.name}, nil) - if err != nil { - return nil, err - } + cmd := command.NewTestCommand("run test", puri, []string{fn.name}, nil) rng := protocol.Range{Start: fn.rng.Start, End: fn.rng.Start} - codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) + codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) } for _, fn := range benchFuncs { - cmd, err := command.NewTestCommand("run benchmark", puri, nil, []string{fn.name}) - if err != nil { - return nil, err - } + cmd := command.NewTestCommand("run benchmark", puri, nil, []string{fn.name}) rng := protocol.Range{Start: fn.rng.Start, End: fn.rng.Start} - codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) + codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) } if len(benchFuncs) > 0 { @@ -79,11 +73,8 @@ func runTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand for _, fn := range benchFuncs { benches = append(benches, fn.name) } - cmd, err := command.NewTestCommand("run file benchmarks", puri, nil, benches) - if err != nil { - return nil, err - } - codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) + cmd := command.NewTestCommand("run file benchmarks", puri, nil, benches) + codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) } return codeLens, nil } @@ -129,7 +120,7 @@ func matchTestFunc(fn *ast.FuncDecl, info *types.Info, nameRe *regexp.Regexp, pa if !ok { return false } - sig := obj.Type().(*types.Signature) + sig := obj.Signature() // Test functions should have only one parameter. if sig.Params().Len() != 1 { return false @@ -171,17 +162,11 @@ func goGenerateCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.H return nil, err } dir := fh.URI().Dir() - nonRecursiveCmd, err := command.NewGenerateCommand("run go generate", command.GenerateArgs{Dir: dir, Recursive: false}) - if err != nil { - return nil, err - } - recursiveCmd, err := command.NewGenerateCommand("run go generate ./...", command.GenerateArgs{Dir: dir, Recursive: true}) - if err != nil { - return nil, err - } + nonRecursiveCmd := command.NewGenerateCommand("run go generate", command.GenerateArgs{Dir: dir, Recursive: false}) + recursiveCmd := command.NewGenerateCommand("run go generate ./...", command.GenerateArgs{Dir: dir, Recursive: true}) return []protocol.CodeLens{ - {Range: rng, Command: &recursiveCmd}, - {Range: rng, Command: &nonRecursiveCmd}, + {Range: rng, Command: recursiveCmd}, + {Range: rng, Command: nonRecursiveCmd}, }, nil } @@ -208,11 +193,8 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha return nil, err } puri := fh.URI() - cmd, err := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri}) - if err != nil { - return nil, err - } - return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil + cmd := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri}) + return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil } func toggleDetailsCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { @@ -229,9 +211,6 @@ func toggleDetailsCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh fil return nil, err } puri := fh.URI() - cmd, err := command.NewGCDetailsCommand("Toggle gc annotation details", puri) - if err != nil { - return nil, err - } - return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil + cmd := command.NewGCDetailsCommand("Toggle gc annotation details", puri) + return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil } diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 31d036bdf40..3c916628a1a 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -9,7 +9,11 @@ import ( "encoding/json" "fmt" "go/ast" + "go/token" "go/types" + "reflect" + "slices" + "sort" "strings" "golang.org/x/tools/go/ast/astutil" @@ -18,191 +22,319 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/golang/stubmethods" "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/gopls/internal/util/slices" + "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typesinternal" ) -// CodeActions returns all wanted code actions (edits and other +// CodeActions returns all enabled code actions (edits and other // commands) available for the selected range. // // Depending on how the request was triggered, fewer actions may be // offered, e.g. to avoid UI distractions after mere cursor motion. // // See ../protocol/codeactionkind.go for some code action theory. -func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, want map[protocol.CodeActionKind]bool, trigger protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) { - // Only compute quick fixes if there are any diagnostics to fix. - wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0 - - // Note: don't forget to update the allow-list in Server.CodeAction - // when adding new query operations like GoTest and GoDoc that - // are permitted even in generated source files - - // Code actions that can be offered based on syntax information alone. - if wantQuickFixes || - want[protocol.SourceOrganizeImports] || - want[protocol.RefactorExtract] || - want[settings.GoFreeSymbols] || - want[settings.GoplsDocFeatures] { - - pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) - if err != nil { - return nil, err - } - - // Process any missing imports and pair them with the diagnostics they fix. - if wantQuickFixes || want[protocol.SourceOrganizeImports] { - importEdits, importEditsPerFix, err := allImportsFixes(ctx, snapshot, pgf) - if err != nil { - event.Error(ctx, "imports fixes", err, label.File.Of(fh.URI().Path())) - importEdits = nil - importEditsPerFix = nil - } - - // Separate this into a set of codeActions per diagnostic, where - // each action is the addition, removal, or renaming of one import. - if wantQuickFixes { - for _, importFix := range importEditsPerFix { - fixed := fixedByImportFix(importFix.fix, diagnostics) - if len(fixed) == 0 { - continue - } - actions = append(actions, protocol.CodeAction{ - Title: importFixTitle(importFix.fix), - Kind: protocol.QuickFix, - Edit: protocol.NewWorkspaceEdit( - protocol.DocumentChangeEdit(fh, importFix.edits)), - Diagnostics: fixed, - }) - } - } - - // Send all of the import edits as one code action if the file is - // being organized. - if want[protocol.SourceOrganizeImports] && len(importEdits) > 0 { - actions = append(actions, protocol.CodeAction{ - Title: "Organize Imports", - Kind: protocol.SourceOrganizeImports, - Edit: protocol.NewWorkspaceEdit( - protocol.DocumentChangeEdit(fh, importEdits)), - }) - } - } +func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool, trigger protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) { - if want[protocol.RefactorExtract] { - extractions, err := getExtractCodeActions(pgf, rng, snapshot.Options()) - if err != nil { - return nil, err - } - actions = append(actions, extractions...) - } + loc := protocol.Location{URI: fh.URI(), Range: rng} - if want[settings.GoFreeSymbols] && rng.End != rng.Start { - loc := protocol.Location{URI: pgf.URI, Range: rng} - cmd, err := command.NewFreeSymbolsCommand("Browse free symbols", snapshot.View().ID(), loc) - if err != nil { - return nil, err - } - // For implementation, see commandHandler.FreeSymbols. - actions = append(actions, protocol.CodeAction{ - Title: cmd.Title, - Kind: settings.GoFreeSymbols, - Command: &cmd, - }) - } + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) + if err != nil { + return nil, err + } + start, end, err := pgf.RangePos(rng) + if err != nil { + return nil, err + } - if want[settings.GoplsDocFeatures] { - // TODO(adonovan): after the docs are published in gopls/v0.17.0, - // use the gopls release tag instead of master. - cmd, err := command.NewClientOpenURLCommand( - "Browse gopls feature documentation", - "https://github.com/golang/tools/blob/master/gopls/doc/features/README.md") - if err != nil { - return nil, err + // Scan to see if any enabled producer needs type information. + var enabledMemo [len(codeActionProducers)]bool + needTypes := false + for i, p := range codeActionProducers { + if enabled(p.kind) { + enabledMemo[i] = true + if p.needPkg { + needTypes = true } - actions = append(actions, protocol.CodeAction{ - Title: cmd.Title, - Kind: settings.GoplsDocFeatures, - Command: &cmd, - }) } } - // Code actions requiring type information. - if want[protocol.RefactorRewrite] || - want[protocol.RefactorInline] || - want[settings.GoAssembly] || - want[settings.GoDoc] || - want[settings.GoTest] { - pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) + // Compute type information if needed. + // Also update pgf, start, end to be consistent with pkg. + // They may differ in case of parse cache miss. + var pkg *cache.Package + if needTypes { + var err error + pkg, pgf, err = NarrowestPackageForFile(ctx, snapshot, loc.URI) if err != nil { return nil, err } - start, end, err := pgf.RangePos(rng) + start, end, err = pgf.RangePos(loc.Range) if err != nil { return nil, err } + } - if want[protocol.RefactorRewrite] { - rewrites, err := getRewriteCodeActions(ctx, pkg, snapshot, pgf, fh, rng, snapshot.Options()) - if err != nil { - return nil, err - } - actions = append(actions, rewrites...) + // Execute each enabled producer function. + req := &codeActionsRequest{ + actions: &actions, + lazy: make(map[reflect.Type]any), + snapshot: snapshot, + fh: fh, + pgf: pgf, + loc: loc, + start: start, + end: end, + diagnostics: diagnostics, + trigger: trigger, + pkg: pkg, + } + for i, p := range codeActionProducers { + if !enabledMemo[i] { + continue + } + req.kind = p.kind + if p.needPkg { + req.pkg = pkg + } else { + req.pkg = nil + } + if err := p.fn(ctx, req); err != nil { + return nil, err } + } - // To avoid distraction (e.g. VS Code lightbulb), offer "inline" - // only after a selection or explicit menu operation. - if want[protocol.RefactorInline] && (trigger != protocol.CodeActionAutomatic || rng.Start != rng.End) { - rewrites, err := getInlineCodeActions(pkg, pgf, rng, snapshot.Options()) - if err != nil { - return nil, err - } - actions = append(actions, rewrites...) + sort.Slice(actions, func(i, j int) bool { + return actions[i].Kind < actions[j].Kind + }) + + return actions, nil +} + +// A codeActionsRequest is passed to each function +// that produces code actions. +type codeActionsRequest struct { + // internal fields for use only by [CodeActions]. + actions *[]protocol.CodeAction // pointer to output slice; call addAction to populate + lazy map[reflect.Type]any // lazy construction + + // inputs to the producer function: + kind protocol.CodeActionKind + snapshot *cache.Snapshot + fh file.Handle + pgf *parsego.File + loc protocol.Location + start, end token.Pos + diagnostics []protocol.Diagnostic + trigger protocol.CodeActionTriggerKind + pkg *cache.Package // set only if producer.needPkg +} + +// addApplyFixAction adds an ApplyFix command-based CodeAction to the result. +func (req *codeActionsRequest) addApplyFixAction(title, fix string, loc protocol.Location) { + cmd := command.NewApplyFixCommand(title, command.ApplyFixArgs{ + Fix: fix, + Location: loc, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd, true) +} + +// addCommandAction adds a CodeAction to the result based on the provided command. +// +// If allowResolveEdits (and the client supports codeAction/resolve) +// then the command is embedded into the code action data field so +// that the client can later ask the server to "resolve" a command +// into an edit that they can preview and apply selectively. +// Set allowResolveEdits only for actions that generate edits. +// +// Otherwise, the command is set as the code action operation. +func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command, allowResolveEdits bool) { + act := protocol.CodeAction{ + Title: cmd.Title, + Kind: req.kind, + } + if allowResolveEdits && req.resolveEdits() { + data, err := json.Marshal(cmd) + if err != nil { + panic("unable to marshal") } + msg := json.RawMessage(data) + act.Data = &msg + } else { + act.Command = cmd + } + req.addAction(act) +} - if want[settings.GoTest] { - fixes, err := getGoTestCodeActions(pkg, pgf, rng) - if err != nil { - return nil, err - } - actions = append(actions, fixes...) +// addCommandAction adds an edit-based CodeAction to the result. +func (req *codeActionsRequest) addEditAction(title string, fixedDiagnostics []protocol.Diagnostic, changes ...protocol.DocumentChange) { + req.addAction(protocol.CodeAction{ + Title: title, + Kind: req.kind, + Diagnostics: fixedDiagnostics, + Edit: protocol.NewWorkspaceEdit(changes...), + }) +} + +// addAction adds a code action to the response. +func (req *codeActionsRequest) addAction(act protocol.CodeAction) { + *req.actions = append(*req.actions, act) +} + +// resolveEdits reports whether the client can resolve edits lazily. +func (req *codeActionsRequest) resolveEdits() bool { + opts := req.snapshot.Options() + return opts.CodeActionResolveOptions != nil && + slices.Contains(opts.CodeActionResolveOptions, "edit") +} + +// lazyInit[*T](ctx, req) returns a pointer to an instance of T, +// calling new(T).init(ctx.req) on the first request. +// +// It is conceptually a (generic) method of req. +func lazyInit[P interface { + init(ctx context.Context, req *codeActionsRequest) + *T +}, T any](ctx context.Context, req *codeActionsRequest) P { + t := reflect.TypeFor[T]() + v, ok := req.lazy[t].(P) + if !ok { + v = new(T) + v.init(ctx, req) + req.lazy[t] = v + } + return v +} + +// -- producers -- + +// A codeActionProducer describes a function that produces CodeActions +// of a particular kind. +// The function is only called if that kind is enabled. +type codeActionProducer struct { + kind protocol.CodeActionKind + fn func(ctx context.Context, req *codeActionsRequest) error + needPkg bool // fn needs type information (req.pkg) +} + +var codeActionProducers = [...]codeActionProducer{ + {kind: protocol.QuickFix, fn: quickFix, needPkg: true}, + {kind: protocol.SourceOrganizeImports, fn: sourceOrganizeImports}, + {kind: settings.GoAssembly, fn: goAssembly, needPkg: true}, + {kind: settings.GoDoc, fn: goDoc, needPkg: true}, + {kind: settings.GoFreeSymbols, fn: goFreeSymbols}, + {kind: settings.GoTest, fn: goTest}, + {kind: settings.GoplsDocFeatures, fn: goplsDocFeatures}, + {kind: settings.RefactorExtractFunction, fn: refactorExtractFunction}, + {kind: settings.RefactorExtractMethod, fn: refactorExtractMethod}, + {kind: settings.RefactorExtractToNewFile, fn: refactorExtractToNewFile}, + {kind: settings.RefactorExtractVariable, fn: refactorExtractVariable}, + {kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true}, + {kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote}, + {kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true}, + {kind: settings.RefactorRewriteFillSwitch, fn: refactorRewriteFillSwitch, needPkg: true}, + {kind: settings.RefactorRewriteInvertIf, fn: refactorRewriteInvertIf}, + {kind: settings.RefactorRewriteJoinLines, fn: refactorRewriteJoinLines, needPkg: true}, + {kind: settings.RefactorRewriteRemoveUnusedParam, fn: refactorRewriteRemoveUnusedParam, needPkg: true}, + {kind: settings.RefactorRewriteSplitLines, fn: refactorRewriteSplitLines, needPkg: true}, + + // Note: don't forget to update the allow-list in Server.CodeAction + // when adding new query operations like GoTest and GoDoc that + // are permitted even in generated source files. +} + +// sourceOrganizeImports produces "Organize Imports" code actions. +func sourceOrganizeImports(ctx context.Context, req *codeActionsRequest) error { + res := lazyInit[*allImportsFixesResult](ctx, req) + + // Send all of the import edits as one code action + // if the file is being organized. + if len(res.allFixEdits) > 0 { + req.addEditAction("Organize Imports", nil, protocol.DocumentChangeEdit(req.fh, res.allFixEdits)) + } + + return nil +} + +// quickFix produces code actions that fix errors, +// for example by adding/deleting/renaming imports, +// or declaring the missing methods of a type. +func quickFix(ctx context.Context, req *codeActionsRequest) error { + // Only compute quick fixes if there are any diagnostics to fix. + if len(req.diagnostics) == 0 { + return nil + } + + // Process any missing imports and pair them with the diagnostics they fix. + res := lazyInit[*allImportsFixesResult](ctx, req) + if res.err != nil { + return nil + } + + // Separate this into a set of codeActions per diagnostic, where + // each action is the addition, removal, or renaming of one import. + for _, importFix := range res.editsPerFix { + fixedDiags := fixedByImportFix(importFix.fix, req.diagnostics) + if len(fixedDiags) == 0 { + continue } + req.addEditAction(importFixTitle(importFix.fix), fixedDiags, protocol.DocumentChangeEdit(req.fh, importFix.edits)) + } - if want[settings.GoDoc] { - // "Browse documentation for ..." - _, _, title := DocFragment(pkg, pgf, start, end) - loc := protocol.Location{URI: pgf.URI, Range: rng} - cmd, err := command.NewDocCommand(title, loc) - if err != nil { - return nil, err - } - actions = append(actions, protocol.CodeAction{ - Title: cmd.Title, - Kind: settings.GoDoc, - Command: &cmd, - }) + // Quick fixes for type errors. + info := req.pkg.TypesInfo() + for _, typeError := range req.pkg.TypeErrors() { + // Does type error overlap with CodeAction range? + start, end := typeError.Pos, typeError.Pos + if _, _, endPos, ok := typesinternal.ReadGo116ErrorData(typeError); ok { + end = endPos + } + typeErrorRange, err := req.pgf.PosRange(start, end) + if err != nil || !protocol.Intersect(typeErrorRange, req.loc.Range) { + continue } - if want[settings.GoAssembly] { - fixes, err := getGoAssemblyAction(snapshot.View(), pkg, pgf, rng) - if err != nil { - return nil, err + // "Missing method" error? (stubmethods) + // Offer a "Declare missing methods of INTERFACE" code action. + // See [stubMethodsFixer] for command implementation. + msg := typeError.Error() + if strings.Contains(msg, "missing method") || + strings.HasPrefix(msg, "cannot convert") || + strings.Contains(msg, "not implement") { + path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end) + si := stubmethods.GetStubInfo(req.pkg.FileSet(), info, path, start) + if si != nil { + qf := typesutil.FileQualifier(req.pgf.File, si.Concrete.Obj().Pkg(), info) + iface := types.TypeString(si.Interface.Type(), qf) + msg := fmt.Sprintf("Declare missing methods of %s", iface) + req.addApplyFixAction(msg, fixStubMethods, req.loc) } - actions = append(actions, fixes...) } } - return actions, nil + + return nil +} + +// allImportsFixesResult is the result of a lazy call to allImportsFixes. +// It implements the codeActionsRequest lazyInit interface. +type allImportsFixesResult struct { + allFixEdits []protocol.TextEdit + editsPerFix []*importFix + err error } -func supportsResolveEdits(options *settings.Options) bool { - return options.CodeActionResolveOptions != nil && slices.Contains(options.CodeActionResolveOptions, "edit") +func (res *allImportsFixesResult) init(ctx context.Context, req *codeActionsRequest) { + res.allFixEdits, res.editsPerFix, res.err = allImportsFixes(ctx, req.snapshot, req.pgf) + if res.err != nil { + event.Error(ctx, "imports fixes", res.err, label.File.Of(req.loc.URI.Path())) + } } func importFixTitle(fix *imports.ImportFix) string { @@ -255,201 +387,151 @@ func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) return results } -// getExtractCodeActions returns any refactor.extract code actions for the selection. -func getExtractCodeActions(pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err - } - puri := pgf.URI - var commands []protocol.Command - if _, ok, methodOk, _ := canExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok { - cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ - Fix: fixExtractFunction, - URI: puri, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err - } - commands = append(commands, cmd) - if methodOk { - cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ - Fix: fixExtractMethod, - URI: puri, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err - } - commands = append(commands, cmd) - } - } - if _, _, ok, _ := canExtractVariable(start, end, pgf.File); ok { - cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ - Fix: fixExtractVariable, - URI: puri, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err - } - commands = append(commands, cmd) - } - if canExtractToNewFile(pgf, start, end) { - cmd, err := command.NewExtractToNewFileCommand( - "Extract declarations to new file", - protocol.Location{URI: pgf.URI, Range: rng}, - ) - if err != nil { - return nil, err - } - commands = append(commands, cmd) - } - var actions []protocol.CodeAction - for i := range commands { - actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorExtract, &commands[i], nil, options)) +// goFreeSymbols produces "Browse free symbols" code actions. +// See [server.commandHandler.FreeSymbols] for command implementation. +func goFreeSymbols(ctx context.Context, req *codeActionsRequest) error { + if !req.loc.Empty() { + cmd := command.NewFreeSymbolsCommand("Browse free symbols", req.snapshot.View().ID(), req.loc) + req.addCommandAction(cmd, false) } - return actions, nil + return nil } -func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Command, diagnostics []protocol.Diagnostic, options *settings.Options) protocol.CodeAction { - action := protocol.CodeAction{ - Title: title, - Kind: kind, - Diagnostics: diagnostics, - } - if !supportsResolveEdits(options) { - action.Command = cmd - } else { - data, err := json.Marshal(cmd) - if err != nil { - panic("unable to marshal") - } - msg := json.RawMessage(data) - action.Data = &msg - } - return action +// goplsDocFeatures produces "Browse gopls feature documentation" code actions. +// See [server.commandHandler.ClientOpenURL] for command implementation. +func goplsDocFeatures(ctx context.Context, req *codeActionsRequest) error { + // TODO(adonovan): after the docs are published in gopls/v0.17.0, + // use the gopls release tag instead of master. + cmd := command.NewClientOpenURLCommand( + "Browse gopls feature documentation", + "https://github.com/golang/tools/blob/master/gopls/doc/features/README.md") + req.addCommandAction(cmd, false) + return nil } -func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *cache.Snapshot, pgf *parsego.File, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { - // golang/go#61693: code actions were refactored to run outside of the - // analysis framework, but as a result they lost their panic recovery. - // - // These code actions should never fail, but put back the panic recovery as a - // defensive measure. - defer func() { - if r := recover(); r != nil { - rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r) - } - }() +// goDoc produces "Browse documentation for X" code actions. +// See [server.commandHandler.Doc] for command implementation. +func goDoc(ctx context.Context, req *codeActionsRequest) error { + _, _, title := DocFragment(req.pkg, req.pgf, req.start, req.end) + cmd := command.NewDocCommand(title, command.DocArgs{Location: req.loc, ShowDocument: true}) + req.addCommandAction(cmd, false) + return nil +} - var actions []protocol.CodeAction +// refactorExtractFunction produces "Extract function" code actions. +// See [extractFunction] for command implementation. +func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error { + if _, ok, _, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok { + req.addApplyFixAction("Extract function", fixExtractFunction, req.loc) + } + return nil +} - if canRemoveParameter(pkg, pgf, rng) { - cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{ - RemoveParameter: protocol.Location{ - URI: pgf.URI, - Range: rng, - }, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err - } - actions = append(actions, newCodeAction("Refactor: remove unused parameter", protocol.RefactorRewrite, &cmd, nil, options)) +// refactorExtractMethod produces "Extract method" code actions. +// See [extractMethod] for command implementation. +func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error { + if _, ok, methodOK, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok && methodOK { + req.addApplyFixAction("Extract method", fixExtractMethod, req.loc) } + return nil +} - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err +// refactorExtractVariable produces "Extract variable" code actions. +// See [extractVariable] for command implementation. +func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error { + if _, _, ok, _ := canExtractVariable(req.start, req.end, req.pgf.File); ok { + req.addApplyFixAction("Extract variable", fixExtractVariable, req.loc) } + return nil +} - if action, ok := convertStringLiteral(pgf, fh, start, end); ok { - actions = append(actions, action) +// refactorExtractToNewFile produces "Extract declarations to new file" code actions. +// See [server.commandHandler.ExtractToNewFile] for command implementation. +func refactorExtractToNewFile(ctx context.Context, req *codeActionsRequest) error { + if canExtractToNewFile(req.pgf, req.start, req.end) { + cmd := command.NewExtractToNewFileCommand("Extract declarations to new file", req.loc) + req.addCommandAction(cmd, true) } + return nil +} - var commands []protocol.Command - if _, ok, _ := canInvertIfCondition(pgf.File, start, end); ok { - cmd, err := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{ - Fix: fixInvertIfCondition, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), +// refactorRewriteRemoveUnusedParam produces "Remove unused parameter" code actions. +// See [server.commandHandler.ChangeSignature] for command implementation. +func refactorRewriteRemoveUnusedParam(ctx context.Context, req *codeActionsRequest) error { + if canRemoveParameter(req.pkg, req.pgf, req.loc.Range) { + cmd := command.NewChangeSignatureCommand("Refactor: remove unused parameter", command.ChangeSignatureArgs{ + RemoveParameter: req.loc, + ResolveEdits: req.resolveEdits(), }) - if err != nil { - return nil, err - } - commands = append(commands, cmd) + req.addCommandAction(cmd, true) } + return nil +} - if msg, ok, _ := canSplitLines(pgf.File, pkg.FileSet(), start, end); ok { - cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ - Fix: fixSplitLines, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err - } - commands = append(commands, cmd) +// refactorRewriteChangeQuote produces "Convert to {raw,interpreted} string literal" code actions. +func refactorRewriteChangeQuote(ctx context.Context, req *codeActionsRequest) error { + convertStringLiteral(req) + return nil +} + +// refactorRewriteChangeQuote produces "Invert 'if' condition" code actions. +// See [invertIfCondition] for command implementation. +func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error { + if _, ok, _ := canInvertIfCondition(req.pgf.File, req.start, req.end); ok { + req.addApplyFixAction("Invert 'if' condition", fixInvertIfCondition, req.loc) } + return nil +} - if msg, ok, _ := canJoinLines(pgf.File, pkg.FileSet(), start, end); ok { - cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ - Fix: fixJoinLines, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err - } - commands = append(commands, cmd) +// refactorRewriteSplitLines produces "Split ITEMS into separate lines" code actions. +// See [splitLines] for command implementation. +func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) error { + // TODO(adonovan): opt: don't set needPkg just for FileSet. + if msg, ok, _ := canSplitLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok { + req.addApplyFixAction(msg, fixSplitLines, req.loc) + } + return nil +} + +// refactorRewriteJoinLines produces "Join ITEMS into one line" code actions. +// See [joinLines] for command implementation. +func refactorRewriteJoinLines(ctx context.Context, req *codeActionsRequest) error { + // TODO(adonovan): opt: don't set needPkg just for FileSet. + if msg, ok, _ := canJoinLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok { + req.addApplyFixAction(msg, fixJoinLines, req.loc) } + return nil +} +// refactorRewriteFillStruct produces "Fill STRUCT" code actions. +// See [fillstruct.SuggestedFix] for command implementation. +func refactorRewriteFillStruct(ctx context.Context, req *codeActionsRequest) error { // fillstruct.Diagnose is a lazy analyzer: all it gives us is // the (start, end, message) of each SuggestedFix; the actual // edit is computed only later by ApplyFix, which calls fillstruct.SuggestedFix. - for _, diag := range fillstruct.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) { - rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End) + for _, diag := range fillstruct.Diagnose(req.pgf.File, req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) { + loc, err := req.pgf.Mapper.PosLocation(req.pgf.Tok, diag.Pos, diag.End) if err != nil { - return nil, err + return err } for _, fix := range diag.SuggestedFixes { - cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ - Fix: diag.Category, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err - } - commands = append(commands, cmd) + req.addApplyFixAction(fix.Message, diag.Category, loc) } } + return nil +} - for _, diag := range fillswitch.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) { - changes, err := suggestedFixToDocumentChange(ctx, snapshot, pkg.FileSet(), &diag.SuggestedFixes[0]) +// refactorRewriteFillSwitch produces "Add cases for TYPE/ENUM" code actions. +func refactorRewriteFillSwitch(ctx context.Context, req *codeActionsRequest) error { + for _, diag := range fillswitch.Diagnose(req.pgf.File, req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) { + changes, err := suggestedFixToDocumentChange(ctx, req.snapshot, req.pkg.FileSet(), &diag.SuggestedFixes[0]) if err != nil { - return nil, err + return err } - actions = append(actions, protocol.CodeAction{ - Title: diag.Message, - Kind: protocol.RefactorRewrite, - Edit: protocol.NewWorkspaceEdit(changes...), - }) - } - for i := range commands { - actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorRewrite, &commands[i], nil, options)) + req.addEditAction(diag.Message, nil, changes...) } - return actions, nil + return nil } // canRemoveParameter reports whether we can remove the function parameter @@ -462,6 +544,8 @@ func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *ca // // (Note that the unusedparam analyzer also computes this property, but // much more precisely, allowing it to report its findings as diagnostics.) +// +// TODO(adonovan): inline into refactorRewriteRemoveUnusedParam. func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) bool { if perrors, terrors := pkg.ParseErrors(), pkg.TypeErrors(); len(perrors) > 0 || len(terrors) > 0 { return false // can't remove parameters from packages with errors @@ -501,76 +585,56 @@ func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Rang return !used } -// getInlineCodeActions returns refactor.inline actions available at the specified range. -func getInlineCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err +// refactorInlineCall produces "Inline call to FUNC" code actions. +// See [inlineCall] for command implementation. +func refactorInlineCall(ctx context.Context, req *codeActionsRequest) error { + // To avoid distraction (e.g. VS Code lightbulb), offer "inline" + // only after a selection or explicit menu operation. + // TODO(adonovan): remove this (and req.trigger); see comment at TestVSCodeIssue65167. + if req.trigger == protocol.CodeActionAutomatic && req.loc.Empty() { + return nil } // If range is within call expression, offer to inline the call. - var commands []protocol.Command - if _, fn, err := enclosingStaticCall(pkg, pgf, start, end); err == nil { - cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ - Fix: fixInlineCall, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err - } - commands = append(commands, cmd) - } - - // Convert commands to actions. - var actions []protocol.CodeAction - for i := range commands { - actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorInline, &commands[i], nil, options)) + if _, fn, err := enclosingStaticCall(req.pkg, req.pgf, req.start, req.end); err == nil { + req.addApplyFixAction("Inline call to "+fn.Name(), fixInlineCall, req.loc) } - return actions, nil + return nil } -// getGoTestCodeActions returns any "run this test/benchmark" code actions for the selection. -func getGoTestCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) { - testFuncs, benchFuncs, err := testsAndBenchmarks(pkg.TypesInfo(), pgf) +// goTest produces "Run tests and benchmarks" code actions. +// See [server.commandHandler.runTests] for command implementation. +func goTest(ctx context.Context, req *codeActionsRequest) error { + testFuncs, benchFuncs, err := testsAndBenchmarks(req.pkg.TypesInfo(), req.pgf) if err != nil { - return nil, err + return err } var tests, benchmarks []string for _, fn := range testFuncs { - if protocol.Intersect(fn.rng, rng) { + if protocol.Intersect(fn.rng, req.loc.Range) { tests = append(tests, fn.name) } } for _, fn := range benchFuncs { - if protocol.Intersect(fn.rng, rng) { + if protocol.Intersect(fn.rng, req.loc.Range) { benchmarks = append(benchmarks, fn.name) } } if len(tests) == 0 && len(benchmarks) == 0 { - return nil, nil + return nil } - cmd, err := command.NewTestCommand("Run tests and benchmarks", pgf.URI, tests, benchmarks) - if err != nil { - return nil, err - } - return []protocol.CodeAction{{ - Title: cmd.Title, - Kind: settings.GoTest, - Command: &cmd, - }}, nil + cmd := command.NewTestCommand("Run tests and benchmarks", req.loc.URI, tests, benchmarks) + req.addCommandAction(cmd, false) + return nil } -// getGoAssemblyAction returns any "Browse assembly for f" code actions for the selection. -func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) { - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err - } +// goAssembly produces "Browse ARCH assembly for FUNC" code actions. +// See [server.commandHandler.Assembly] for command implementation. +func goAssembly(ctx context.Context, req *codeActionsRequest) error { + view := req.snapshot.View() // Find the enclosing toplevel function or method, // and compute its symbol name (e.g. "pkgpath.(T).method"). @@ -592,12 +656,11 @@ func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File // directly to (say) a lambda of interest. // Perhaps we could scroll to STEXT for the innermost // enclosing nested function? - var actions []protocol.CodeAction - path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) + path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end) if len(path) >= 2 { // [... FuncDecl File] if decl, ok := path[len(path)-2].(*ast.FuncDecl); ok { - if fn, ok := pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok { - sig := fn.Type().(*types.Signature) + if fn, ok := req.pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok { + sig := fn.Signature() // Compute the linker symbol of the enclosing function. var sym strings.Builder @@ -622,23 +685,15 @@ func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File if fn.Name() != "_" && // blank functions are not compiled (fn.Name() != "init" || sig.Recv() != nil) && // init functions aren't linker functions sig.TypeParams() == nil && sig.RecvTypeParams() == nil { // generic => no assembly - cmd, err := command.NewAssemblyCommand( + cmd := command.NewAssemblyCommand( fmt.Sprintf("Browse %s assembly for %s", view.GOARCH(), decl.Name), view.ID(), - string(pkg.Metadata().ID), + string(req.pkg.Metadata().ID), sym.String()) - if err != nil { - return nil, err - } - // For handler, see commandHandler.Assembly. - actions = append(actions, protocol.CodeAction{ - Title: cmd.Title, - Kind: settings.GoAssembly, - Command: &cmd, - }) + req.addCommandAction(cmd, false) } } } } - return actions, nil + return nil } diff --git a/gopls/internal/golang/comment.go b/gopls/internal/golang/comment.go index dc8c1c83f77..3a0d8153665 100644 --- a/gopls/internal/golang/comment.go +++ b/gopls/internal/golang/comment.go @@ -111,7 +111,7 @@ func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.O for _, idx := range docLinkRegex.FindAllStringSubmatchIndex(text, -1) { // The [idx[2], idx[3]) identifies the first submatch, - // which is the reference name in the doc link. + // which is the reference name in the doc link (sans '*'). // e.g. The "[fmt.Println]" reference name is "fmt.Println". if !(idx[2] <= lineOffset && lineOffset < idx[3]) { continue @@ -126,7 +126,7 @@ func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.O name = name[:i] i = strings.LastIndexByte(name, '.') } - obj := lookupObjectByName(pkg, pgf, name) + obj := lookupDocLinkSymbol(pkg, pgf, name) if obj == nil { return nil, protocol.Range{}, errNoCommentReference } @@ -141,19 +141,42 @@ func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.O return nil, protocol.Range{}, errNoCommentReference } -func lookupObjectByName(pkg *cache.Package, pgf *parsego.File, name string) types.Object { +// lookupDocLinkSymbol returns the symbol denoted by a doc link such +// as "fmt.Println" or "bytes.Buffer.Write" in the specified file. +func lookupDocLinkSymbol(pkg *cache.Package, pgf *parsego.File, name string) types.Object { scope := pkg.Types().Scope() + + prefix, suffix, _ := strings.Cut(name, ".") + + // Try treating the prefix as a package name, + // allowing for non-renaming and renaming imports. fileScope := pkg.TypesInfo().Scopes[pgf.File] - pkgName, suffix, _ := strings.Cut(name, ".") - obj, ok := fileScope.Lookup(pkgName).(*types.PkgName) - if ok { - scope = obj.Imported().Scope() + pkgname, ok := fileScope.Lookup(prefix).(*types.PkgName) // ok => prefix is imported name + if !ok { + // Handle renaming import, e.g. + // [path.Join] after import pathpkg "path". + // (Should we look at all files of the package?) + for _, imp := range pgf.File.Imports { + pkgname2 := pkg.TypesInfo().PkgNameOf(imp) + if pkgname2 != nil && pkgname2.Imported().Name() == prefix { + pkgname = pkgname2 + break + } + } + } + if pkgname != nil { + scope = pkgname.Imported().Scope() if suffix == "" { - return obj + return pkgname // not really a valid doc link } name = suffix } + // TODO(adonovan): try searching the forward closure for packages + // that define the symbol but are not directly imported; + // see https://github.com/golang/go/issues/61677 + + // Type.Method? recv, method, ok := strings.Cut(name, ".") if ok { obj, ok := scope.Lookup(recv).(*types.TypeName) @@ -173,5 +196,6 @@ func lookupObjectByName(pkg *cache.Package, pgf *parsego.File, name string) type return nil } + // package-level symbol return scope.Lookup(name) } diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 77c8b61615d..cf398693113 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -18,7 +18,7 @@ import ( "go/token" "go/types" "math" - "reflect" + "slices" "sort" "strconv" "strings" @@ -39,9 +39,7 @@ import ( "golang.org/x/tools/gopls/internal/settings" goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/util/typesutil" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/stdlib" @@ -198,7 +196,7 @@ type completer struct { // goversion is the version of Go in force in the file, as // defined by x/tools/internal/versions. Empty if unknown. - // TODO(adonovan): with go1.22+ it should always be known. + // Since go1.22 it should always be known. goversion string // (tokFile, pos) is the position at which the request was triggered. @@ -1037,7 +1035,7 @@ func (c *completer) populateCommentCompletions(comment *ast.CommentGroup) { // collect receiver struct fields if node.Recv != nil { - sig := c.pkg.TypesInfo().Defs[node.Name].(*types.Func).Type().(*types.Signature) + sig := c.pkg.TypesInfo().Defs[node.Name].(*types.Func).Signature() _, named := typesinternal.ReceiverNamed(sig.Recv()) // may be nil if ill-typed if named != nil { if recvStruct, ok := named.Underlying().(*types.Struct); ok { @@ -1395,12 +1393,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { return nil } - var goversion string - // TODO(adonovan): after go1.21, replace with: - // goversion = c.pkg.GetTypesInfo().FileVersions[c.file] - if v := reflect.ValueOf(c.pkg.TypesInfo()).Elem().FieldByName("FileVersions"); v.IsValid() { - goversion = v.Interface().(map[*ast.File]string)[c.file] // may be "" - } + goversion := c.pkg.TypesInfo().FileVersions[c.file] // Extract the package-level candidates using a quick parse. var g errgroup.Group @@ -1503,6 +1496,13 @@ func ignoreUnimportedCompletion(fix *imports.ImportFix) bool { } func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *importInfo, cb func(candidate)) { + if isStarTestingDotF(typ) { + // is that a sufficient test? (or is more care needed?) + if c.fuzz(typ, imp, cb) { + return + } + } + mset := c.methodSetCache[methodSetKey{typ, addressable}] if mset == nil { if addressable && !types.IsInterface(typ) && !isPointer(typ) { @@ -1515,13 +1515,6 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *impo c.methodSetCache[methodSetKey{typ, addressable}] = mset } - if isStarTestingDotF(typ) && addressable { - // is that a sufficient test? (or is more care needed?) - if c.fuzz(mset, imp, cb) { - return - } - } - for i := 0; i < mset.Len(); i++ { obj := mset.At(i).Obj() // to the other side of the cb() queue? @@ -1646,7 +1639,7 @@ func (c *completer) lexical(ctx context.Context) error { } if c.inference.objType != nil { - if named, ok := aliases.Unalias(typesinternal.Unpointer(c.inference.objType)).(*types.Named); ok { + if named, ok := types.Unalias(typesinternal.Unpointer(c.inference.objType)).(*types.Named); ok { // If we expected a named type, check the type's package for // completion items. This is useful when the current file hasn't // imported the type's package yet. @@ -1720,7 +1713,7 @@ func (c *completer) injectType(ctx context.Context, t types.Type) { // considered via a lexical search, so we need to directly inject // them. Also allow generic types since lexical search does not // infer instantiated versions of them. - if named, ok := aliases.Unalias(t).(*types.Named); !ok || named.TypeParams().Len() > 0 { + if named, ok := types.Unalias(t).(*types.Named); !ok || named.TypeParams().Len() > 0 { // If our expected type is "[]int", this will add a literal // candidate of "[]int{}". c.literal(ctx, t, nil) @@ -1904,7 +1897,7 @@ func (c *completer) structLiteralFieldName(ctx context.Context) error { } // Add struct fields. - if t, ok := aliases.Unalias(clInfo.clType).(*types.Struct); ok { + if t, ok := types.Unalias(clInfo.clType).(*types.Struct); ok { const deltaScore = 0.0001 for i := 0; i < t.NumFields(); i++ { field := t.Field(i) @@ -3363,7 +3356,7 @@ func isSlice(obj types.Object) bool { // The AST position information is garbage. func forEachPackageMember(content []byte, f func(tok token.Token, id *ast.Ident, fn *ast.FuncDecl)) { purged := goplsastutil.PurgeFuncBodies(content) - file, _ := parser.ParseFile(token.NewFileSet(), "", purged, 0) + file, _ := parser.ParseFile(token.NewFileSet(), "", purged, parser.SkipObjectResolution) for _, decl := range file.Decls { switch decl := decl.(type) { case *ast.GenDecl: diff --git a/gopls/internal/golang/completion/format.go b/gopls/internal/golang/completion/format.go index dbc57c18082..c2b955ca7e9 100644 --- a/gopls/internal/golang/completion/format.go +++ b/gopls/internal/golang/completion/format.go @@ -17,7 +17,6 @@ import ( "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" ) @@ -61,7 +60,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e } if isTypeName(obj) && c.wantTypeParams() { x := cand.obj.(*types.TypeName) - if named, ok := aliases.Unalias(x.Type()).(*types.Named); ok { + if named, ok := types.Unalias(x.Type()).(*types.Named); ok { tp := named.TypeParams() label += golang.FormatTypeParams(tp) insert = label // maintain invariant above (label == insert) @@ -95,7 +94,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e break } case *types.Func: - if obj.Type().(*types.Signature).Recv() == nil { + if obj.Signature().Recv() == nil { kind = protocol.FunctionCompletion } else { kind = protocol.MethodCompletion @@ -412,8 +411,8 @@ func inferableTypeParams(sig *types.Signature) map[*types.TypeParam]bool { } case *types.TypeParam: free[t] = true - case *aliases.Alias: - visit(aliases.Unalias(t)) + case *types.Alias: + visit(types.Unalias(t)) case *types.Named: targs := t.TypeArgs() for i := 0; i < targs.Len(); i++ { diff --git a/gopls/internal/golang/completion/fuzz.go b/gopls/internal/golang/completion/fuzz.go index 313e7f7b391..3f5ac99c428 100644 --- a/gopls/internal/golang/completion/fuzz.go +++ b/gopls/internal/golang/completion/fuzz.go @@ -20,12 +20,14 @@ import ( // PJW: are there other packages where we can deduce usage constraints? // if we find fuzz completions, then return true, as those are the only completions to offer -func (c *completer) fuzz(mset *types.MethodSet, imp *importInfo, cb func(candidate)) bool { +func (c *completer) fuzz(testingF types.Type, imp *importInfo, cb func(candidate)) bool { // 1. inside f.Fuzz? (only f.Failed and f.Name) // 2. possible completing f.Fuzz? // [Ident,SelectorExpr,Callexpr,ExprStmt,BlockiStmt,FuncDecl(Fuzz...)] // 3. before f.Fuzz, same (for 2., offer choice when looking at an F) + mset := types.NewMethodSet(testingF) + // does the path contain FuncLit as arg to f.Fuzz CallExpr? inside := false Loop: diff --git a/gopls/internal/golang/completion/literal.go b/gopls/internal/golang/completion/literal.go index 62398f064c2..7427d559e94 100644 --- a/gopls/internal/golang/completion/literal.go +++ b/gopls/internal/golang/completion/literal.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typesinternal" ) @@ -55,9 +54,9 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im // TODO(adonovan): think about aliases: // they should probably be treated more like Named. // Should this use Deref not Unpointer? - if is[*types.Named](aliases.Unalias(literalType)) && + if is[*types.Named](types.Unalias(literalType)) && expType != nil && - !is[*types.Named](aliases.Unalias(typesinternal.Unpointer(expType))) { + !is[*types.Named](types.Unalias(typesinternal.Unpointer(expType))) { return } @@ -200,7 +199,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m name = p.Name() ) - if tp, _ := aliases.Unalias(p.Type()).(*types.TypeParam); tp != nil && !c.typeParamInScope(tp) { + if tp, _ := types.Unalias(p.Type()).(*types.TypeParam); tp != nil && !c.typeParamInScope(tp) { hasTypeParams = true } @@ -291,7 +290,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m typeStr = strings.Replace(typeStr, "[]", "...", 1) } - if tp, ok := aliases.Unalias(p.Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { + if tp, ok := types.Unalias(p.Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { snip.WritePlaceholder(func(snip *snippet.Builder) { snip.WriteText(typeStr) }) @@ -312,7 +311,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m var resultHasTypeParams bool for i := 0; i < results.Len(); i++ { - if tp, ok := aliases.Unalias(results.At(i).Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { + if tp, ok := types.Unalias(results.At(i).Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { resultHasTypeParams = true } } @@ -345,7 +344,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m } return } - if tp, ok := aliases.Unalias(r.Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { + if tp, ok := types.Unalias(r.Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { snip.WritePlaceholder(func(snip *snippet.Builder) { snip.WriteText(text) }) @@ -519,7 +518,7 @@ func (c *completer) typeNameSnippet(literalType types.Type, qf types.Qualifier) typeName string // TODO(adonovan): think more about aliases. // They should probably be treated more like Named. - named, _ = aliases.Unalias(literalType).(*types.Named) + named, _ = types.Unalias(literalType).(*types.Named) ) if named != nil && named.Obj() != nil && named.TypeParams().Len() > 0 && !c.fullyInstantiated(named) { @@ -567,7 +566,7 @@ func (c *completer) fullyInstantiated(t *types.Named) bool { for i := 0; i < tas.Len(); i++ { // TODO(adonovan) think about generic aliases. - switch ta := aliases.Unalias(tas.At(i)).(type) { + switch ta := types.Unalias(tas.At(i)).(type) { case *types.TypeParam: // A *TypeParam only counts as specified if it is currently in // scope (i.e. we are in a generic definition). diff --git a/gopls/internal/golang/completion/postfix_snippets.go b/gopls/internal/golang/completion/postfix_snippets.go index 641fe8746eb..d322775cc7f 100644 --- a/gopls/internal/golang/completion/postfix_snippets.go +++ b/gopls/internal/golang/completion/postfix_snippets.go @@ -21,7 +21,6 @@ import ( "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typesinternal" @@ -465,7 +464,7 @@ func (a *postfixTmplArgs) VarName(t types.Type, nonNamedDefault string) string { // go/types predicates are undefined on types.Typ[types.Invalid]. if !types.Identical(t, types.Typ[types.Invalid]) && types.Implements(t, errorIntf) { name = "err" - } else if !is[*types.Named](aliases.Unalias(typesinternal.Unpointer(t))) { + } else if !is[*types.Named](types.Unalias(typesinternal.Unpointer(t))) { name = nonNamedDefault } diff --git a/gopls/internal/golang/completion/util.go b/gopls/internal/golang/completion/util.go index ad4ee5e09fc..a13f5094839 100644 --- a/gopls/internal/golang/completion/util.go +++ b/gopls/internal/golang/completion/util.go @@ -13,7 +13,6 @@ import ( "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/typeparams" ) @@ -65,7 +64,7 @@ func eachField(T types.Type, fn func(*types.Var)) { func typeIsValid(typ types.Type) bool { // Check named types separately, because we don't want // to call Underlying() on them to avoid problems with recursive types. - if _, ok := aliases.Unalias(typ).(*types.Named); ok { + if _, ok := types.Unalias(typ).(*types.Named); ok { return true } @@ -140,7 +139,7 @@ func isPkgName(obj types.Object) bool { return is[*types.PkgName](obj) } // It returns false for a Named type whose Underlying is a Pointer. // // TODO(adonovan): shouldn't this use CoreType(T)? -func isPointer(T types.Type) bool { return is[*types.Pointer](aliases.Unalias(T)) } +func isPointer(T types.Type) bool { return is[*types.Pointer](types.Unalias(T)) } // isEmptyInterface whether T is a (possibly Named or Alias) empty interface // type, such that every type is assignable to T. @@ -156,7 +155,7 @@ func isEmptyInterface(T types.Type) bool { } func isUntyped(T types.Type) bool { - if basic, ok := aliases.Unalias(T).(*types.Basic); ok { + if basic, ok := types.Unalias(T).(*types.Basic); ok { return basic.Info()&types.IsUntyped > 0 } return false @@ -321,8 +320,8 @@ func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.Tex // assignableTo is like types.AssignableTo, but returns false if // either type is invalid. func assignableTo(x, to types.Type) bool { - if aliases.Unalias(x) == types.Typ[types.Invalid] || - aliases.Unalias(to) == types.Typ[types.Invalid] { + if types.Unalias(x) == types.Typ[types.Invalid] || + types.Unalias(to) == types.Typ[types.Invalid] { return false } @@ -332,8 +331,8 @@ func assignableTo(x, to types.Type) bool { // convertibleTo is like types.ConvertibleTo, but returns false if // either type is invalid. func convertibleTo(x, to types.Type) bool { - if aliases.Unalias(x) == types.Typ[types.Invalid] || - aliases.Unalias(to) == types.Typ[types.Invalid] { + if types.Unalias(x) == types.Typ[types.Invalid] || + types.Unalias(to) == types.Typ[types.Invalid] { return false } diff --git a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go index 438fe2a3949..f20fe85f541 100644 --- a/gopls/internal/golang/definition.go +++ b/gopls/internal/golang/definition.go @@ -12,12 +12,15 @@ import ( "go/parser" "go/token" "go/types" + "regexp" + "strings" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/event" ) @@ -92,6 +95,18 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p return builtinDefinition(ctx, snapshot, obj) } + // Non-go (e.g. assembly) symbols + // + // When already at the definition of a Go function without + // a body, we jump to its non-Go (C or assembly) definition. + for _, decl := range pgf.File.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok && + decl.Body == nil && + astutil.NodeContains(decl.Name, pos) { + return nonGoDefinition(ctx, snapshot, pkg, decl.Name.Name) + } + } + // Finally, map the object position. loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj)) if err != nil { @@ -353,3 +368,40 @@ func mapPosition(ctx context.Context, fset *token.FileSet, s file.Source, start, m := protocol.NewMapper(fh.URI(), content) return m.PosLocation(file, start, end) } + +// nonGoDefinition returns the location of the definition of a non-Go symbol. +// Only assembly is supported for now. +func nonGoDefinition(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string) ([]protocol.Location, error) { + // Examples: + // TEXT runtime·foo(SB) + // TEXT ·foo(SB) + // TODO(adonovan): why does ^TEXT cause it not to match? + pattern := regexp.MustCompile("TEXT\\b.*·(" + regexp.QuoteMeta(symbol) + ")[\\(<]") + + for _, uri := range pkg.Metadata().OtherFiles { + if strings.HasSuffix(uri.Path(), ".s") { + fh, err := snapshot.ReadFile(ctx, uri) + if err != nil { + return nil, err // context cancelled + } + content, err := fh.Content() + if err != nil { + continue // can't read file + } + if match := pattern.FindSubmatchIndex(content); match != nil { + mapper := protocol.NewMapper(uri, content) + loc, err := mapper.OffsetLocation(match[2], match[3]) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil + } + } + } + + // TODO(adonovan): try C files + + // This may be reached for functions that aren't implemented + // in assembly (e.g. compiler intrinsics like getg). + return nil, fmt.Errorf("can't find non-Go definition of %s", symbol) +} diff --git a/gopls/internal/golang/diagnostics.go b/gopls/internal/golang/diagnostics.go index 0dc5ae22aeb..1c6da2e9d4e 100644 --- a/gopls/internal/golang/diagnostics.go +++ b/gopls/internal/golang/diagnostics.go @@ -6,13 +6,15 @@ package golang import ( "context" + "maps" + "slices" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/progress" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" ) // Analyze reports go/analysis-framework diagnostics in the specified package. @@ -28,9 +30,9 @@ func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID return nil, ctx.Err() } - analyzers := maps.Values(settings.DefaultAnalyzers) + analyzers := slices.Collect(maps.Values(settings.DefaultAnalyzers)) if snapshot.Options().Staticcheck { - analyzers = append(analyzers, maps.Values(settings.StaticcheckAnalyzers)...) + analyzers = slices.AppendSeq(analyzers, maps.Values(settings.StaticcheckAnalyzers)) } analysisDiagnostics, err := snapshot.Analyze(ctx, pkgIDs, analyzers, tracker) @@ -38,5 +40,5 @@ func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID return nil, err } byURI := func(d *cache.Diagnostic) protocol.DocumentURI { return d.URI } - return maps.Group(analysisDiagnostics, byURI), nil + return moremaps.Group(analysisDiagnostics, byURI), nil } diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go index cc82a53f966..6ea011e220e 100644 --- a/gopls/internal/golang/extract.go +++ b/gopls/internal/golang/extract.go @@ -246,6 +246,10 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte if n.Pos() < start || n.End() > end { return n.Pos() <= end } + // exclude return statements in function literals because they don't affect the refactor. + if _, ok := n.(*ast.FuncLit); ok { + return false + } ret, ok := n.(*ast.ReturnStmt) if !ok { return true @@ -651,8 +655,12 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte }, nil } -// isSelector reports if e is the selector expr , . +// isSelector reports if e is the selector expr , . It works for pointer and non-pointer selector expressions. func isSelector(e ast.Expr, x, sel string) bool { + unary, ok := e.(*ast.UnaryExpr) + if ok && unary.Op == token.MUL { + e = unary.X + } selectorExpr, ok := e.(*ast.SelectorExpr) if !ok { return false @@ -666,9 +674,15 @@ func isSelector(e ast.Expr, x, sel string) bool { // reorderParams reorders the given parameters in-place to follow common Go conventions. func reorderParams(params []ast.Expr, paramTypes []*ast.Field) { + moveParamToFrontIfFound(params, paramTypes, "testing", "T") + moveParamToFrontIfFound(params, paramTypes, "testing", "B") + moveParamToFrontIfFound(params, paramTypes, "context", "Context") +} + +func moveParamToFrontIfFound(params []ast.Expr, paramTypes []*ast.Field, x, sel string) { // Move Context parameter (if any) to front. for i, t := range paramTypes { - if isSelector(t.Type, "context", "Context") { + if isSelector(t.Type, x, sel) { p, t := params[i], paramTypes[i] copy(params[1:], params[:i]) copy(paramTypes[1:], paramTypes[:i]) @@ -1151,7 +1165,7 @@ func varOverridden(info *types.Info, firstUse *ast.Ident, obj types.Object, isFr // file that represents the text. func parseBlockStmt(fset *token.FileSet, src []byte) (*ast.BlockStmt, error) { text := "package main\nfunc _() { " + string(src) + " }" - extract, err := parser.ParseFile(fset, "", text, 0) + extract, err := parser.ParseFile(fset, "", text, parser.SkipObjectResolution) if err != nil { return nil, err } diff --git a/gopls/internal/golang/extracttofile.go b/gopls/internal/golang/extracttofile.go index 9b3aad5bda3..0a1d74408d7 100644 --- a/gopls/internal/golang/extracttofile.go +++ b/gopls/internal/golang/extracttofile.go @@ -25,7 +25,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/typesutil" ) // canExtractToNewFile reports whether the code in the given range can be extracted to a new file. @@ -56,8 +55,8 @@ func findImportEdits(file *ast.File, info *types.Info, start, end token.Pos) (ad // TODO: support dot imports. return nil, nil, errors.New("\"extract to new file\" does not support files containing dot imports") } - pkgName, ok := typesutil.ImportedPkgName(info, spec) - if !ok { + pkgName := info.PkgNameOf(spec) + if pkgName == nil { continue } usedInSelection := false @@ -152,7 +151,7 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han return nil, fmt.Errorf("%s: %w", errorPrefix, err) } - fileStart := pgf.Tok.Pos(0) // TODO(adonovan): use go1.20 pgf.File.FileStart + fileStart := pgf.File.FileStart buf.Write(pgf.Src[start-fileStart : end-fileStart]) // TODO: attempt to duplicate the copyright header, if any. diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go index 3844fc0d65c..7c44aa4d273 100644 --- a/gopls/internal/golang/fix.go +++ b/gopls/internal/golang/fix.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/gopls/internal/analysis/embeddirective" "golang.org/x/tools/gopls/internal/analysis/fillstruct" - "golang.org/x/tools/gopls/internal/analysis/stubmethods" "golang.org/x/tools/gopls/internal/analysis/undeclaredname" "golang.org/x/tools/gopls/internal/analysis/unusedparams" "golang.org/x/tools/gopls/internal/cache" @@ -66,6 +65,7 @@ const ( fixInvertIfCondition = "invert_if_condition" fixSplitLines = "split_lines" fixJoinLines = "join_lines" + fixStubMethods = "stub_methods" ) // ApplyFix applies the specified kind of suggested fix to the given @@ -98,7 +98,6 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file // These match the Diagnostic.Category. embeddirective.FixCategory: addEmbedImport, fillstruct.FixCategory: singleFile(fillstruct.SuggestedFix), - stubmethods.FixCategory: stubMethodsFixer, undeclaredname.FixCategory: singleFile(undeclaredname.SuggestedFix), // Ad-hoc fixers: these are used when the command is @@ -110,6 +109,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file fixInvertIfCondition: singleFile(invertIfCondition), fixSplitLines: singleFile(splitLines), fixJoinLines: singleFile(joinLines), + fixStubMethods: stubMethodsFixer, } fixer, ok := fixers[fix] if !ok { diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index 5755f7ae2ea..8f735f38cf4 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go @@ -21,12 +21,12 @@ import ( "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/tokeninternal" + gofumptFormat "mvdan.cc/gofumpt/format" ) // Format formats a file with a given range. @@ -67,7 +67,7 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr // Apply additional formatting, if any is supported. Currently, the only // supported additional formatter is gofumpt. - if format := settings.GofumptFormat; snapshot.Options().Gofumpt && format != nil { + if snapshot.Options().Gofumpt { // gofumpt can customize formatting based on language version and module // path, if available. // @@ -76,15 +76,17 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr // TODO: under which circumstances can we fail to find module information? // Can this, for example, result in inconsistent formatting across saves, // due to pending calls to packages.Load? - var langVersion, modulePath string + var opts gofumptFormat.Options meta, err := NarrowestMetadataForFile(ctx, snapshot, fh.URI()) if err == nil { if mi := meta.Module; mi != nil { - langVersion = mi.GoVersion - modulePath = mi.Path + if v := mi.GoVersion; v != "" { + opts.LangVersion = "go" + v + } + opts.ModulePath = mi.Path } } - b, err := format(ctx, langVersion, modulePath, buf.Bytes()) + b, err := gofumptFormat.Source(buf.Bytes(), opts) if err != nil { return nil, err } diff --git a/gopls/internal/golang/freesymbols.go b/gopls/internal/golang/freesymbols.go index f09975d759a..0e2422d421b 100644 --- a/gopls/internal/golang/freesymbols.go +++ b/gopls/internal/golang/freesymbols.go @@ -13,6 +13,7 @@ import ( "go/token" "go/types" "html" + "slices" "sort" "strings" @@ -20,9 +21,8 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/typesinternal" ) @@ -119,11 +119,7 @@ func FreeSymbolsHTML(viewID string, pkg *cache.Package, pgf *parsego.File, start // Imported symbols. // Produce one record per package, with a list of symbols. - pkgPaths := maps.Keys(imported) - sort.Strings(pkgPaths) - for _, pkgPath := range pkgPaths { - refs := imported[pkgPath] - + for pkgPath, refs := range moremaps.Sorted(imported) { var syms []string for _, ref := range refs { // strip package name (bytes.Buffer.Len -> Buffer.Len) @@ -218,7 +214,7 @@ p { max-width: 6in; } pos := start emitTo := func(end token.Pos) { if pos < end { - fileStart := pgf.Tok.Pos(0) // TODO(adonovan): use go1.20 pgf.File.FileStart + fileStart := pgf.File.FileStart text := pgf.Mapper.Content[pos-fileStart : end-fileStart] buf.WriteString(html.EscapeString(string(text))) pos = end diff --git a/gopls/internal/golang/freesymbols_test.go b/gopls/internal/golang/freesymbols_test.go index 9ad8ca3ee6e..8885c32dbbc 100644 --- a/gopls/internal/golang/freesymbols_test.go +++ b/gopls/internal/golang/freesymbols_test.go @@ -98,7 +98,7 @@ func TestFreeRefs(t *testing.T) { test.src[startOffset+len("«"):endOffset] + " " + test.src[endOffset+len("»"):] - f, err := parser.ParseFile(fset, name, src, 0) + f, err := parser.ParseFile(fset, name, src, parser.SkipObjectResolution) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index 863c09f7974..f53e73f3053 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/event" ) @@ -85,7 +84,7 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa highlight(imp) // ...and all references to it in the file. - if pkgname, ok := typesutil.ImportedPkgName(info, imp); ok { + if pkgname := info.PkgNameOf(imp); pkgname != nil { ast.Inspect(file, func(n ast.Node) bool { if id, ok := n.(*ast.Ident); ok && info.Uses[id] == pkgname { @@ -559,6 +558,11 @@ func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result highlightWriteInExpr(n.Chan) case *ast.CompositeLit: t := info.TypeOf(n) + // Every expression should have a type; + // work around https://github.com/golang/go/issues/69092. + if t == nil { + t = types.Typ[types.Invalid] + } if ptr, ok := t.Underlying().(*types.Pointer); ok { t = ptr.Elem() } @@ -586,8 +590,8 @@ func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result highlightIdent(n, protocol.Text) } case *ast.ImportSpec: - pkgname, ok := typesutil.ImportedPkgName(info, n) - if ok && pkgname == obj { + pkgname := info.PkgNameOf(n) + if pkgname == obj { if n.Name != nil { highlightNode(result, n.Name, protocol.Text) } else { diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index b315b7383d4..a0622fd764e 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -36,9 +36,7 @@ import ( gastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/util/typesutil" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/tokeninternal" @@ -382,7 +380,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro // (2) we lose inline comments // Furthermore, we include a summary of their method set. _, isTypeName := obj.(*types.TypeName) - _, isTypeParam := aliases.Unalias(obj.Type()).(*types.TypeParam) + _, isTypeParam := types.Unalias(obj.Type()).(*types.TypeParam) if isTypeName && !isTypeParam { spec, ok := spec.(*ast.TypeSpec) if !ok { @@ -536,7 +534,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro var recv types.Object switch obj := obj.(type) { case *types.Func: - sig := obj.Type().(*types.Signature) + sig := obj.Signature() if sig.Recv() != nil { tname := typeToObject(sig.Recv().Type()) if tname != nil { // beware typed nil @@ -963,7 +961,7 @@ func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file // specifically, we show the receiver name, // and replace the period in (T).f by a space (#62190). - sig := obj.Type().(*types.Signature) + sig := obj.Signature() var buf bytes.Buffer buf.WriteString("func ") @@ -1025,7 +1023,7 @@ func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file } // Special formatting cases. - switch typ := aliases.Unalias(obj.Type()).(type) { + switch typ := types.Unalias(obj.Type()).(type) { case *types.Named: // Try to add a formatted duration as an inline comment. pkg := typ.Obj().Pkg() @@ -1111,6 +1109,10 @@ func chooseDocComment(decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.Comme // pos; the resulting File and Pos may belong to the same or a // different FileSet, such as one synthesized by the parser cache, if // parse-caching is enabled. +// +// TODO(adonovan): change this function to accept a filename and a +// byte offset, and eliminate the confusing (fset, pos) parameters. +// Then simplify stubmethods.StubInfo, which doesn't need a Fset. func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, pos token.Pos) (*parsego.File, token.Pos, error) { f := fset.File(pos) if f == nil { @@ -1186,11 +1188,13 @@ func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path Packa if h.stdVersion == nil || *h.stdVersion == stdlib.Version(0) { parts[5] = "" // suppress stdlib version if not applicable or initial version 1.0 } - parts = slices.Remove(parts, "") var b strings.Builder - for i, part := range parts { - if i > 0 { + for _, part := range parts { + if part == "" { + continue + } + if b.Len() > 0 { if options.PreferredContentFormat == protocol.Markdown { b.WriteString("\n\n") } else { @@ -1235,7 +1239,7 @@ func StdSymbolOf(obj types.Object) *stdlib.Symbol { // Handle Method. if fn, _ := obj.(*types.Func); fn != nil { - isPtr, named := typesinternal.ReceiverNamed(fn.Type().(*types.Signature).Recv()) + isPtr, named := typesinternal.ReceiverNamed(fn.Signature().Recv()) if isPackageLevel(named.Obj()) { for _, s := range symbols { if s.Kind != stdlib.Method { diff --git a/gopls/internal/golang/identifier.go b/gopls/internal/golang/identifier.go index 30a83d3a05a..813b3261f87 100644 --- a/gopls/internal/golang/identifier.go +++ b/gopls/internal/golang/identifier.go @@ -9,7 +9,6 @@ import ( "go/ast" "go/types" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) @@ -22,7 +21,7 @@ var ErrNoIdentFound = errors.New("no identifier found") // If no such signature exists, it returns nil. func inferredSignature(info *types.Info, id *ast.Ident) *types.Signature { inst := info.Instances[id] - sig, _ := aliases.Unalias(inst.Type).(*types.Signature) + sig, _ := types.Unalias(inst.Type).(*types.Signature) return sig } @@ -41,7 +40,7 @@ func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { // Keep track of the last exported type seen. var exported *types.TypeName - if named, ok := aliases.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { + if named, ok := types.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { exported = named.Obj() } // We don't want the last element, as that's the field or @@ -49,7 +48,7 @@ func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { for _, index := range sel.Index()[:len(sel.Index())-1] { if r, ok := recv.Underlying().(*types.Struct); ok { recv = typesinternal.Unpointer(r.Field(index).Type()) - if named, ok := aliases.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { + if named, ok := types.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { exported = named.Obj() } } @@ -66,7 +65,7 @@ func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { // a single non-error result, and ignoring built-in named types. func typeToObject(typ types.Type) *types.TypeName { switch typ := typ.(type) { - case *aliases.Alias: + case *types.Alias: return typ.Obj() case *types.Named: // TODO(rfindley): this should use typeparams.NamedTypeOrigin. diff --git a/gopls/internal/golang/identifier_test.go b/gopls/internal/golang/identifier_test.go index b1e6d5a75a2..d78d8fe99f5 100644 --- a/gopls/internal/golang/identifier_test.go +++ b/gopls/internal/golang/identifier_test.go @@ -46,7 +46,7 @@ func TestSearchForEnclosing(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { fset := token.NewFileSet() - file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors) + file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors|parser.SkipObjectResolution) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/golang/implementation.go b/gopls/internal/golang/implementation.go index 72679ad7176..b3accff452f 100644 --- a/gopls/internal/golang/implementation.go +++ b/gopls/internal/golang/implementation.go @@ -126,7 +126,7 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand return obj.Type(), "" case *types.Func: // For methods, use the receiver type, which may be anonymous. - if recv := obj.Type().(*types.Signature).Recv(); recv != nil { + if recv := obj.Signature().Recv(); recv != nil { return recv.Type(), obj.Id() } } @@ -317,7 +317,7 @@ func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.D case *types.TypeName: // ok case *types.Func: - if obj.Type().(*types.Signature).Recv() == nil { + if obj.Signature().Recv() == nil { return nil, nil, fmt.Errorf("%s is a function, not a method", id.Name) } case nil: diff --git a/gopls/internal/golang/invertifcondition.go b/gopls/internal/golang/invertifcondition.go index 16eaaa39bd2..0fb7d1e4d0a 100644 --- a/gopls/internal/golang/invertifcondition.go +++ b/gopls/internal/golang/invertifcondition.go @@ -240,7 +240,7 @@ func invertAndOr(fset *token.FileSet, expr *ast.BinaryExpr, src []byte) ([]byte, } // canInvertIfCondition reports whether we can do invert-if-condition on the -// code in the given range +// code in the given range. func canInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) { path, _ := astutil.PathEnclosingInterval(file, start, end) for _, node := range path { diff --git a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go index 24239941a2c..6a17e928b34 100644 --- a/gopls/internal/golang/lines.go +++ b/gopls/internal/golang/lines.go @@ -13,13 +13,13 @@ import ( "go/ast" "go/token" "go/types" + "slices" "sort" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" ) // canSplitLines checks whether we can split lists of elements inside diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go index 8f6b636027d..ed8f1b388f0 100644 --- a/gopls/internal/golang/pkgdoc.go +++ b/gopls/internal/golang/pkgdoc.go @@ -39,7 +39,9 @@ import ( "go/token" "go/types" "html" + "iter" "path/filepath" + "slices" "strings" "golang.org/x/tools/go/ast/astutil" @@ -49,8 +51,6 @@ import ( goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" - "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/typesinternal" ) @@ -120,7 +120,7 @@ func DocFragment(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (p if !sym.Exported() { // Unexported method of exported type? if fn, ok := sym.(*types.Func); ok { - if recv := fn.Type().(*types.Signature).Recv(); recv != nil { + if recv := fn.Signature().Recv(); recv != nil { _, named := typesinternal.ReceiverNamed(recv) if named != nil && named.Obj().Exported() { sym = named.Obj() @@ -147,7 +147,7 @@ func DocFragment(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (p // Inv: sym is field or method, or local. switch sym := sym.(type) { case *types.Func: // => method - sig := sym.Type().(*types.Signature) + sig := sym.Signature() isPtr, named := typesinternal.ReceiverNamed(sig.Recv()) if named != nil { if !named.Obj().Exported() { @@ -199,7 +199,7 @@ func thingAtPoint(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) t // In an import spec? if len(path) >= 3 { // [...ImportSpec GenDecl File] if spec, ok := path[len(path)-3].(*ast.ImportSpec); ok { - if pkgname, ok := typesutil.ImportedPkgName(pkg.TypesInfo(), spec); ok { + if pkgname := pkg.TypesInfo().PkgNameOf(spec); pkgname != nil { return thing{pkg: pkgname.Imported()} } } @@ -364,8 +364,8 @@ func PackageDocHTML(viewID string, pkg *cache.Package, web Web) ([]byte, error) // canonical name. for _, f := range pkg.Syntax() { for _, imp := range f.Imports { - pkgName, ok := typesutil.ImportedPkgName(pkg.TypesInfo(), imp) - if ok && pkgName.Name() == name { + pkgName := pkg.TypesInfo().PkgNameOf(imp) + if pkgName != nil && pkgName.Name() == name { return pkgName.Imported().Path(), true } } @@ -469,7 +469,7 @@ window.addEventListener('load', function() { label := obj.Name() // for a type if fn, ok := obj.(*types.Func); ok { var buf strings.Builder - sig := fn.Type().(*types.Signature) + sig := fn.Signature() if sig.Recv() != nil { fmt.Fprintf(&buf, "(%s) ", sig.Recv().Name()) fragment = recvType + "." + fn.Name() @@ -551,7 +551,7 @@ window.addEventListener('load', function() { // method of package-level named type? if fn, ok := obj.(*types.Func); ok { - sig := fn.Type().(*types.Signature) + sig := fn.Signature() if sig.Recv() != nil { _, named := typesinternal.ReceiverNamed(sig.Recv()) if named != nil { @@ -648,7 +648,7 @@ window.addEventListener('load', function() { fnString := func(fn *types.Func) string { pkgRelative := typesinternal.NameRelativeTo(pkg.Types()) - sig := fn.Type().(*types.Signature) + sig := fn.Signature() // Emit "func (recv T) F". var buf bytes.Buffer @@ -693,7 +693,7 @@ window.addEventListener('load', function() { cloneTparams(sig.RecvTypeParams()), cloneTparams(sig.TypeParams()), types.NewTuple(append( - typesSeqToSlice[*types.Var](sig.Params())[:3], + slices.Collect(tupleVariables(sig.Params()))[:3], types.NewVar(0, nil, "", types.Typ[types.Invalid]))...), sig.Results(), false) // any final ...T parameter is truncated @@ -874,18 +874,16 @@ window.addEventListener('load', function() { return buf.Bytes(), nil } -// typesSeq abstracts various go/types sequence types: -// MethodSet, Tuple, TypeParamList, TypeList. -// TODO(adonovan): replace with go1.23 iterators. -type typesSeq[T any] interface { - Len() int - At(int) T -} - -func typesSeqToSlice[T any](seq typesSeq[T]) []T { - slice := make([]T, seq.Len()) - for i := range slice { - slice[i] = seq.At(i) +// tupleVariables returns a go1.23 iterator over the variables of a tuple type. +// +// Example: for v := range tuple.Variables() { ... } +// TODO(adonovan): use t.Variables in go1.24. +func tupleVariables(t *types.Tuple) iter.Seq[*types.Var] { + return func(yield func(v *types.Var) bool) { + for i := range t.Len() { + if !yield(t.At(i)) { + break + } + } } - return slice } diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go index 52d02543a33..6679b45df6b 100644 --- a/gopls/internal/golang/references.go +++ b/gopls/internal/golang/references.go @@ -615,7 +615,7 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo // comparisons for obj, if it is a method, or nil otherwise. func effectiveReceiver(obj types.Object) types.Type { if fn, ok := obj.(*types.Func); ok { - if recv := fn.Type().(*types.Signature).Recv(); recv != nil { + if recv := fn.Signature().Recv(); recv != nil { return methodsets.EnsurePointer(recv.Type()) } } @@ -653,12 +653,6 @@ func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Objec targets[obj] = leaf } } else { - // Note: prior to go1.21, go/types issue #60372 causes the position - // a field Var T created for struct{*p.T} to be recorded at the - // start of the field type ("*") not the location of the T. - // This affects references and other gopls operations (issue #60369). - // TODO(adonovan): delete this comment when we drop support for go1.20. - // For struct{T}, we prefer the defined field Var over the used TypeName. obj := info.ObjectOf(leaf) if obj == nil { diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index c5cf0ac0932..c2633dcd315 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -66,7 +66,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typesinternal" @@ -353,7 +352,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle // of the type parameters, unlike methods). switch obj.(type) { // avoid "obj :=" since cases reassign the var case *types.TypeName: - if _, ok := aliases.Unalias(obj.Type()).(*types.TypeParam); ok { + if _, ok := types.Unalias(obj.Type()).(*types.TypeParam); ok { // As with capitalized function parameters below, type parameters are // local. goto skipObjectPath @@ -426,7 +425,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle // contain a reference (xrefs) to the target field. case *types.Func: - if obj.Type().(*types.Signature).Recv() != nil { + if obj.Signature().Recv() != nil { transitive = true // method } @@ -978,7 +977,7 @@ func renameObjects(newName string, pkg *cache.Package, targets ...types.Object) // TODO(adonovan): pull this into the caller. for _, obj := range targets { if obj, ok := obj.(*types.Func); ok { - recv := obj.Type().(*types.Signature).Recv() + recv := obj.Signature().Recv() if recv != nil && types.IsInterface(recv.Type().Underlying()) { r.changeMethods = true break @@ -1168,7 +1167,7 @@ func (r *renamer) updateCommentDocLinks() (map[protocol.DocumentURI][]diff.Edit, if !isFunc { continue } - recv := obj.Type().(*types.Signature).Recv() + recv := obj.Signature().Recv() if recv == nil { continue } diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 497f1a09ca2..ed6424c918f 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -45,7 +45,6 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/refactor/satisfy" @@ -504,7 +503,7 @@ func (r *renamer) checkStructField(from *types.Var) { if from.Anonymous() { if named, ok := from.Type().(*types.Named); ok { r.check(named.Obj()) - } else if named, ok := aliases.Unalias(typesinternal.Unpointer(from.Type())).(*types.Named); ok { + } else if named, ok := types.Unalias(typesinternal.Unpointer(from.Type())).(*types.Named); ok { r.check(named.Obj()) } } @@ -813,7 +812,7 @@ func (r *renamer) checkMethod(from *types.Func) { var iface string I := recv(imeth).Type() - if named, ok := aliases.Unalias(I).(*types.Named); ok { + if named, ok := types.Unalias(I).(*types.Named); ok { pos = named.Obj().Pos() iface = "interface " + named.Obj().Name() } else { @@ -867,9 +866,17 @@ func (r *renamer) satisfy() map[satisfy.Constraint]bool { // // Only proceed if all packages have no errors. if len(pkg.ParseErrors()) > 0 || len(pkg.TypeErrors()) > 0 { + var filename string + if len(pkg.ParseErrors()) > 0 { + err := pkg.ParseErrors()[0][0] + filename = filepath.Base(err.Pos.Filename) + } else if len(pkg.TypeErrors()) > 0 { + err := pkg.TypeErrors()[0] + filename = filepath.Base(err.Fset.File(err.Pos).Name()) + } r.errorf(token.NoPos, // we don't have a position for this error. - "renaming %q to %q not possible because %q has errors", - r.from, r.to, pkg.Metadata().PkgPath) + "renaming %q to %q not possible because %q in %q has errors", + r.from, r.to, filename, pkg.Metadata().PkgPath) return nil } f.Find(pkg.TypesInfo(), pkg.Syntax()) @@ -883,7 +890,7 @@ func (r *renamer) satisfy() map[satisfy.Constraint]bool { // recv returns the method's receiver. func recv(meth *types.Func) *types.Var { - return meth.Type().(*types.Signature).Recv() + return meth.Signature().Recv() } // someUse returns an arbitrary use of obj within info. diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index 9fd093fe5fc..e008d8cdaea 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go @@ -28,8 +28,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol/semtok" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/typesutil" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" ) @@ -128,7 +126,7 @@ func (tv *tokenVisitor) visit() { importByName := make(map[string]*types.PkgName) for _, pgf := range tv.pkg.CompiledGoFiles() { for _, imp := range pgf.File.Imports { - if obj, _ := typesutil.ImportedPkgName(tv.pkg.TypesInfo(), imp); obj != nil { + if obj := tv.pkg.TypesInfo().PkgNameOf(imp); obj != nil { if old, ok := importByName[obj.Name()]; ok { if old != nil && old.Imported() != obj.Imported() { importByName[obj.Name()] = nil // nil => ambiguous across files @@ -574,13 +572,13 @@ func (tv *tokenVisitor) ident(id *ast.Ident) { case *types.PkgName: emit(semtok.TokNamespace) case *types.TypeName: // could be a TypeParam - if is[*types.TypeParam](aliases.Unalias(obj.Type())) { + if is[*types.TypeParam](types.Unalias(obj.Type())) { emit(semtok.TokTypeParam) } else { emit(semtok.TokType, appendTypeModifiers(nil, obj)...) } case *types.Var: - if is[*types.Signature](aliases.Unalias(obj.Type())) { + if is[*types.Signature](types.Unalias(obj.Type())) { emit(semtok.TokFunction) } else if tv.isParam(obj.Pos()) { // variable, unless use.pos is the pos of a Field in an ancestor FuncDecl diff --git a/gopls/internal/golang/signature_help.go b/gopls/internal/golang/signature_help.go index a91be296cbd..dfdce041ff6 100644 --- a/gopls/internal/golang/signature_help.go +++ b/gopls/internal/golang/signature_help.go @@ -45,13 +45,32 @@ func SignatureHelp(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle if path == nil { return nil, 0, fmt.Errorf("cannot find node enclosing position") } -FindCall: - for _, node := range path { + info := pkg.TypesInfo() + var fnval ast.Expr +loop: + for i, node := range path { switch node := node.(type) { + case *ast.Ident: + // If the selected text is a function/method Ident orSelectorExpr, + // even one not in function call position, + // show help for its signature. Example: + // once.Do(initialize⁁) + // should show help for initialize, not once.Do. + if t := info.TypeOf(node); t != nil && + info.Defs[node] == nil && + is[*types.Signature](t.Underlying()) { + if sel, ok := path[i+1].(*ast.SelectorExpr); ok && sel.Sel == node { + fnval = sel // e.g. fmt.Println⁁ + } else { + fnval = node + } + break loop + } case *ast.CallExpr: if pos >= node.Lparen && pos <= node.Rparen { callExpr = node - break FindCall + fnval = callExpr.Fun + break loop } case *ast.FuncLit, *ast.FuncType: // The user is within an anonymous function, @@ -70,20 +89,19 @@ FindCall: } } - if callExpr == nil || callExpr.Fun == nil { + + if fnval == nil { return nil, 0, nil } - info := pkg.TypesInfo() - // Get the type information for the function being called. var sig *types.Signature - if tv, ok := info.Types[callExpr.Fun]; !ok { - return nil, 0, fmt.Errorf("cannot get type for Fun %[1]T (%[1]v)", callExpr.Fun) + if tv, ok := info.Types[fnval]; !ok { + return nil, 0, fmt.Errorf("cannot get type for Fun %[1]T (%[1]v)", fnval) } else if tv.IsType() { return nil, 0, nil // a conversion, not a call } else if sig, ok = tv.Type.Underlying().(*types.Signature); !ok { - return nil, 0, fmt.Errorf("call operand is not a func or type: %[1]T (%[1]v)", callExpr.Fun) + return nil, 0, fmt.Errorf("call operand is not a func or type: %[1]T (%[1]v)", fnval) } // Inv: sig != nil @@ -93,7 +111,7 @@ FindCall: // There is no object in certain cases such as calling a function returned by // a function (e.g. "foo()()"). var obj types.Object - switch t := callExpr.Fun.(type) { + switch t := fnval.(type) { case *ast.Ident: obj = info.ObjectOf(t) case *ast.SelectorExpr: @@ -116,7 +134,12 @@ FindCall: return nil, 0, bug.Errorf("call to unexpected built-in %v (%T)", obj, obj) } - activeParam := activeParameter(callExpr, sig.Params().Len(), sig.Variadic(), pos) + activeParam := 0 + if callExpr != nil { + // only return activeParam when CallExpr + // because we don't modify arguments when get function signature only + activeParam = activeParameter(callExpr, sig.Params().Len(), sig.Variadic(), pos) + } var ( name string @@ -148,6 +171,8 @@ FindCall: }, activeParam, nil } +// Note: callExpr may be nil when signatureHelp is invoked outside the call +// argument list (golang/go#69552). func builtinSignature(ctx context.Context, snapshot *cache.Snapshot, callExpr *ast.CallExpr, name string, pos token.Pos) (*protocol.SignatureInformation, int, error) { sig, err := NewBuiltinSignature(ctx, snapshot, name) if err != nil { @@ -157,7 +182,10 @@ func builtinSignature(ctx context.Context, snapshot *cache.Snapshot, callExpr *a for _, p := range sig.params { paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p}) } - activeParam := activeParameter(callExpr, len(sig.params), sig.variadic, pos) + activeParam := 0 + if callExpr != nil { + activeParam = activeParameter(callExpr, len(sig.params), sig.variadic, pos) + } return &protocol.SignatureInformation{ Label: sig.name + sig.Format(), Documentation: stringToSigInfoDocumentation(sig.doc, snapshot.Options()), diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go index 47bcf3a7dcf..ca5f0055c3b 100644 --- a/gopls/internal/golang/stub.go +++ b/gopls/internal/golang/stub.go @@ -18,10 +18,10 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/gopls/internal/analysis/stubmethods" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/golang/stubmethods" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" @@ -40,6 +40,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. // A function-local type cannot be stubbed // since there's nowhere to put the methods. + // TODO(adonovan): move this check into GetStubInfo instead of offering a bad fix. conc := si.Concrete.Obj() if conc.Parent() != conc.Pkg().Scope() { return nil, nil, fmt.Errorf("local type %q cannot be stubbed", conc.Name()) @@ -208,7 +209,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. // Otherwise, use lowercase for the first letter of the object. rn := strings.ToLower(si.Concrete.Obj().Name()[0:1]) for i := 0; i < si.Concrete.NumMethods(); i++ { - if recv := si.Concrete.Method(i).Type().(*types.Signature).Recv(); recv.Name() != "" { + if recv := si.Concrete.Method(i).Signature().Recv(); recv.Name() != "" { rn = recv.Name() break } @@ -229,7 +230,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. for index := range missing { mrn := rn + " " - sig := missing[index].fn.Type().(*types.Signature) + sig := missing[index].fn.Signature() if checkRecvName(sig.Params()) || checkRecvName(sig.Results()) { mrn = "" } @@ -281,7 +282,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. // Re-parse the file. fset := token.NewFileSet() - newF, err := parser.ParseFile(fset, declPGF.URI.Path(), buf.Bytes(), parser.ParseComments) + newF, err := parser.ParseFile(fset, declPGF.URI.Path(), buf.Bytes(), parser.ParseComments|parser.SkipObjectResolution) if err != nil { return nil, nil, fmt.Errorf("could not reparse file: %w", err) } diff --git a/gopls/internal/analysis/stubmethods/stubmethods.go b/gopls/internal/golang/stubmethods/stubmethods.go similarity index 75% rename from gopls/internal/analysis/stubmethods/stubmethods.go rename to gopls/internal/golang/stubmethods/stubmethods.go index f4c30aadd7d..ee7b525a6a0 100644 --- a/gopls/internal/analysis/stubmethods/stubmethods.go +++ b/gopls/internal/golang/stubmethods/stubmethods.go @@ -2,103 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package stubmethods provides the analysis logic for the quick fix +// to "Declare missing methods of TYPE" errors. (The fix logic lives +// in golang.stubMethodsFixer.) package stubmethods import ( - "bytes" - _ "embed" "fmt" "go/ast" - "go/format" "go/token" "go/types" - "strings" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/gopls/internal/util/typesutil" - "golang.org/x/tools/internal/aliases" - "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/typesinternal" ) -//go:embed doc.go -var doc string - -var Analyzer = &analysis.Analyzer{ - Name: "stubmethods", - Doc: analysisinternal.MustExtractDoc(doc, "stubmethods"), - Run: run, - RunDespiteErrors: true, - URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods", -} - -// TODO(rfindley): remove this thin wrapper around the stubmethods refactoring, -// and eliminate the stubmethods analyzer. -// -// Previous iterations used the analysis framework for computing refactorings, -// which proved inefficient. -func run(pass *analysis.Pass) (interface{}, error) { - for _, err := range pass.TypeErrors { - var file *ast.File - for _, f := range pass.Files { - if f.Pos() <= err.Pos && err.Pos < f.End() { - file = f - break - } - } - // Get the end position of the error. - _, _, end, ok := typesinternal.ReadGo116ErrorData(err) - if !ok { - var buf bytes.Buffer - if err := format.Node(&buf, pass.Fset, file); err != nil { - continue - } - end = analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos) - } - if diag, ok := DiagnosticForError(pass.Fset, file, err.Pos, end, err.Msg, pass.TypesInfo); ok { - pass.Report(diag) - } - } - - return nil, nil -} - -// MatchesMessage reports whether msg matches the error message sought after by -// the stubmethods fix. -func MatchesMessage(msg string) bool { - return strings.Contains(msg, "missing method") || strings.HasPrefix(msg, "cannot convert") || strings.Contains(msg, "not implement") -} - -// DiagnosticForError computes a diagnostic suggesting to implement an -// interface to fix the type checking error defined by (start, end, msg). -// -// If no such fix is possible, the second result is false. -func DiagnosticForError(fset *token.FileSet, file *ast.File, start, end token.Pos, msg string, info *types.Info) (analysis.Diagnostic, bool) { - if !MatchesMessage(msg) { - return analysis.Diagnostic{}, false - } - - path, _ := astutil.PathEnclosingInterval(file, start, end) - si := GetStubInfo(fset, info, path, start) - if si == nil { - return analysis.Diagnostic{}, false - } - qf := typesutil.FileQualifier(file, si.Concrete.Obj().Pkg(), info) - iface := types.TypeString(si.Interface.Type(), qf) - return analysis.Diagnostic{ - Pos: start, - End: end, - Message: msg, - Category: FixCategory, - SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("Declare missing methods of %s", iface), - // No TextEdits => computed later by gopls. - }}, - }, true -} - -const FixCategory = "stubmethods" // recognized by gopls ApplyFix +// TODO(adonovan): eliminate the confusing Fset parameter; only the +// file name and byte offset of Concrete are needed. // StubInfo represents a concrete type // that wants to stub out an interface type @@ -179,7 +96,7 @@ func fromCallExpr(fset *token.FileSet, info *types.Info, pos token.Pos, call *as if !ok { return nil } - sig, ok := aliases.Unalias(tv.Type).(*types.Signature) + sig, ok := types.Unalias(tv.Type).(*types.Signature) if !ok { return nil } @@ -344,7 +261,7 @@ func ifaceType(e ast.Expr, info *types.Info) *types.TypeName { } func ifaceObjFromType(t types.Type) *types.TypeName { - named, ok := aliases.Unalias(t).(*types.Named) + named, ok := types.Unalias(t).(*types.Named) if !ok { return nil } @@ -373,11 +290,11 @@ func concreteType(e ast.Expr, info *types.Info) (*types.Named, bool) { return nil, false } typ := tv.Type - ptr, isPtr := aliases.Unalias(typ).(*types.Pointer) + ptr, isPtr := types.Unalias(typ).(*types.Pointer) if isPtr { typ = ptr.Elem() } - named, ok := aliases.Unalias(typ).(*types.Named) + named, ok := types.Unalias(typ).(*types.Named) if !ok { return nil, false } diff --git a/gopls/internal/golang/types_format.go b/gopls/internal/golang/types_format.go index 51584bcb013..41828244e11 100644 --- a/gopls/internal/golang/types_format.go +++ b/gopls/internal/golang/types_format.go @@ -214,6 +214,9 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si if err != nil { return nil, err } + if sig.Variadic() && i == sig.Params().Len()-1 { + typ = strings.Replace(typ, "[]", "...", 1) + } p := typ if el.Name() != "" { p = el.Name() + " " + typ @@ -261,6 +264,10 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si }, nil } +// We look for 'invalidTypeString' to determine if we can use the fast path for +// FormatVarType. +var invalidTypeString = types.Typ[types.Invalid].String() + // FormatVarType formats a *types.Var, accounting for type aliases. // To do this, it looks in the AST of the file in which the object is declared. // On any errors, it always falls back to types.TypeString. @@ -268,6 +275,21 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si // TODO(rfindley): this function could return the actual name used in syntax, // for better parameter names. func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache.Package, obj *types.Var, qf types.Qualifier, mq MetadataQualifier) (string, error) { + typeString := types.TypeString(obj.Type(), qf) + // Fast path: if the type string does not contain 'invalid type', we no + // longer need to do any special handling, thanks to materialized aliases in + // Go 1.23+. + // + // Unfortunately, due to the handling of invalid types, we can't quite delete + // the rather complicated preexisting logic of FormatVarType--it isn't an + // acceptable regression to start printing "invalid type" in completion or + // signature help. strings.Contains is conservative: the type string of a + // valid type may actually contain "invalid type" (due to struct tags or + // field formatting), but such cases should be exceedingly rare. + if !strings.Contains(typeString, invalidTypeString) { + return typeString, nil + } + // TODO(rfindley): This looks wrong. The previous comment said: // "If the given expr refers to a type parameter, then use the // object's Type instead of the type parameter declaration. This helps @@ -280,13 +302,13 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache. // // Left this during refactoring in order to preserve pre-existing logic. if typeparams.IsTypeParam(obj.Type()) { - return types.TypeString(obj.Type(), qf), nil + return typeString, nil } if isBuiltin(obj) { // This is defensive, though it is extremely unlikely we'll ever have a // builtin var. - return types.TypeString(obj.Type(), qf), nil + return typeString, nil } // TODO(rfindley): parsing to produce candidates can be costly; consider @@ -309,7 +331,7 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache. // for parameterized decls. if decl, _ := decl.(*ast.FuncDecl); decl != nil { if decl.Type.TypeParams.NumFields() > 0 { - return types.TypeString(obj.Type(), qf), nil // in generic function + return typeString, nil // in generic function } if decl.Recv != nil && len(decl.Recv.List) > 0 { rtype := decl.Recv.List[0].Type @@ -317,18 +339,18 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache. rtype = e.X } if x, _, _, _ := typeparams.UnpackIndexExpr(rtype); x != nil { - return types.TypeString(obj.Type(), qf), nil // in method of generic type + return typeString, nil // in method of generic type } } } if spec, _ := spec.(*ast.TypeSpec); spec != nil && spec.TypeParams.NumFields() > 0 { - return types.TypeString(obj.Type(), qf), nil // in generic type decl + return typeString, nil // in generic type decl } if field == nil { // TODO(rfindley): we should never reach here from an ordinary var, so // should probably return an error here. - return types.TypeString(obj.Type(), qf), nil + return typeString, nil } expr := field.Type diff --git a/gopls/internal/mod/code_lens.go b/gopls/internal/mod/code_lens.go index f23cc641365..f80063625ff 100644 --- a/gopls/internal/mod/code_lens.go +++ b/gopls/internal/mod/code_lens.go @@ -34,16 +34,13 @@ func upgradeLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle return nil, err } uri := fh.URI() - reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.ResetGoModDiagnosticsArgs{URIArg: command.URIArg{URI: uri}}) - if err != nil { - return nil, err - } + reset := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.ResetGoModDiagnosticsArgs{URIArg: command.URIArg{URI: uri}}) // Put the `Reset go.mod diagnostics` codelens on the module statement. modrng, err := moduleStmtRange(fh, pm) if err != nil { return nil, err } - lenses := []protocol.CodeLens{{Range: modrng, Command: &reset}} + lenses := []protocol.CodeLens{{Range: modrng, Command: reset}} if len(pm.File.Require) == 0 { // Nothing to upgrade. return lenses, nil @@ -52,29 +49,20 @@ func upgradeLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle for _, req := range pm.File.Require { requires = append(requires, req.Mod.Path) } - checkUpgrade, err := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{ + checkUpgrade := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{ URI: uri, Modules: requires, }) - if err != nil { - return nil, err - } - upgradeTransitive, err := command.NewUpgradeDependencyCommand("Upgrade transitive dependencies", command.DependencyArgs{ + upgradeTransitive := command.NewUpgradeDependencyCommand("Upgrade transitive dependencies", command.DependencyArgs{ URI: uri, AddRequire: false, GoCmdArgs: []string{"-d", "-u", "-t", "./..."}, }) - if err != nil { - return nil, err - } - upgradeDirect, err := command.NewUpgradeDependencyCommand("Upgrade direct dependencies", command.DependencyArgs{ + upgradeDirect := command.NewUpgradeDependencyCommand("Upgrade direct dependencies", command.DependencyArgs{ URI: uri, AddRequire: false, GoCmdArgs: append([]string{"-d"}, requires...), }) - if err != nil { - return nil, err - } // Put the upgrade code lenses above the first require block or statement. rng, err := firstRequireRange(fh, pm) @@ -83,9 +71,9 @@ func upgradeLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle } return append(lenses, []protocol.CodeLens{ - {Range: rng, Command: &checkUpgrade}, - {Range: rng, Command: &upgradeTransitive}, - {Range: rng, Command: &upgradeDirect}, + {Range: rng, Command: checkUpgrade}, + {Range: rng, Command: upgradeTransitive}, + {Range: rng, Command: upgradeDirect}, }...), nil } @@ -95,17 +83,14 @@ func tidyLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([] return nil, err } uri := fh.URI() - cmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: []protocol.DocumentURI{uri}}) - if err != nil { - return nil, err - } + cmd := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: []protocol.DocumentURI{uri}}) rng, err := moduleStmtRange(fh, pm) if err != nil { return nil, err } return []protocol.CodeLens{{ Range: rng, - Command: &cmd, + Command: cmd, }}, nil } @@ -124,17 +109,14 @@ func vendorLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ( } title := "Create vendor directory" uri := fh.URI() - cmd, err := command.NewVendorCommand(title, command.URIArg{URI: uri}) - if err != nil { - return nil, err - } + cmd := command.NewVendorCommand(title, command.URIArg{URI: uri}) // Change the message depending on whether or not the module already has a // vendor directory. vendorDir := filepath.Join(filepath.Dir(fh.URI().Path()), "vendor") if info, _ := os.Stat(vendorDir); info != nil && info.IsDir() { title = "Sync vendor directory" } - return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil + return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil } func moduleStmtRange(fh file.Handle, pm *cache.ParsedModule) (protocol.Range, error) { @@ -180,14 +162,11 @@ func vulncheckLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand return nil, err } - vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ + vulncheck := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ URI: uri, Pattern: "./...", }) - if err != nil { - return nil, err - } return []protocol.CodeLens{ - {Range: rng, Command: &vulncheck}, + {Range: rng, Command: vulncheck}, }, nil } diff --git a/gopls/internal/mod/diagnostics.go b/gopls/internal/mod/diagnostics.go index d24c3144ffd..8da69313e49 100644 --- a/gopls/internal/mod/diagnostics.go +++ b/gopls/internal/mod/diagnostics.go @@ -157,14 +157,11 @@ func ModUpgradeDiagnostics(ctx context.Context, snapshot *cache.Snapshot, fh fil } // Upgrade to the exact version we offer the user, not the most recent. title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, ver) - cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ + cmd := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ URI: fh.URI(), AddRequire: false, GoCmdArgs: []string{req.Mod.Path + "@" + ver}, }) - if err != nil { - return nil, err - } upgradeDiagnostics = append(upgradeDiagnostics, &cache.Diagnostic{ URI: fh.URI(), Range: rng, @@ -268,10 +265,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot *cache.Snapshot, } // Upgrade to the exact version we offer the user, not the most recent. 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 - } + cmd := getUpgradeCodeAction(fh, req, fixedVersion) sf := cache.SuggestedFixFromCommand(cmd, protocol.QuickFix) switch _, typ := foundVuln(finding); typ { case vulnImported: @@ -295,10 +289,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot *cache.Snapshot, } // Add an upgrade for module@latest. // TODO(suzmue): verify if latest is the same as fixedVersion. - latest, err := getUpgradeCodeAction(fh, req, "latest") - if err != nil { - return nil, err // TODO: bug report - } + latest := getUpgradeCodeAction(fh, req, "latest") sf := cache.SuggestedFixFromCommand(latest, protocol.QuickFix) if len(warningFixes) > 0 { warningFixes = append(warningFixes, sf) @@ -445,22 +436,16 @@ func sortedKeys(m map[string]bool) []string { // (if the present vulncheck diagnostics are already based on govulncheck run). func suggestGovulncheckAction(fromGovulncheck bool, uri protocol.DocumentURI) (cache.SuggestedFix, error) { if fromGovulncheck { - resetVulncheck, err := command.NewResetGoModDiagnosticsCommand("Reset govulncheck result", command.ResetGoModDiagnosticsArgs{ + resetVulncheck := command.NewResetGoModDiagnosticsCommand("Reset govulncheck result", command.ResetGoModDiagnosticsArgs{ URIArg: command.URIArg{URI: uri}, DiagnosticSource: string(cache.Govulncheck), }) - if err != nil { - return cache.SuggestedFix{}, err - } return cache.SuggestedFixFromCommand(resetVulncheck, protocol.QuickFix), nil } - vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck to verify", command.VulncheckArgs{ + vulncheck := command.NewRunGovulncheckCommand("Run govulncheck to verify", command.VulncheckArgs{ URI: uri, Pattern: "./...", }) - if err != nil { - return cache.SuggestedFix{}, err - } return cache.SuggestedFixFromCommand(vulncheck, protocol.QuickFix), nil } @@ -501,16 +486,12 @@ func href(vulnID string) string { return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vulnID) } -func getUpgradeCodeAction(fh file.Handle, req *modfile.Require, version string) (protocol.Command, error) { - cmd, err := command.NewUpgradeDependencyCommand(upgradeTitle(version), command.DependencyArgs{ +func getUpgradeCodeAction(fh file.Handle, req *modfile.Require, version string) *protocol.Command { + return command.NewUpgradeDependencyCommand(upgradeTitle(version), command.DependencyArgs{ URI: fh.URI(), AddRequire: false, GoCmdArgs: []string{req.Mod.Path + "@" + version}, }) - if err != nil { - return protocol.Command{}, err - } - return cmd, nil } func upgradeTitle(fixedVersion string) string { diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index 7f9ba1bc9db..10c6c043a09 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go @@ -170,11 +170,11 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte } return nil, s.DiagnoseFiles(ctx, a0) case Doc: - var a0 protocol.Location + var a0 DocArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { return nil, err } - return nil, s.Doc(ctx, a0) + return s.Doc(ctx, a0) case EditGoDirective: var a0 EditGoDirectiveArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -348,469 +348,330 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, fmt.Errorf("unsupported command %q", params.Command) } -func NewAddDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewAddDependencyCommand(title string, a0 DependencyArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: AddDependency.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewAddImportCommand(title string, a0 AddImportArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewAddImportCommand(title string, a0 AddImportArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: AddImport.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewAddTelemetryCountersCommand(title string, a0 AddTelemetryCountersArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewAddTelemetryCountersCommand(title string, a0 AddTelemetryCountersArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: AddTelemetryCounters.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewApplyFixCommand(title string, a0 ApplyFixArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewApplyFixCommand(title string, a0 ApplyFixArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: ApplyFix.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewAssemblyCommand(title string, a0 string, a1 string, a2 string) (protocol.Command, error) { - args, err := MarshalArgs(a0, a1, a2) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewAssemblyCommand(title string, a0 string, a1 string, a2 string) *protocol.Command { + return &protocol.Command{ Title: title, Command: Assembly.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0, a1, a2), + } } -func NewChangeSignatureCommand(title string, a0 ChangeSignatureArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewChangeSignatureCommand(title string, a0 ChangeSignatureArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: ChangeSignature.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: CheckUpgrades.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewClientOpenURLCommand(title string, a0 string) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewClientOpenURLCommand(title string, a0 string) *protocol.Command { + return &protocol.Command{ Title: title, Command: ClientOpenURL.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewDiagnoseFilesCommand(title string, a0 DiagnoseFilesArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewDiagnoseFilesCommand(title string, a0 DiagnoseFilesArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: DiagnoseFiles.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewDocCommand(title string, a0 protocol.Location) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewDocCommand(title string, a0 DocArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: Doc.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewEditGoDirectiveCommand(title string, a0 EditGoDirectiveArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewEditGoDirectiveCommand(title string, a0 EditGoDirectiveArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: EditGoDirective.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewExtractToNewFileCommand(title string, a0 protocol.Location) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewExtractToNewFileCommand(title string, a0 protocol.Location) *protocol.Command { + return &protocol.Command{ Title: title, Command: ExtractToNewFile.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewFetchVulncheckResultCommand(title string, a0 URIArg) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewFetchVulncheckResultCommand(title string, a0 URIArg) *protocol.Command { + return &protocol.Command{ Title: title, Command: FetchVulncheckResult.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewFreeSymbolsCommand(title string, a0 string, a1 protocol.Location) (protocol.Command, error) { - args, err := MarshalArgs(a0, a1) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewFreeSymbolsCommand(title string, a0 string, a1 protocol.Location) *protocol.Command { + return &protocol.Command{ Title: title, Command: FreeSymbols.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0, a1), + } } -func NewGCDetailsCommand(title string, a0 protocol.DocumentURI) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewGCDetailsCommand(title string, a0 protocol.DocumentURI) *protocol.Command { + return &protocol.Command{ Title: title, Command: GCDetails.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewGenerateCommand(title string, a0 GenerateArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewGenerateCommand(title string, a0 GenerateArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: Generate.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: GoGetPackage.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewListImportsCommand(title string, a0 URIArg) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewListImportsCommand(title string, a0 URIArg) *protocol.Command { + return &protocol.Command{ Title: title, Command: ListImports.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewListKnownPackagesCommand(title string, a0 URIArg) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewListKnownPackagesCommand(title string, a0 URIArg) *protocol.Command { + return &protocol.Command{ Title: title, Command: ListKnownPackages.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewMaybePromptForTelemetryCommand(title string) (protocol.Command, error) { - return protocol.Command{ - Title: title, - Command: MaybePromptForTelemetry.String(), - }, nil +func NewMaybePromptForTelemetryCommand(title string) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: MaybePromptForTelemetry.String(), + Arguments: MustMarshalArgs(), + } } -func NewMemStatsCommand(title string) (protocol.Command, error) { - return protocol.Command{ - Title: title, - Command: MemStats.String(), - }, nil +func NewMemStatsCommand(title string) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: MemStats.String(), + Arguments: MustMarshalArgs(), + } } -func NewModulesCommand(title string, a0 ModulesArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewModulesCommand(title string, a0 ModulesArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: Modules.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewPackagesCommand(title string, a0 PackagesArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewPackagesCommand(title string, a0 PackagesArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: Packages.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewRegenerateCgoCommand(title string, a0 URIArg) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewRegenerateCgoCommand(title string, a0 URIArg) *protocol.Command { + return &protocol.Command{ Title: title, Command: RegenerateCgo.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: RemoveDependency.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewResetGoModDiagnosticsCommand(title string, a0 ResetGoModDiagnosticsArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewResetGoModDiagnosticsCommand(title string, a0 ResetGoModDiagnosticsArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: ResetGoModDiagnostics.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewRunGoWorkCommandCommand(title string, a0 RunGoWorkArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewRunGoWorkCommandCommand(title string, a0 RunGoWorkArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: RunGoWorkCommand.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewRunGovulncheckCommand(title string, a0 VulncheckArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewRunGovulncheckCommand(title string, a0 VulncheckArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: RunGovulncheck.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewRunTestsCommand(title string, a0 RunTestsArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: RunTests.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewScanImportsCommand(title string) (protocol.Command, error) { - return protocol.Command{ - Title: title, - Command: ScanImports.String(), - }, nil +func NewScanImportsCommand(title string) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: ScanImports.String(), + Arguments: MustMarshalArgs(), + } } -func NewStartDebuggingCommand(title string, a0 DebuggingArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewStartDebuggingCommand(title string, a0 DebuggingArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: StartDebugging.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewStartProfileCommand(title string, a0 StartProfileArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewStartProfileCommand(title string, a0 StartProfileArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: StartProfile.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewStopProfileCommand(title string, a0 StopProfileArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewStopProfileCommand(title string, a0 StopProfileArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: StopProfile.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewTestCommand(title string, a0 protocol.DocumentURI, a1 []string, a2 []string) (protocol.Command, error) { - args, err := MarshalArgs(a0, a1, a2) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewTestCommand(title string, a0 protocol.DocumentURI, a1 []string, a2 []string) *protocol.Command { + return &protocol.Command{ Title: title, Command: Test.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0, a1, a2), + } } -func NewTidyCommand(title string, a0 URIArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewTidyCommand(title string, a0 URIArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: Tidy.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewToggleGCDetailsCommand(title string, a0 URIArg) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewToggleGCDetailsCommand(title string, a0 URIArg) *protocol.Command { + return &protocol.Command{ Title: title, Command: ToggleGCDetails.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewUpdateGoSumCommand(title string, a0 URIArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewUpdateGoSumCommand(title string, a0 URIArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: UpdateGoSum.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewUpgradeDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewUpgradeDependencyCommand(title string, a0 DependencyArgs) *protocol.Command { + return &protocol.Command{ Title: title, Command: UpgradeDependency.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewVendorCommand(title string, a0 URIArg) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ +func NewVendorCommand(title string, a0 URIArg) *protocol.Command { + return &protocol.Command{ Title: title, Command: Vendor.String(), - Arguments: args, - }, nil + Arguments: MustMarshalArgs(a0), + } } -func NewViewsCommand(title string) (protocol.Command, error) { - return protocol.Command{ - Title: title, - Command: Views.String(), - }, nil +func NewViewsCommand(title string) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: Views.String(), + Arguments: MustMarshalArgs(), + } } -func NewWorkspaceStatsCommand(title string) (protocol.Command, error) { - return protocol.Command{ - Title: title, - Command: WorkspaceStats.String(), - }, nil +func NewWorkspaceStatsCommand(title string) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: WorkspaceStats.String(), + Arguments: MustMarshalArgs(), + } } diff --git a/gopls/internal/protocol/command/commandmeta/meta.go b/gopls/internal/protocol/command/commandmeta/meta.go index 0ef80b72f02..dcc366521e5 100644 --- a/gopls/internal/protocol/command/commandmeta/meta.go +++ b/gopls/internal/protocol/command/commandmeta/meta.go @@ -20,7 +20,6 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/aliases" // (does not depend on gopls itself) ) @@ -125,7 +124,7 @@ func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command if i == 0 { // Lazy check that the first argument is a context. We could relax this, // but then the generated code gets more complicated. - if named, ok := aliases.Unalias(fld.Type).(*types.Named); !ok || named.Obj().Name() != "Context" || named.Obj().Pkg().Path() != "context" { + if named, ok := types.Unalias(fld.Type).(*types.Named); !ok || named.Obj().Name() != "Context" || named.Obj().Pkg().Path() != "context" { return nil, fmt.Errorf("first method parameter must be context.Context") } // Skip the context argument, as it is implied. @@ -146,6 +145,12 @@ func (l *fieldLoader) loadField(pkg *packages.Package, obj *types.Var, doc, tag Type: obj.Type(), JSONTag: reflect.StructTag(tag).Get("json"), } + + // This must be done here to handle nested types, such as: + // + // type Test struct { Subtests []Test } + l.loaded[obj] = fld + under := fld.Type.Underlying() // Quick-and-dirty handling for various underlying types. switch p := under.(type) { diff --git a/gopls/internal/protocol/command/gen/gen.go b/gopls/internal/protocol/command/gen/gen.go index b0523e7cece..d9722902ca9 100644 --- a/gopls/internal/protocol/command/gen/gen.go +++ b/gopls/internal/protocol/command/gen/gen.go @@ -10,6 +10,7 @@ import ( "bytes" "fmt" "go/types" + "log" "text/template" "golang.org/x/tools/gopls/internal/protocol/command/commandmeta" @@ -71,21 +72,28 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte } {{- range .Commands}} -func New{{.MethodName}}Command(title string, {{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}} {{typeString $v.Type}}{{end}}) (protocol.Command, error) { - {{- if .Args -}} +{{if fallible .Args}} +func New{{.MethodName}}Command(title string, {{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}} {{typeString $v.Type}}{{end}}) (*protocol.Command, error) { args, err := MarshalArgs({{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}}{{end}}) if err != nil { - return protocol.Command{}, err + return nil, err } - {{end -}} - return protocol.Command{ + return &protocol.Command{ Title: title, Command: {{.MethodName}}.String(), - {{- if .Args}} Arguments: args, - {{end}} }, nil } +{{else}} +func New{{.MethodName}}Command(title string, {{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}} {{typeString $v.Type}}{{end}}) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: {{.MethodName}}.String(), + Arguments: MustMarshalArgs({{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}}{{end}}), + } +} +{{end}} + {{end}} ` @@ -112,6 +120,33 @@ func Generate() ([]byte, error) { "typeString": func(t types.Type) string { return types.TypeString(t, qf) }, + "fallible": func(args []*commandmeta.Field) bool { + var fallible func(types.Type) bool + fallible = func(t types.Type) bool { + switch t := t.Underlying().(type) { + case *types.Basic: + return false + case *types.Slice: + return fallible(t.Elem()) + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if fallible(t.Field(i).Type()) { + return true + } + } + return false + } + // Assume all other types are fallible for now: + log.Println("Command.Args has fallible type", t) + return true + } + for _, arg := range args { + if fallible(arg.Type) { + return true + } + } + return false + }, }).Parse(src) if err != nil { return nil, err diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index 35e191eb413..98c5e6a061b 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -73,7 +73,7 @@ type Interface interface { // // Opens the Go package documentation page for the current // package in a browser. - Doc(context.Context, protocol.Location) error + Doc(context.Context, DocArgs) (protocol.URI, error) // RegenerateCgo: Regenerate cgo // @@ -310,6 +310,11 @@ type GenerateArgs struct { Recursive bool } +type DocArgs struct { + Location protocol.Location + ShowDocument bool // in addition to returning the URL, send showDocument +} + // TODO(rFindley): document the rest of these once the docgen is fleshed out. type ApplyFixArgs struct { @@ -323,10 +328,9 @@ type ApplyFixArgs struct { // upon by the code action and golang.ApplyFix. Fix string - // The file URI for the document to fix. - URI protocol.DocumentURI - // The document range to scan for fixes. - Range protocol.Range + // The portion of the document to fix. + Location protocol.Location + // Whether to resolve and return the edits. ResolveEdits bool } @@ -599,7 +603,7 @@ type PackagesArgs struct { // the result may describe any of them. Files []protocol.DocumentURI - // Enumerate all packages under the directry loadable with + // Enumerate all packages under the directory loadable with // the ... pattern. // The search does not cross the module boundaries and // does not return packages that are not yet loaded. @@ -637,6 +641,8 @@ type Package struct { // Module path. Empty if the package doesn't // belong to any module. ModulePath string + // q in a "p [q.test]" package. + ForTest string // Note: the result does not include the directory name // of the package because mapping between a package and @@ -683,7 +689,7 @@ type TestCase struct { // analysis; if so, it should aim to simulate the actual computed // name of the test, including any disambiguating suffix such as "#01". // To run only this test, clients need to compute the -run, -bench, -fuzz - // flag values by first splitting the Name with “/” and + // flag values by first splitting the Name with "/" and // quoting each element with "^" + regexp.QuoteMeta(Name) + "$". // e.g. TestToplevel/Inner.Subtest → -run=^TestToplevel$/^Inner\.Subtest$ Name string diff --git a/gopls/internal/protocol/command/util.go b/gopls/internal/protocol/command/util.go index b403679fc9a..7cd5662e3e1 100644 --- a/gopls/internal/protocol/command/util.go +++ b/gopls/internal/protocol/command/util.go @@ -45,6 +45,15 @@ func MarshalArgs(args ...interface{}) ([]json.RawMessage, error) { return out, nil } +// MustMarshalArgs is like MarshalArgs, but panics on error. +func MustMarshalArgs(args ...interface{}) []json.RawMessage { + msg, err := MarshalArgs(args...) + if err != nil { + panic(err) + } + return msg +} + // UnmarshalArgs decodes the given json.RawMessages to the variables provided // by args. Each element of args should be a pointer. // diff --git a/gopls/internal/protocol/span.go b/gopls/internal/protocol/span.go index 47d04df9d0e..2911d4aa29b 100644 --- a/gopls/internal/protocol/span.go +++ b/gopls/internal/protocol/span.go @@ -9,6 +9,12 @@ import ( "unicode/utf8" ) +// Empty reports whether the Range is an empty selection. +func (rng Range) Empty() bool { return rng.Start == rng.End } + +// Empty reports whether the Location is an empty selection. +func (loc Location) Empty() bool { return loc.Range.Empty() } + // CompareLocation defines a three-valued comparison over locations, // lexicographically ordered by (URI, Range). func CompareLocation(x, y Location) int { @@ -52,12 +58,37 @@ func ComparePosition(a, b Position) int { return 0 } -func Intersect(a, b Range) bool { - if a.Start.Line > b.End.Line || a.End.Line < b.Start.Line { - return false +// Intersect reports whether x and y intersect. +// +// Two non-empty half-open integer intervals intersect iff: +// +// y.start < x.end && x.start < y.end +// +// Mathematical conventional views an interval as a set of integers. +// An empty interval is the empty set, so its intersection with any +// other interval is empty, and thus an empty interval does not +// intersect any other interval. +// +// However, this function uses a looser definition appropriate for +// text selections: if either x or y is empty, it uses <= operators +// instead, so an empty range within or abutting a non-empty range is +// considered to overlap it, and an empty range overlaps itself. +// +// This handles the common case in which there is no selection, but +// the cursor is at the start or end of an expression and the caller +// wants to know whether the cursor intersects the range of the +// expression. The answer in this case should be yes, even though the +// selection is empty. Similarly the answer should also be yes if the +// cursor is properly within the range of the expression. But a +// non-empty selection abutting the expression should not be +// considered to intersect it. +func Intersect(x, y Range) bool { + r1 := ComparePosition(x.Start, y.End) + r2 := ComparePosition(y.Start, x.End) + if r1 < 0 && r2 < 0 { + return true // mathematical intersection } - return !((a.Start.Line == b.End.Line) && a.Start.Character > b.End.Character || - (a.End.Line == b.Start.Line) && a.End.Character < b.Start.Character) + return (x.Empty() || y.Empty()) && r1 <= 0 && r2 <= 0 } // Format implements fmt.Formatter. diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go index fe1c885b87f..e00e343850d 100644 --- a/gopls/internal/server/code_action.go +++ b/gopls/internal/server/code_action.go @@ -7,6 +7,7 @@ package server import ( "context" "fmt" + "slices" "sort" "strings" @@ -17,7 +18,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/event" ) @@ -31,56 +31,108 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara } defer release() uri := fh.URI() - - // Determine the supported actions for this file kind. kind := snapshot.FileKind(fh) - supportedCodeActions, ok := snapshot.Options().SupportedCodeActions[kind] - if !ok { - return nil, fmt.Errorf("no supported code actions for %v file kind", kind) - } - if len(supportedCodeActions) == 0 { - return nil, nil // not an error if there are none supported - } - // The Only field of the context specifies which code actions the client wants. - // If Only is empty, assume that the client wants all of the non-explicit code actions. - want := supportedCodeActions + // Determine the supported code action kinds for this file. + // + // We interpret CodeActionKinds hierarchically, so refactor.rewrite + // subsumes refactor.rewrite.change_quote, for example, + // and "" (protocol.Empty) subsumes all kinds. + // See ../protocol/codeactionkind.go for some code action theory. + // + // The Context.Only field specifies which code actions + // the client wants. According to LSP 3.18 textDocument_codeAction, + // an Only=[] should be interpreted as Only=["quickfix"]: + // + // "In version 1.0 of the protocol, there weren’t any + // source or refactoring code actions. Code actions + // were solely used to (quick) fix code, not to + // write/rewrite code. So if a client asks for code + // actions without any kind, the standard quick fix + // code actions should be returned." + // + // However, this would deny clients (e.g. Vim+coc.nvim, + // Emacs+eglot, and possibly others) the easiest and most + // natural way of querying the server for the entire set of + // available code actions. But reporting all available code + // actions would be a nuisance for VS Code, since mere cursor + // motion into a region with a code action (~anywhere) would + // trigger a lightbulb usually associated with quickfixes. + // + // As a compromise, we use the trigger kind as a heuristic: if + // the query was triggered by cursor motion (Automatic), we + // respond with only quick fixes; if the query was invoked + // explicitly (Invoked), we respond with all available + // actions. + codeActionKinds := make(map[protocol.CodeActionKind]bool) if len(params.Context.Only) > 0 { - want = make(map[protocol.CodeActionKind]bool) + for _, kind := range params.Context.Only { // kind may be "" (=> all) + codeActionKinds[kind] = true + } + } else { + // No explicit kind specified. + // Heuristic: decide based on trigger. + if triggerKind(params) == protocol.CodeActionAutomatic { + // e.g. cursor motion: show only quick fixes + codeActionKinds[protocol.QuickFix] = true + } else { + // e.g. a menu selection (or unknown trigger kind, + // as in our tests): show all available code actions. + codeActionKinds[protocol.Empty] = true + } + } - // Explicit Code Actions are opt-in and shouldn't be - // returned to the client unless requested using Only. + // enabled reports whether the specified kind of code action is required. + enabled := func(kind protocol.CodeActionKind) bool { + // Given "refactor.rewrite.foo", check for it, + // then "refactor.rewrite", "refactor", then "". + // A false map entry prunes the search for ancestors. // - // This mechanim exists to avoid a distracting - // lightbulb (code action) on each Test function. - // These actions are unwanted in VS Code because it - // has Test Explorer, and in other editors because - // the UX of executeCommand is unsatisfactory for tests: - // it doesn't show the complete streaming output. - // See https://github.com/joaotavora/eglot/discussions/1402 - // for a better solution. - explicit := map[protocol.CodeActionKind]bool{ - settings.GoTest: true, - } + // If codeActionKinds contains protocol.Empty (""), + // all kinds are enabled. + for { + if v, ok := codeActionKinds[kind]; ok { + return v + } + if kind == "" { + return false + } - for _, only := range params.Context.Only { - for k, v := range supportedCodeActions { - if only == k || strings.HasPrefix(string(k), string(only)+".") { - want[k] = want[k] || v - } + // The "source.test" code action shouldn't be + // returned to the client unless requested by + // an exact match in Only. + // + // This mechanism exists to avoid a distracting + // lightbulb (code action) on each Test function. + // These actions are unwanted in VS Code because it + // has Test Explorer, and in other editors because + // the UX of executeCommand is unsatisfactory for tests: + // it doesn't show the complete streaming output. + // See https://github.com/joaotavora/eglot/discussions/1402 + // for a better solution. See also + // https://github.com/golang/go/issues/67400. + // + // TODO(adonovan): consider instead switching on + // codeActionTriggerKind. Perhaps other noisy Source + // Actions should be guarded in the same way. + if kind == settings.GoTest { + return false // don't search ancestors + } + + // Try the parent. + if dot := strings.LastIndexByte(string(kind), '.'); dot >= 0 { + kind = kind[:dot] // "refactor.foo" -> "refactor" + } else { + kind = "" // "refactor" -> "" } - want[only] = want[only] || explicit[only] } } - if len(want) == 0 { - return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only) - } switch kind { case file.Mod: var actions []protocol.CodeAction - fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, want) + fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, enabled) if err != nil { return nil, err } @@ -117,17 +169,13 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara // Note s.codeActionsMatchingDiagnostics returns only fixes // detected during the analysis phase. golang.CodeActions computes // extra changes that can address some diagnostics. - actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, params.Context.Diagnostics, want) + actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, params.Context.Diagnostics, enabled) if err != nil { return nil, err } // computed code actions (may include quickfixes from diagnostics) - trigger := protocol.CodeActionUnknownTrigger - if k := params.Context.TriggerKind; k != nil { // (some clients omit it) - trigger = *k - } - moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, want, trigger) + moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, enabled, triggerKind(params)) if err != nil { return nil, err } @@ -159,6 +207,13 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara } } +func triggerKind(params *protocol.CodeActionParams) protocol.CodeActionTriggerKind { + if kind := params.Context.TriggerKind; kind != nil { // (some clients omit it) + return *kind + } + return protocol.CodeActionUnknownTrigger +} + // ResolveCodeAction resolves missing Edit information (that is, computes the // details of the necessary patch) in the given code action using the provided // Data field of the CodeAction, which should contain the raw json of a protocol.Command. @@ -206,14 +261,14 @@ func (s *server) ResolveCodeAction(ctx context.Context, ca *protocol.CodeAction) // protocol.Diagnostic.Data field or, if there were none, by creating // actions from edits associated with a matching Diagnostic from the // set of stored diagnostics for this file. -func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protocol.DocumentURI, snapshot *cache.Snapshot, pds []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { +func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protocol.DocumentURI, snapshot *cache.Snapshot, pds []protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool) ([]protocol.CodeAction, error) { var actions []protocol.CodeAction var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field for _, pd := range pds { bundled := cache.BundledLazyFixes(pd) if len(bundled) > 0 { for _, fix := range bundled { - if want[fix.Kind] { + if enabled(fix.Kind) { actions = append(actions, fix) } } @@ -225,7 +280,7 @@ func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protoco for _, pd := range unbundled { for _, sd := range s.findMatchingDiagnostics(uri, pd) { - diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, &pd, want) + diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, &pd, enabled) if err != nil { return nil, err } @@ -235,10 +290,10 @@ func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protoco return actions, nil } -func codeActionsForDiagnostic(ctx context.Context, snapshot *cache.Snapshot, sd *cache.Diagnostic, pd *protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { +func codeActionsForDiagnostic(ctx context.Context, snapshot *cache.Snapshot, sd *cache.Diagnostic, pd *protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool) ([]protocol.CodeAction, error) { var actions []protocol.CodeAction for _, fix := range sd.SuggestedFixes { - if !want[fix.ActionKind] { + if !enabled(fix.ActionKind) { continue } var changes []protocol.DocumentChange diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 98e4bf92e32..4f6f24d869f 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -17,6 +17,7 @@ import ( "regexp" "runtime" "runtime/pprof" + "slices" "sort" "strings" "sync" @@ -144,18 +145,20 @@ func (h *commandHandler) Modules(ctx context.Context, args command.ModulesArgs) } func (h *commandHandler) Packages(ctx context.Context, args command.PackagesArgs) (command.PackagesResult, error) { - wantTests := args.Mode&command.NeedTests != 0 - result := command.PackagesResult{ - Module: make(map[string]command.Module), + // Convert file arguments into directories + dirs := make([]protocol.DocumentURI, len(args.Files)) + for i, file := range args.Files { + if filepath.Ext(file.Path()) == ".go" { + dirs[i] = file.Dir() + } else { + dirs[i] = file + } } keepPackage := func(pkg *metadata.Package) bool { for _, file := range pkg.GoFiles { - for _, arg := range args.Files { - if file == arg || file.Dir() == arg { - return true - } - if args.Recursive && arg.Encloses(file) { + for _, dir := range dirs { + if file.Dir() == dir || args.Recursive && dir.Encloses(file) { return true } } @@ -163,27 +166,8 @@ func (h *commandHandler) Packages(ctx context.Context, args command.PackagesArgs return false } - buildPackage := func(snapshot *cache.Snapshot, meta *metadata.Package) (command.Package, command.Module) { - if wantTests { - // These will be used in the next CL to query tests - _, _ = ctx, snapshot - panic("unimplemented") - } - - pkg := command.Package{ - Path: string(meta.PkgPath), - } - if meta.Module == nil { - return pkg, command.Module{} - } - - mod := command.Module{ - Path: meta.Module.Path, - Version: meta.Module.Version, - GoMod: protocol.URIFromPath(meta.Module.GoMod), - } - pkg.ModulePath = mod.Path - return pkg, mod + result := command.PackagesResult{ + Module: make(map[string]command.Module), } err := h.run(ctx, commandConfig{ @@ -201,20 +185,67 @@ func (h *commandHandler) Packages(ctx context.Context, args command.PackagesArgs return err } + // Filter out unwanted packages + metas = slices.DeleteFunc(metas, func(meta *metadata.Package) bool { + return meta.IsIntermediateTestVariant() || + !keepPackage(meta) + }) + + start := len(result.Packages) for _, meta := range metas { - if meta.IsIntermediateTestVariant() { - continue - } - if !keepPackage(meta) { - continue + var mod command.Module + if meta.Module != nil { + mod = command.Module{ + Path: meta.Module.Path, + Version: meta.Module.Version, + GoMod: protocol.URIFromPath(meta.Module.GoMod), + } + result.Module[mod.Path] = mod // Overwriting is ok } - pkg, mod := buildPackage(snapshot, meta) - result.Packages = append(result.Packages, pkg) + result.Packages = append(result.Packages, command.Package{ + Path: string(meta.PkgPath), + ForTest: string(meta.ForTest), + ModulePath: mod.Path, + }) + } + + if args.Mode&command.NeedTests == 0 { + continue + } - // Overwriting is ok - if mod.Path != "" { - result.Module[mod.Path] = mod + // Make a single request to the index (per snapshot) to minimize the + // performance hit + var ids []cache.PackageID + for _, meta := range metas { + ids = append(ids, meta.ID) + } + + allTests, err := snapshot.Tests(ctx, ids...) + if err != nil { + return err + } + + for i, tests := range allTests { + pkg := &result.Packages[start+i] + fileByPath := map[protocol.DocumentURI]*command.TestFile{} + for _, test := range tests.All() { + test := command.TestCase{ + Name: test.Name, + Loc: test.Location, + } + + file, ok := fileByPath[test.Loc.URI] + if !ok { + f := command.TestFile{ + URI: test.Loc.URI, + } + i := len(pkg.TestFiles) + pkg.TestFiles = append(pkg.TestFiles, f) + file = &pkg.TestFiles[i] + fileByPath[test.Loc.URI] = file + } + file.Tests = append(file.Tests, test) } } } @@ -358,9 +389,9 @@ func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs var result *protocol.WorkspaceEdit err := c.run(ctx, commandConfig{ // Note: no progress here. Applying fixes should be quick. - forURI: args.URI, + forURI: args.Location.URI, }, func(ctx context.Context, deps commandDeps) error { - changes, err := golang.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Range) + changes, err := golang.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Location.Range) if err != nil { return err } @@ -648,16 +679,21 @@ func (c *commandHandler) Test(ctx context.Context, uri protocol.DocumentURI, tes }) } -func (c *commandHandler) Doc(ctx context.Context, loc protocol.Location) error { - return c.run(ctx, commandConfig{ +func (c *commandHandler) Doc(ctx context.Context, args command.DocArgs) (protocol.URI, error) { + if args.Location.URI == "" { + return "", errors.New("missing location URI") + } + + var result protocol.URI + err := c.run(ctx, commandConfig{ progress: "", // the operation should be fast - forURI: loc.URI, + forURI: args.Location.URI, }, func(ctx context.Context, deps commandDeps) error { - pkg, pgf, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, loc.URI) + pkg, pgf, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, args.Location.URI) if err != nil { return err } - start, end, err := pgf.RangePos(loc.Range) + start, end, err := pgf.RangePos(args.Location.Range) if err != nil { return err } @@ -673,11 +709,14 @@ func (c *commandHandler) Doc(ctx context.Context, loc protocol.Location) error { pkgpath, fragment, _ := golang.DocFragment(pkg, pgf, start, end) // Direct the client to open the /pkg page. - url := web.PkgURL(deps.snapshot.View().ID(), pkgpath, fragment) - openClientBrowser(ctx, c.s.client, url) + result = web.PkgURL(deps.snapshot.View().ID(), pkgpath, fragment) + if args.ShowDocument { + openClientBrowser(ctx, c.s.client, result) + } return nil }) + return result, err } func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs) error { diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go index 3770a735ff8..f4a32d708e2 100644 --- a/gopls/internal/server/diagnostics.go +++ b/gopls/internal/server/diagnostics.go @@ -26,7 +26,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/template" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/work" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" @@ -103,7 +103,7 @@ func sortDiagnostics(d []*cache.Diagnostic) { func (s *server) diagnoseChangedViews(ctx context.Context, modID uint64, lastChange map[*cache.View][]protocol.DocumentURI, cause ModificationSource) { // Collect views needing diagnosis. s.modificationMu.Lock() - needsDiagnosis := maps.Keys(s.viewsToDiagnose) + needsDiagnosis := moremaps.KeySlice(s.viewsToDiagnose) s.modificationMu.Unlock() // Diagnose views concurrently. @@ -192,17 +192,6 @@ func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot, // file modifications. // // The second phase runs after the delay, and does everything. - // - // We wait a brief delay before the first phase, to allow higher priority - // work such as autocompletion to acquire the type checking mutex (though - // typically both diagnosing changed files and performing autocompletion - // will be doing the same work: recomputing active packages). - const minDelay = 20 * time.Millisecond - select { - case <-time.After(minDelay): - case <-ctx.Done(): - return - } if len(changedURIs) > 0 { diagnostics, err := s.diagnoseChangedFiles(ctx, snapshot, changedURIs) @@ -215,12 +204,6 @@ func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot, s.updateDiagnostics(ctx, snapshot, diagnostics, false) } - if delay < minDelay { - delay = 0 - } else { - delay -= minDelay - } - select { case <-time.After(delay): case <-ctx.Done(): @@ -288,7 +271,7 @@ func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snaps toDiagnose[meta.ID] = meta } } - diags, err := snapshot.PackageDiagnostics(ctx, maps.Keys(toDiagnose)...) + diags, err := snapshot.PackageDiagnostics(ctx, moremaps.KeySlice(toDiagnose)...) if err != nil { if ctx.Err() == nil { event.Error(ctx, "warning: diagnostics failed", err, snapshot.Labels()...) @@ -495,7 +478,7 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa go func() { defer wg.Done() var err error - pkgDiags, err = snapshot.PackageDiagnostics(ctx, maps.Keys(toDiagnose)...) + pkgDiags, err = snapshot.PackageDiagnostics(ctx, moremaps.KeySlice(toDiagnose)...) if err != nil { event.Error(ctx, "warning: diagnostics failed", err, snapshot.Labels()...) } @@ -511,7 +494,7 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa // if err is non-nil (though as of today it's OK). analysisDiags, err = golang.Analyze(ctx, snapshot, toAnalyze, s.progress) if err != nil { - event.Error(ctx, "warning: analyzing package", err, append(snapshot.Labels(), label.Package.Of(keys.Join(maps.Keys(toDiagnose))))...) + event.Error(ctx, "warning: analyzing package", err, append(snapshot.Labels(), label.Package.Of(keys.Join(moremaps.KeySlice(toDiagnose))))...) return } }() diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index 08b65b1bc84..e330bd5bbc3 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go @@ -29,7 +29,7 @@ import ( "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/goversion" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" ) @@ -372,7 +372,7 @@ func (s *server) updateWatchedDirectories(ctx context.Context) error { defer s.watchedGlobPatternsMu.Unlock() // Nothing to do if the set of workspace directories is unchanged. - if maps.SameKeys(s.watchedGlobPatterns, patterns) { + if moremaps.SameKeys(s.watchedGlobPatterns, patterns) { return nil } @@ -472,13 +472,24 @@ func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, nam // Increment folder counters. switch { case env.GOTOOLCHAIN == "auto" || strings.Contains(env.GOTOOLCHAIN, "+auto"): - counter.New("gopls/gotoolchain:auto").Inc() + counter.Inc("gopls/gotoolchain:auto") case env.GOTOOLCHAIN == "path" || strings.Contains(env.GOTOOLCHAIN, "+path"): - counter.New("gopls/gotoolchain:path").Inc() + counter.Inc("gopls/gotoolchain:path") case env.GOTOOLCHAIN == "local": // local+auto and local+path handled above - counter.New("gopls/gotoolchain:local").Inc() + counter.Inc("gopls/gotoolchain:local") default: - counter.New("gopls/gotoolchain:other").Inc() + counter.Inc("gopls/gotoolchain:other") + } + + // Record whether a driver is in use so that it appears in the + // user's telemetry upload. Although we can't correlate the + // driver information with the crash or bug.Report at the + // granularity of the process instance, users that use a + // driver tend to do so most of the time, so we'll get a + // strong clue. See #60890 for an example of an issue where + // this information would have been helpful. + if env.EffectiveGOPACKAGESDRIVER != "" { + counter.Inc("gopls/gopackagesdriver") } return &cache.Folder{ diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go index bbf3c75bc83..86fa4766b51 100644 --- a/gopls/internal/settings/analysis.go +++ b/gopls/internal/settings/analysis.go @@ -55,7 +55,6 @@ import ( "golang.org/x/tools/gopls/internal/analysis/simplifycompositelit" "golang.org/x/tools/gopls/internal/analysis/simplifyrange" "golang.org/x/tools/gopls/internal/analysis/simplifyslice" - "golang.org/x/tools/gopls/internal/analysis/stubmethods" "golang.org/x/tools/gopls/internal/analysis/undeclaredname" "golang.org/x/tools/gopls/internal/analysis/unusedparams" "golang.org/x/tools/gopls/internal/analysis/unusedvariable" @@ -202,7 +201,6 @@ func init() { {analyzer: fillreturns.Analyzer, enabled: true}, {analyzer: nonewvars.Analyzer, enabled: true}, {analyzer: noresultvalues.Analyzer, enabled: true}, - {analyzer: stubmethods.Analyzer, enabled: true}, {analyzer: undeclaredname.Analyzer, enabled: true}, // TODO(rfindley): why isn't the 'unusedvariable' analyzer enabled, if it // is only enhancing type errors with suggested fixes? @@ -216,7 +214,3 @@ func init() { DefaultAnalyzers[analyzer.analyzer.Name] = analyzer } } - -// StaticcheckAnalzyers describes available Staticcheck analyzers, keyed by -// analyzer name. -var StaticcheckAnalyzers = make(map[string]*Analyzer) // written by analysis_.go diff --git a/gopls/internal/settings/analysis_119.go b/gopls/internal/settings/analysis_119.go deleted file mode 100644 index 4493a380237..00000000000 --- a/gopls/internal/settings/analysis_119.go +++ /dev/null @@ -1,10 +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.20 -// +build !go1.20 - -package settings - -const StaticcheckSupported = false diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index 7cc13229279..fa06b90e7e3 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go @@ -12,16 +12,29 @@ import "golang.org/x/tools/gopls/internal/protocol" // // See ../protocol/tsprotocol.go for LSP standard kinds, including // -// "quickfix" -// "refactor" -// "refactor.extract" -// "refactor.inline" -// "refactor.move" -// "refactor.rewrite" -// "source" -// "source.organizeImports" -// "source.fixAll" -// "notebook" +// quickfix +// refactor +// refactor.extract +// refactor.inline +// refactor.move +// refactor.rewrite +// source +// source.organizeImports +// source.fixAll +// notebook +// +// Kinds are hierarchical: "refactor" subsumes "refactor.inline", +// which subsumes "refactor.inline.call". This rule implies that the +// empty string, confusingly named protocol.Empty, subsumes all kinds. +// The "Only" field in a CodeAction request may specify a category +// such as "refactor"; any matching code action will be returned. +// +// All CodeActions returned by gopls use a specific leaf kind such as +// "refactor.inline.call", except for quick fixes, which all use +// "quickfix". TODO(adonovan): perhaps quick fixes should also be +// hierarchical (e.g. quickfix.govulncheck.{reset,upgrade})? +// +// # VS Code // // The effects of CodeActionKind on the behavior of VS Code are // baffling and undocumented. Here's what we have observed. @@ -29,10 +42,16 @@ import "golang.org/x/tools/gopls/internal/protocol" // Clicking on the "Refactor..." menu item shows a submenu of actions // with kind="refactor.*", and clicking on "Source action..." shows // actions with kind="source.*". A lightbulb appears in both cases. +// // A third menu, "Quick fix...", not found on the usual context // menu but accessible through the command palette or "⌘.", -// displays code actions of kind "quickfix.*" and "refactor.*", -// and ad hoc ones ("More actions...") such as "gopls.*". +// does not set the Only field in its request, so the set of +// kinds is determined by how the server interprets the default. +// The LSP 3.18 guidance is that this should be treated +// equivalent to Only=["quickfix"], and that is what gopls +// now does. (If the server responds with more kinds, they will +// be displayed in menu subsections.) +// // All of these CodeAction requests have triggerkind=Invoked. // // Cursor motion also performs a CodeAction request, but with @@ -48,38 +67,43 @@ import "golang.org/x/tools/gopls/internal/protocol" // // In all these menus, VS Code organizes the actions' menu items // into groups based on their kind, with hardwired captions such as -// "Extract", "Inline", "More actions", and "Quick fix". +// "Refactor...", "Extract", "Inline", "More actions", and "Quick fix". // // The special category "source.fixAll" is intended for actions that // are unambiguously safe to apply so that clients may automatically // apply all actions matching this category on save. (That said, this // is not VS Code's default behavior; see editor.codeActionsOnSave.) -// -// TODO(adonovan): the intent of CodeActionKind is a hierarchy. We -// should changes gopls so that we don't create instances of the -// predefined kinds directly, but treat them as interfaces. -// -// For example, -// -// instead of: we should create: -// refactor.extract refactor.extract.const -// refactor.extract.var -// refactor.extract.func -// refactor.rewrite refactor.rewrite.fillstruct -// refactor.rewrite.unusedparam -// quickfix quickfix.govulncheck.reset -// quickfix.govulncheck.upgrade -// -// etc, so that client editors and scripts can be more specific in -// their requests. -// -// This entails that we use a segmented-path matching operator -// instead of == for CodeActionKinds throughout gopls. -// See golang/go#40438 for related discussion. const ( - GoAssembly protocol.CodeActionKind = "source.assembly" - GoDoc protocol.CodeActionKind = "source.doc" - GoFreeSymbols protocol.CodeActionKind = "source.freesymbols" - GoTest protocol.CodeActionKind = "source.test" + // source + GoAssembly protocol.CodeActionKind = "source.assembly" + GoDoc protocol.CodeActionKind = "source.doc" + GoFreeSymbols protocol.CodeActionKind = "source.freesymbols" + GoTest protocol.CodeActionKind = "source.test" + + // gopls + // TODO(adonovan): we should not use this category as it will + // never be requested now that we no longer interpret "no kind + // restriction" as "quickfix" instead of "all kinds". + // We need another way to make docs discoverable. GoplsDocFeatures protocol.CodeActionKind = "gopls.doc.features" + + // refactor.rewrite + RefactorRewriteChangeQuote protocol.CodeActionKind = "refactor.rewrite.changeQuote" + RefactorRewriteFillStruct protocol.CodeActionKind = "refactor.rewrite.fillStruct" + RefactorRewriteFillSwitch protocol.CodeActionKind = "refactor.rewrite.fillSwitch" + RefactorRewriteInvertIf protocol.CodeActionKind = "refactor.rewrite.invertIf" + RefactorRewriteJoinLines protocol.CodeActionKind = "refactor.rewrite.joinLines" + RefactorRewriteRemoveUnusedParam protocol.CodeActionKind = "refactor.rewrite.removeUnusedParam" + RefactorRewriteSplitLines protocol.CodeActionKind = "refactor.rewrite.splitLines" + + // refactor.inline + RefactorInlineCall protocol.CodeActionKind = "refactor.inline.call" + + // refactor.extract + RefactorExtractFunction protocol.CodeActionKind = "refactor.extract.function" + RefactorExtractMethod protocol.CodeActionKind = "refactor.extract.method" + RefactorExtractVariable protocol.CodeActionKind = "refactor.extract.variable" + RefactorExtractToNewFile protocol.CodeActionKind = "refactor.extract.toNewFile" + + // Note: add new kinds to the SupportedCodeActions map in defaults.go too. ) diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index 9641613cd5d..25f3eae80f5 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -43,17 +43,29 @@ func DefaultOptions(overrides ...func(*Options)) *Options { ServerOptions: ServerOptions{ SupportedCodeActions: map[file.Kind]map[protocol.CodeActionKind]bool{ file.Go: { - protocol.SourceFixAll: true, - protocol.SourceOrganizeImports: true, - protocol.QuickFix: true, - protocol.RefactorRewrite: true, - protocol.RefactorInline: true, - protocol.RefactorExtract: true, - GoAssembly: true, - GoDoc: true, - GoFreeSymbols: true, + // This should include specific leaves in the tree, + // (e.g. refactor.inline.call) not generic branches + // (e.g. refactor.inline or refactor). + protocol.SourceFixAll: true, + protocol.SourceOrganizeImports: true, + protocol.QuickFix: true, + GoAssembly: true, + GoDoc: true, + GoFreeSymbols: true, + GoplsDocFeatures: true, + RefactorRewriteChangeQuote: true, + RefactorRewriteFillStruct: true, + RefactorRewriteFillSwitch: true, + RefactorRewriteInvertIf: true, + RefactorRewriteJoinLines: true, + RefactorRewriteRemoveUnusedParam: true, + RefactorRewriteSplitLines: true, + RefactorInlineCall: true, + RefactorExtractFunction: true, + RefactorExtractMethod: true, + RefactorExtractVariable: true, + RefactorExtractToNewFile: true, // Not GoTest: it must be explicit in CodeActionParams.Context.Only - GoplsDocFeatures: true, }, file.Mod: { protocol.SourceOrganizeImports: true, diff --git a/gopls/internal/settings/gofumpt_119.go b/gopls/internal/settings/gofumpt_119.go deleted file mode 100644 index 5734802c686..00000000000 --- a/gopls/internal/settings/gofumpt_119.go +++ /dev/null @@ -1,14 +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.20 -// +build !go1.20 - -package settings - -import "context" - -const GofumptSupported = false - -var GofumptFormat func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) diff --git a/gopls/internal/settings/gofumpt_120.go b/gopls/internal/settings/gofumpt_120.go deleted file mode 100644 index eebf1f77b57..00000000000 --- a/gopls/internal/settings/gofumpt_120.go +++ /dev/null @@ -1,80 +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.20 -// +build go1.20 - -package settings - -import ( - "context" - "fmt" - - "mvdan.cc/gofumpt/format" -) - -const GofumptSupported = true - -// GofumptFormat allows the gopls module to wire in a call to -// gofumpt/format.Source. langVersion and modulePath are used for some -// Gofumpt formatting rules -- see the Gofumpt documentation for details. -var GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { - fixedVersion, err := fixLangVersion(langVersion) - if err != nil { - return nil, err - } - return format.Source(src, format.Options{ - LangVersion: fixedVersion, - ModulePath: modulePath, - }) -} - -// fixLangVersion function cleans the input so that gofumpt doesn't panic. It is -// rather permissive, and accepts version strings that aren't technically valid -// in a go.mod file. -// -// More specifically, it looks for an optional 'v' followed by 1-3 -// '.'-separated numbers. The resulting string is stripped of any suffix beyond -// this expected version number pattern. -// -// See also golang/go#61692: gofumpt does not accept the new language versions -// appearing in go.mod files (e.g. go1.21rc3). -func fixLangVersion(input string) (string, error) { - bad := func() (string, error) { - return "", fmt.Errorf("invalid language version syntax %q", input) - } - if input == "" { - return input, nil - } - i := 0 - if input[0] == 'v' { // be flexible about 'v' - i++ - } - // takeDigits consumes ascii numerals 0-9 and reports if at least one was - // consumed. - takeDigits := func() bool { - found := false - for ; i < len(input) && '0' <= input[i] && input[i] <= '9'; i++ { - found = true - } - return found - } - if !takeDigits() { // versions must start with at least one number - return bad() - } - - // Accept optional minor and patch versions. - for n := 0; n < 2; n++ { - if i < len(input) && input[i] == '.' { - // Look for minor/patch version. - i++ - if !takeDigits() { - i-- - break - } - } - } - // Accept any suffix. - return input[:i], nil -} diff --git a/gopls/internal/settings/gofumpt_120_test.go b/gopls/internal/settings/gofumpt_120_test.go deleted file mode 100644 index 7ed54d5c888..00000000000 --- a/gopls/internal/settings/gofumpt_120_test.go +++ /dev/null @@ -1,53 +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.20 -// +build go1.20 - -package settings - -import "testing" - -func TestFixLangVersion(t *testing.T) { - tests := []struct { - input, want string - wantErr bool - }{ - {"", "", false}, - {"1.18", "1.18", false}, - {"v1.18", "v1.18", false}, - {"1.21", "1.21", false}, - {"1.21rc3", "1.21", false}, - {"1.21.0", "1.21.0", false}, - {"1.21.1", "1.21.1", false}, - {"v1.21.1", "v1.21.1", false}, - {"v1.21.0rc1", "v1.21.0", false}, // not technically valid, but we're flexible - {"v1.21.0.0", "v1.21.0", false}, // also technically invalid - {"1.1", "1.1", false}, - {"v1", "v1", false}, - {"1", "1", false}, - {"v1.21.", "v1.21", false}, // also invalid - {"1.21.", "1.21", false}, - - // Error cases. - {"rc1", "", true}, - {"x1.2.3", "", true}, - } - - for _, test := range tests { - got, err := fixLangVersion(test.input) - if test.wantErr { - if err == nil { - t.Errorf("fixLangVersion(%q) succeeded unexpectedly", test.input) - } - continue - } - if err != nil { - t.Fatalf("fixLangVersion(%q) failed: %v", test.input, err) - } - if got != test.want { - t.Errorf("fixLangVersion(%q) = %s, want %s", test.input, got, test.want) - } - } -} diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 2cd504b2555..719d0690b5a 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -6,15 +6,14 @@ package settings import ( "fmt" + "maps" "path/filepath" - "runtime" "strings" "time" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/frob" - "golang.org/x/tools/gopls/internal/util/maps" ) type Annotation string @@ -118,7 +117,7 @@ type BuildOptions struct { // Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules` DirectoryFilters []string - // TemplateExtensions gives the extensions of file names that are treateed + // TemplateExtensions gives the extensions of file names that are treated // as template files. (The extension // is the part of the file name after the final dot.) TemplateExtensions []string @@ -1060,6 +1059,9 @@ func (o *Options) setOne(name string, value any) error { if err != nil { return err } + if o.Codelenses == nil { + o.Codelenses = make(map[CodeLensSource]bool) + } o.Codelenses = maps.Clone(o.Codelenses) for source, enabled := range lensOverrides { o.Codelenses[source] = enabled @@ -1070,15 +1072,7 @@ func (o *Options) setOne(name string, value any) error { } case "staticcheck": - v, err := asBool(value) - if err != nil { - return err - } - if v && !StaticcheckSupported { - return fmt.Errorf("staticcheck is not supported at %s;"+ - " rebuild gopls with a more recent version of Go", runtime.Version()) - } - o.Staticcheck = v + return setBool(&o.Staticcheck, value) case "local": return setString(&o.Local, value) @@ -1093,15 +1087,7 @@ func (o *Options) setOne(name string, value any) error { return setBool(&o.ShowBugReports, value) case "gofumpt": - v, err := asBool(value) - if err != nil { - return err - } - if v && !GofumptSupported { - return fmt.Errorf("gofumpt is not supported at %s;"+ - " rebuild gopls with a more recent version of Go", runtime.Version()) - } - o.Gofumpt = v + return setBool(&o.Gofumpt, value) case "completeFunctionCalls": return setBool(&o.CompleteFunctionCalls, value) diff --git a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go index e2375222639..6f865083a9d 100644 --- a/gopls/internal/settings/settings_test.go +++ b/gopls/internal/settings/settings_test.go @@ -199,15 +199,6 @@ func TestOptions_Set(t *testing.T) { }, } - if !StaticcheckSupported { - tests = append(tests, testCase{ - name: "staticcheck", - value: true, - check: func(o Options) bool { return o.Staticcheck == true }, - wantError: true, // o.StaticcheckSupported is unset - }) - } - for _, test := range tests { var opts Options err := opts.Set(map[string]any{test.name: test.value}) diff --git a/gopls/internal/settings/analysis_120.go b/gopls/internal/settings/staticcheck.go similarity index 90% rename from gopls/internal/settings/analysis_120.go rename to gopls/internal/settings/staticcheck.go index 6a53f365eaa..fca3e55f17e 100644 --- a/gopls/internal/settings/analysis_120.go +++ b/gopls/internal/settings/staticcheck.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.20 -// +build go1.20 - package settings import ( @@ -16,7 +13,9 @@ import ( "honnef.co/go/tools/stylecheck" ) -const StaticcheckSupported = true +// StaticcheckAnalzyers describes available Staticcheck analyzers, keyed by +// analyzer name. +var StaticcheckAnalyzers = make(map[string]*Analyzer) // written by analysis_.go func init() { mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go index 3da90f81f4b..f3e7fae359b 100644 --- a/gopls/internal/telemetry/cmd/stacks/stacks.go +++ b/gopls/internal/telemetry/cmd/stacks/stacks.go @@ -2,39 +2,97 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build linux || darwin + // The stacks command finds all gopls stack traces reported by // telemetry in the past 7 days, and reports their associated GitHub // issue, creating new issues as needed. +// +// The association of stacks with GitHub issues (labelled +// gopls/telemetry-wins) is represented in two different ways by the +// body (first comment) of the issue: +// +// 1. Each distinct stack is identified by an ID, 6-digit base64 +// string such as "TwtkSg". If a stack's ID appears anywhere +// within the issue body, the stack is associated with the issue. +// +// Some problems are highly deterministic, resulting in many +// field reports of the exact same stack. For such problems, a +// single ID in the issue body suffices to record the +// association. But most problems are exhibited in a variety of +// ways, leading to multiple field reports of similar but +// distinct stacks. +// +// 2. Each GitHub issue body may start with a code block of this form: +// +// ``` +// #!stacks +// "runtime.sigpanic" && "golang.hover:+170" +// ``` +// +// The first line indicates the purpose of the block; the +// remainder is a predicate that matches stacks. +// It is an expression defined by this grammar: +// +// > expr = "string literal" +// > | ( expr ) +// > | ! expr +// > | expr && expr +// > | expr || expr +// +// Each string literal implies a substring match on the stack; +// the other productions are boolean operations. +// +// The stacks command gathers all such predicates out of the +// labelled issues and evaluates each one against each new stack. +// If the predicate for an issue matches, the issue is considered +// to have "claimed" the stack: the stack command appends a +// comment containing the new (variant) stack to the issue, and +// appends the stack's ID to the last line of the issue body. +// +// It is an error if two issues' predicates attempt to claim the +// same stack. package main +// TODO(adonovan): create a proper package with tests. Much of this +// machinery might find wider use in other x/telemetry clients. + import ( "bytes" + "context" "encoding/base64" "encoding/json" "flag" "fmt" + "go/ast" + "go/parser" + "go/token" "hash/fnv" + "io" "log" "net/http" "net/url" "os" + "os/exec" "path/filepath" + "runtime" "sort" + "strconv" "strings" "time" + "unicode" - "io" - + "golang.org/x/sys/unix" "golang.org/x/telemetry" "golang.org/x/tools/gopls/internal/util/browser" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" ) // flags var ( daysFlag = flag.Int("days", 7, "number of previous days of telemetry data to read") - token string // optional GitHub authentication token, to relax the rate limit + authToken string // mandatory GitHub authentication token (for R/W issues access) ) func main() { @@ -46,8 +104,8 @@ func main() { // // You can create one using the flow at: GitHub > You > Settings > // Developer Settings > Personal Access Tokens > Fine-grained tokens > - // Generate New Token. Generate the token on behalf of yourself - // (not "golang" or "google"), with no special permissions. + // Generate New Token. Generate the token on behalf of golang/go + // with R/W access to "Issues". // The token is typically of the form "github_pat_XXX", with 82 hex digits. // Save it in the file, with mode 0400. // @@ -64,13 +122,13 @@ func main() { if !os.IsNotExist(err) { log.Fatalf("cannot read GitHub authentication token: %v", err) } - log.Printf("no file %s containing GitHub authentication token; continuing without authentication, which is subject to stricter rate limits (https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api).", tokenFile) + log.Fatalf("no file %s containing GitHub authentication token.", tokenFile) } - token = string(bytes.TrimSpace(content)) + authToken = string(bytes.TrimSpace(content)) } - // Maps stack text to Version/GoVersion/GOOS/GOARCH string to counter. - stacks := make(map[string]map[string]int64) + // Maps stack text to Info to count. + stacks := make(map[string]map[Info]int64) var distinctStacks int // Maps stack to a telemetry URL. @@ -79,8 +137,7 @@ func main() { // Read all recent telemetry reports. t := time.Now() for i := 0; i < *daysFlag; i++ { - const DateOnly = "2006-01-02" // TODO(adonovan): use time.DateOnly in go1.20. - date := t.Add(-time.Duration(i+1) * 24 * time.Hour).Format(DateOnly) + date := t.Add(-time.Duration(i+1) * 24 * time.Hour).Format(time.DateOnly) url := fmt.Sprintf("https://storage.googleapis.com/prod-telemetry-merged/%s.json", date) resp, err := http.Get(url) @@ -114,7 +171,7 @@ func main() { } sort.Strings(clients) if len(clients) > 0 { - clientSuffix = " " + strings.Join(clients, ",") + clientSuffix = strings.Join(clients, ",") } // Ignore @devel versions as they correspond to @@ -126,14 +183,18 @@ func main() { distinctStacks++ - info := fmt.Sprintf("%s@%s %s %s/%s%s", - prog.Program, prog.Version, - prog.GoVersion, prog.GOOS, prog.GOARCH, - clientSuffix) + info := Info{ + Program: prog.Program, + Version: prog.Version, + GoVersion: prog.GoVersion, + GOOS: prog.GOOS, + GOARCH: prog.GOARCH, + Client: clientSuffix, + } for stack, count := range prog.Stacks { counts := stacks[stack] if counts == nil { - counts = make(map[string]int64) + counts = make(map[Info]int64) stacks[stack] = counts } counts[info] += count @@ -144,41 +205,141 @@ func main() { } } - // Compute IDs of all stacks. - var stackIDs []string - for stack := range stacks { - stackIDs = append(stackIDs, stackID(stack)) + // Query GitHub for all existing GitHub issues with label:gopls/telemetry-wins. + // + // TODO(adonovan): by default GitHub returns at most 30 + // issues; we have lifted this to 100 using per_page=%d, but + // that won't work forever; use paging. + const query = "is:issue label:gopls/telemetry-wins" + res, err := searchIssues(query) + if err != nil { + log.Fatalf("GitHub issues query %q failed: %v", query, err) } - // Query GitHub for existing GitHub issues. - // (Note: there may be multiple Issue records - // for the same logical issue, i.e. Issue.Number.) - issuesByStackID := make(map[string]*Issue) - for len(stackIDs) > 0 { - // For some reason GitHub returns 422 UnprocessableEntity - // if we attempt to read more than 6 at once. - batch := stackIDs[:min(6, len(stackIDs))] - stackIDs = stackIDs[len(batch):] + // Extract and validate predicate expressions in ```#!stacks...``` code blocks. + // See the package doc comment for the grammar. + for _, issue := range res.Items { + block := findPredicateBlock(issue.Body) + if block != "" { + expr, err := parser.ParseExpr(block) + if err != nil { + log.Printf("invalid predicate in issue #%d: %v\n<<%s>>", + issue.Number, err, block) + continue + } + var validate func(ast.Expr) error + validate = func(e ast.Expr) error { + switch e := e.(type) { + case *ast.UnaryExpr: + if e.Op != token.NOT { + return fmt.Errorf("invalid op: %s", e.Op) + } + return validate(e.X) - query := "is:issue label:gopls/telemetry-wins in:body " + strings.Join(batch, " OR ") - res, err := searchIssues(query) - if err != nil { - log.Fatalf("GitHub issues query %q failed: %v", query, err) - } - for _, issue := range res.Items { - for _, id := range batch { - // Matching is a little fuzzy here - // but base64 will rarely produce - // words that appear in the body - // by chance. - if strings.Contains(issue.Body, id) { - issuesByStackID[id] = issue + case *ast.BinaryExpr: + if e.Op != token.LAND && e.Op != token.LOR { + return fmt.Errorf("invalid op: %s", e.Op) + } + if err := validate(e.X); err != nil { + return err + } + return validate(e.Y) + + case *ast.ParenExpr: + return validate(e.X) + + case *ast.BasicLit: + if e.Kind != token.STRING { + return fmt.Errorf("invalid literal (%s)", e.Kind) + } + if _, err := strconv.Unquote(e.Value); err != nil { + return err + } + + default: + return fmt.Errorf("syntax error (%T)", e) } + return nil + } + if err := validate(expr); err != nil { + log.Printf("invalid predicate in issue #%d: %v\n<<%s>>", + issue.Number, err, block) + continue + } + issue.predicateText = block + issue.predicate = func(stack string) bool { + var eval func(ast.Expr) bool + eval = func(e ast.Expr) bool { + switch e := e.(type) { + case *ast.UnaryExpr: + return !eval(e.X) + + case *ast.BinaryExpr: + if e.Op == token.LAND { + return eval(e.X) && eval(e.Y) + } else { + return eval(e.X) || eval(e.Y) + } + + case *ast.ParenExpr: + return eval(e.X) + + case *ast.BasicLit: + substr, _ := strconv.Unquote(e.Value) + return strings.Contains(stack, substr) + } + panic("unreachable") + } + return eval(expr) } } } - fmt.Printf("Found %d distinct stacks in last %v days:\n", distinctStacks, *daysFlag) + // Map each stack ID to its issue. + // + // An issue can claim a stack two ways: + // + // 1. if the issue body contains the ID of the stack. Matching + // is a little loose but base64 will rarely produce words + // that appear in the body by chance. + // + // 2. if the issue body contains a ```#!stacks``` predicate + // that matches the stack. + // + // We report an error if two different issues attempt to claim + // the same stack. + // + // This is O(new stacks x existing issues). + claimedBy := make(map[string]*Issue) + for stack := range stacks { + id := stackID(stack) + for _, issue := range res.Items { + byPredicate := false + if strings.Contains(issue.Body, id) { + // nop + } else if issue.predicate != nil && issue.predicate(stack) { + byPredicate = true + } else { + continue + } + + if prev := claimedBy[id]; prev != nil && prev != issue { + log.Printf("stack %s is claimed by issues #%d and #%d", + id, prev.Number, issue.Number) + continue + } + if false { + log.Printf("stack %s claimed by issue #%d", + id, issue.Number) + } + claimedBy[id] = issue + if byPredicate { + // The stack ID matched the predicate but was not + // found in the issue body, so this is a new stack. + issue.newStacks = append(issue.newStacks, stack) + } + } + } // For each stack, show existing issue or create a new one. // Aggregate stack IDs by issue summary. @@ -195,7 +356,7 @@ func main() { total += count } - if issue, ok := issuesByStackID[id]; ok { + if issue, ok := claimedBy[id]; ok { // existing issue summary := fmt.Sprintf("#%d: %s [%s]", issue.Number, issue.Title, issue.State) @@ -207,15 +368,60 @@ func main() { newIssues[summary] += total } } + + // Update existing issues that claimed new stacks by predicate. + for _, issue := range res.Items { + if len(issue.newStacks) == 0 { + continue + } + + // Add a comment to the existing issue listing all its new stacks. + // (Save the ID of each stack for the second step.) + comment := new(bytes.Buffer) + var newStackIDs []string + for _, stack := range issue.newStacks { + id := stackID(stack) + newStackIDs = append(newStackIDs, id) + writeStackComment(comment, stack, id, stackToURL[stack], stacks[stack]) + } + if err := addIssueComment(issue.Number, comment.String()); err != nil { + log.Println(err) + continue + } + + // Append to the "Dups: ID ..." list on last line of issue body. + body := strings.TrimSpace(issue.Body) + lastLineStart := strings.LastIndexByte(body, '\n') + 1 + lastLine := body[lastLineStart:] + if !strings.HasPrefix(lastLine, "Dups:") { + body += "\nDups:" + } + body += " " + strings.Join(newStackIDs, " ") + if err := updateIssueBody(issue.Number, body); err != nil { + log.Printf("added comment to issue #%d but failed to update body: %v", + issue.Number, err) + continue + } + + log.Printf("added stacks %s to issue #%d", newStackIDs, issue.Number) + } + + fmt.Printf("Found %d distinct stacks in last %v days:\n", distinctStacks, *daysFlag) print := func(caption string, issues map[string]int64) { // Print items in descending frequency. - keys := maps.Keys(issues) + keys := moremaps.KeySlice(issues) sort.Slice(keys, func(i, j int) bool { return issues[keys[i]] > issues[keys[j]] }) fmt.Printf("%s issues:\n", caption) for _, summary := range keys { count := issues[summary] + // Show closed issues in "white". + if isTerminal(os.Stdout) && strings.Contains(summary, "[closed]") { + // ESC + "[" + n + "m" => change color to n + // (37 = white, 0 = default) + summary = "\x1B[37m" + summary + "\x1B[0m" + } fmt.Printf("%s (n=%d)\n", summary, count) } } @@ -223,12 +429,28 @@ func main() { print("New", newIssues) } +// Info is used as a key for de-duping and aggregating. +// Do not add detail about particular records (e.g. data, telemetry URL). +type Info struct { + Program string // "golang.org/x/tools/gopls" + Version, GoVersion string // e.g. "gopls/v0.16.1", "go1.23" + GOOS, GOARCH string + Client string // e.g. "vscode" +} + +func (info Info) String() string { + return fmt.Sprintf("%s@%s %s %s/%s %s", + info.Program, info.Version, + info.GoVersion, info.GOOS, info.GOARCH, + info.Client) +} + // stackID returns a 32-bit identifier for a stack // suitable for use in GitHub issue titles. func stackID(stack string) string { // Encode it using base64 (6 bytes) for brevity, // as a single issue's body might contain multiple IDs - // if separate issues with same cause wre manually de-duped, + // if separate issues with same cause were manually de-duped, // e.g. "AAAAAA, BBBBBB" // // https://hbfs.wordpress.com/2012/03/30/finding-collisions: @@ -247,11 +469,14 @@ func stackID(stack string) string { // manually de-dup the issue before deciding whether to submit the form.) // // It returns the title. -func newIssue(stack, id, jsonURL string, counts map[string]int64) string { +func newIssue(stack, id string, jsonURL string, counts map[Info]int64) string { // Use a heuristic to find a suitable symbol to blame // in the title: the first public function or method // of a public type, in gopls, to appear in the stack // trace. We can always refine it later. + // + // TODO(adonovan): include in the issue a source snippet ±5 + // lines around the PC in this symbol. var symbol string for _, line := range strings.Split(stack, "\n") { // Look for: @@ -275,38 +500,154 @@ func newIssue(stack, id, jsonURL string, counts map[string]int64) string { } // Populate the form (title, body, label) - title := fmt.Sprintf("x/tools/gopls:%s bug reported by telemetry", symbol) + title := fmt.Sprintf("x/tools/gopls: bug in %s", symbol) + body := new(bytes.Buffer) + + // Add a placeholder ```#!stacks``` block since this is a new issue. + body.WriteString("```" + ` +#!stacks +"" +` + "```\n") + fmt.Fprintf(body, "Issue created by [stacks](https://pkg.go.dev/golang.org/x/tools/gopls/internal/telemetry/cmd/stacks).\n\n") + + writeStackComment(body, stack, id, jsonURL, counts) + + const labels = "gopls,Tools,gopls/telemetry-wins,NeedsInvestigation" + + // Report it. The user will interactively finish the task, + // since they will typically de-dup it without even creating a new issue + // by expanding the #!stacks predicate of an existing issue. + if !browser.Open("https://github.com/golang/go/issues/new?labels=" + labels + "&title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body.String())) { + log.Print("Please file a new issue at golang.org/issue/new using this template:\n\n") + log.Printf("Title: %s\n", title) + log.Printf("Labels: %s\n", labels) + log.Printf("Body: %s\n", body) + } + + return title +} + +// writeStackComment writes a stack in Markdown form, for a new GitHub +// issue or new comment on an existing one. +func writeStackComment(body *bytes.Buffer, stack, id string, jsonURL string, counts map[Info]int64) { + if len(counts) == 0 { + panic("no counts") + } + var info Info // pick an arbitrary key + for info = range counts { + break + } + fmt.Fprintf(body, "This stack `%s` was [reported by telemetry](%s):\n\n", id, jsonURL) - fmt.Fprintf(body, "```\n%s\n```\n", stack) + + // Read the mapping from symbols to file/line. + pclntab, err := readPCLineTable(info) + if err != nil { + log.Fatal(err) + } + + // Parse the stack and get the symbol names out. + for _, frame := range strings.Split(stack, "\n") { + if url := frameURL(pclntab, info, frame); url != "" { + fmt.Fprintf(body, "- [`%s`](%s)\n", frame, url) + } else { + fmt.Fprintf(body, "- `%s`\n", frame) + } + } // Add counts, gopls version, and platform info. // This isn't very precise but should provide clues. - // - // TODO(adonovan): link each stack (ideally each frame) to source: - // https://cs.opensource.google/go/x/tools/+/gopls/VERSION:gopls/FILE;l=LINE - // (Requires parsing stack, shallow-cloning gopls module at that tag, and - // computing correct line offsets. Would be labor-saving though.) fmt.Fprintf(body, "```\n") for info, count := range counts { fmt.Fprintf(body, "%s (%d)\n", info, count) } fmt.Fprintf(body, "```\n\n") +} - fmt.Fprintf(body, "Issue created by golang.org/x/tools/gopls/internal/telemetry/cmd/stacks.\n") +// frameURL returns the CodeSearch URL for the stack frame, if known. +func frameURL(pclntab map[string]FileLine, info Info, frame string) string { + // e.g. "golang.org/x/tools/gopls/foo.(*Type).Method.inlined.func3:+5" + symbol, offset, ok := strings.Cut(frame, ":") + if !ok { + // Not a symbol (perhaps stack counter title: "gopls/bug"?) + return "" + } - const labels = "gopls,Tools,gopls/telemetry-wins,NeedsInvestigation" + fileline, ok := pclntab[symbol] + if !ok { + // objdump reports ELF symbol names, which in + // rare cases may be the Go symbols of + // runtime.CallersFrames mangled by (e.g.) the + // addition of .abi0 suffix; see + // https://github.com/golang/go/issues/69390#issuecomment-2343795920 + // So this should not be a hard error. + if symbol != "runtime.goexit" { + log.Printf("no pclntab info for symbol: %s", symbol) + } + return "" + } - // Report it. - if !browser.Open("https://github.com/golang/go/issues/new?labels=" + labels + "&title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body.String())) { - log.Print("Please file a new issue at golang.org/issue/new using this template:\n\n") - log.Printf("Title: %s\n", title) - log.Printf("Labels: %s\n", labels) - log.Printf("Body: %s\n", body) + if offset == "" { + log.Fatalf("missing line offset: %s", frame) + } + if unicode.IsDigit(rune(offset[0])) { + // Fix gopls/v0.14.2 legacy syntax ":%d" -> ":+%d". + offset = "+" + offset + } + offsetNum, err := strconv.Atoi(offset[1:]) + if err != nil { + log.Fatalf("invalid line offset: %s", frame) + } + linenum := fileline.line + switch offset[0] { + case '-': + linenum -= offsetNum + case '+': + linenum += offsetNum + case '=': + linenum = offsetNum } - return title + // Construct CodeSearch URL. + + // std module? + firstSegment, _, _ := strings.Cut(fileline.file, "/") + if !strings.Contains(firstSegment, ".") { + // (First segment is a dir beneath GOROOT/src, not a module domain name.) + return fmt.Sprintf("https://cs.opensource.google/go/go/+/%s:src/%s;l=%d", + info.GoVersion, fileline.file, linenum) + } + + // x/tools repo (tools or gopls module)? + if rest, ok := strings.CutPrefix(fileline.file, "golang.org/x/tools"); ok { + if rest[0] == '/' { + // "golang.org/x/tools/gopls" -> "gopls" + rest = rest[1:] + } else if rest[0] == '@' { + // "golang.org/x/tools@version/dir/file.go" -> "dir/file.go" + rest = rest[strings.Index(rest, "/")+1:] + } + + return fmt.Sprintf("https://cs.opensource.google/go/x/tools/+/%s:%s;l=%d", + "gopls/"+info.Version, rest, linenum) + } + + // other x/ module dependency? + // e.g. golang.org/x/sync@v0.8.0/errgroup/errgroup.go + if rest, ok := strings.CutPrefix(fileline.file, "golang.org/x/"); ok { + if modVer, filename, ok := strings.Cut(rest, "/"); ok { + if mod, version, ok := strings.Cut(modVer, "@"); ok { + return fmt.Sprintf("https://cs.opensource.google/go/x/%s/+/%s:%s;l=%d", + mod, version, filename, linenum) + } + } + } + + log.Printf("no CodeSearch URL for %q (%s:%d)", + symbol, fileline.file, linenum) + return "" } // -- GitHub search -- @@ -315,34 +656,88 @@ func newIssue(stack, id, jsonURL string, counts map[string]int64) string { func searchIssues(query string) (*IssuesSearchResult, error) { q := url.QueryEscape(query) - req, err := http.NewRequest("GET", IssuesURL+"?q="+q, nil) + req, err := http.NewRequest("GET", "https://api.github.com/search/issues?q="+q+"&per_page=100", nil) if err != nil { return nil, err } - if token != "" { - req.Header.Add("Authorization", "Bearer "+token) - } + req.Header.Add("Authorization", "Bearer "+authToken) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - resp.Body.Close() return nil, fmt.Errorf("search query failed: %s (body: %s)", resp.Status, body) } var result IssuesSearchResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - resp.Body.Close() return nil, err } - resp.Body.Close() return &result, nil } -// See https://developer.github.com/v3/search/#search-issues. +// updateIssueBody updates the body of the numbered issue. +func updateIssueBody(number int, body string) error { + // https://docs.github.com/en/rest/issues/comments#update-an-issue + var payload struct { + Body string `json:"body"` + } + payload.Body = body + data, err := json.Marshal(payload) + if err != nil { + return err + } + + url := fmt.Sprintf("https://api.github.com/repos/golang/go/issues/%d", number) + req, err := http.NewRequest("PATCH", url, bytes.NewReader(data)) + if err != nil { + return err + } + req.Header.Add("Authorization", "Bearer "+authToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("issue update failed: %s (body: %s)", resp.Status, body) + } + return nil +} + +// addIssueComment adds a markdown comment to the numbered issue. +func addIssueComment(number int, comment string) error { + // https://docs.github.com/en/rest/issues/comments#create-an-issue-comment + var payload struct { + Body string `json:"body"` + } + payload.Body = comment + data, err := json.Marshal(payload) + if err != nil { + return err + } + + url := fmt.Sprintf("https://api.github.com/repos/golang/go/issues/%d/comments", number) + req, err := http.NewRequest("POST", url, bytes.NewReader(data)) + if err != nil { + return err + } + req.Header.Add("Authorization", "Bearer "+authToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to create issue comment: %s (body: %s)", resp.Status, body) + } + return nil +} -const IssuesURL = "https://api.github.com/search/issues" +// See https://developer.github.com/v3/search/#search-issues. type IssuesSearchResult struct { TotalCount int `json:"total_count"` @@ -357,6 +752,10 @@ type Issue struct { User *User CreatedAt time.Time `json:"created_at"` Body string // in Markdown format + + predicateText string // text of ```#!stacks...``` predicate block + predicate func(string) bool // matching predicate over stack text + newStacks []string // new stacks to add to existing issue (comments and IDs) } type User struct { @@ -364,12 +763,195 @@ type User struct { HTMLURL string `json:"html_url"` } -// -- helpers -- +// -- pclntab -- + +type FileLine struct { + file string // "module@version/dir/file.go" or path relative to $GOROOT/src + line int +} + +// readPCLineTable builds the gopls executable specified by info, +// reads its PC-to-line-number table, and returns the file/line of +// each TEXT symbol. +func readPCLineTable(info Info) (map[string]FileLine, error) { + // The stacks dir will be a semi-durable temp directory + // (i.e. lasts for at least hours) holding source trees + // and executables we have built recently. + // + // Each subdir will hold a specific revision. + stacksDir := "/tmp/gopls-stacks" + if err := os.MkdirAll(stacksDir, 0777); err != nil { + return nil, fmt.Errorf("can't create stacks dir: %v", err) + } + + // Fetch the source for the tools repo, + // shallow-cloning just the desired revision. + // (Skip if it's already cloned.) + revDir := filepath.Join(stacksDir, info.Version) + if !fileExists(revDir) { + log.Printf("cloning tools@gopls/%s", info.Version) + if err := shallowClone(revDir, "https://go.googlesource.com/tools", "gopls/"+info.Version); err != nil { + _ = os.RemoveAll(revDir) // ignore errors + return nil, fmt.Errorf("clone: %v", err) + } + } + + // Build the executable with the correct GOTOOLCHAIN, GOOS, GOARCH. + // Use -trimpath for normalized file names. + // (Skip if it's already built.) + exe := fmt.Sprintf("exe-%s.%s-%s", info.GoVersion, info.GOOS, info.GOARCH) + cmd := exec.Command("go", "build", "-trimpath", "-o", "../"+exe) + cmd.Stderr = os.Stderr + cmd.Dir = filepath.Join(revDir, "gopls") + cmd.Env = append(os.Environ(), + "GOTOOLCHAIN="+info.GoVersion, + "GOOS="+info.GOOS, + "GOARCH="+info.GOARCH, + ) + if !fileExists(filepath.Join(revDir, exe)) { + log.Printf("building %s@%s with %s for %s/%s", + info.Program, info.Version, info.GoVersion, info.GOOS, info.GOARCH) + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("building: %v (rm -fr /tmp/gopls-stacks?)", err) + } + } + + // Read pclntab of executable. + cmd = exec.Command("go", "tool", "objdump", exe) + cmd.Stdout = new(strings.Builder) + cmd.Stderr = os.Stderr + cmd.Dir = revDir + cmd.Env = append(os.Environ(), + "GOTOOLCHAIN="+info.GoVersion, + "GOOS="+info.GOOS, + "GOARCH="+info.GOARCH, + ) + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("reading pclntab %v", err) + } + pclntab := make(map[string]FileLine) + lines := strings.Split(fmt.Sprint(cmd.Stdout), "\n") + for i, line := range lines { + // Each function is of this form: + // + // TEXT symbol(SB) filename + // basename.go:line instruction + // ... + if !strings.HasPrefix(line, "TEXT ") { + continue + } + fields := strings.Fields(line) + if len(fields) != 3 { + continue // symbol without file (e.g. go:buildid) + } + + symbol := strings.TrimSuffix(fields[1], "(SB)") + + filename := fields[2] + + _, line, ok := strings.Cut(strings.Fields(lines[i+1])[0], ":") + if !ok { + return nil, fmt.Errorf("can't parse 'basename.go:line' from first instruction of %s:\n%s", + symbol, line) + } + linenum, err := strconv.Atoi(line) + if err != nil { + return nil, fmt.Errorf("can't parse line number of %s: %s", symbol, line) + } + pclntab[symbol] = FileLine{filename, linenum} + } + + return pclntab, nil +} + +// shallowClone performs a shallow clone of repo into dir at the given +// 'commitish' ref (any commit reference understood by git). +// +// The directory dir must not already exist. +func shallowClone(dir, repo, commitish string) error { + if err := os.Mkdir(dir, 0750); err != nil { + return fmt.Errorf("creating dir for %s: %v", repo, err) + } + + // Set a timeout for git fetch. If this proves flaky, it can be removed. + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + // Use a shallow fetch to download just the relevant commit. + shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", repo, commitish) + initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) + initCmd.Dir = dir + if output, err := initCmd.CombinedOutput(); err != nil { + return fmt.Errorf("checking out %s: %v\n%s", repo, err, output) + } + return nil +} + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +// findPredicateBlock returns the content (sans "#!stacks") of the +// code block at the start of the issue body. +// Logic plundered from x/build/cmd/watchflakes/github.go. +func findPredicateBlock(body string) string { + // Extract ```-fenced or indented code block at start of issue description (body). + body = strings.ReplaceAll(body, "\r\n", "\n") + lines := strings.SplitAfter(body, "\n") + for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" { + lines = lines[1:] + } + text := "" + // A code quotation is bracketed by sequence of 3+ backticks. + // (More than 3 are permitted so that one can quote 3 backticks.) + if len(lines) > 0 && strings.HasPrefix(lines[0], "```") { + marker := lines[0] + n := 0 + for n < len(marker) && marker[n] == '`' { + n++ + } + marker = marker[:n] + i := 1 + for i := 1; i < len(lines); i++ { + if strings.HasPrefix(lines[i], marker) && strings.TrimSpace(strings.TrimLeft(lines[i], "`")) == "" { + text = strings.Join(lines[1:i], "") + break + } + } + if i < len(lines) { + } + } else if strings.HasPrefix(lines[0], "\t") || strings.HasPrefix(lines[0], " ") { + i := 1 + for i < len(lines) && (strings.HasPrefix(lines[i], "\t") || strings.HasPrefix(lines[i], " ")) { + i++ + } + text = strings.Join(lines[:i], "") + } + + // Must start with #!stacks so we're sure it is for us. + hdr, rest, _ := strings.Cut(text, "\n") + hdr = strings.TrimSpace(hdr) + if hdr != "#!stacks" { + return "" + } + return rest +} -func min(x, y int) int { - if x < y { - return x - } else { - return y +// isTerminal reports whether file is a terminal, +// avoiding a dependency on golang.org/x/term. +func isTerminal(file *os.File) bool { + // Hardwire the constants to avoid the need for build tags. + // The values here are good for our dev machines. + switch runtime.GOOS { + case "darwin": + const TIOCGETA = 0x40487413 // from unix.TIOCGETA + _, err := unix.IoctlGetTermios(int(file.Fd()), TIOCGETA) + return err == nil + case "linux": + const TCGETS = 0x5401 // from unix.TCGETS + _, err := unix.IoctlGetTermios(int(file.Fd()), TCGETS) + return err == nil } + panic("unreachable") } diff --git a/gopls/internal/test/integration/bench/bench_test.go b/gopls/internal/test/integration/bench/bench_test.go index 5de6804c03b..3e163d11127 100644 --- a/gopls/internal/test/integration/bench/bench_test.go +++ b/gopls/internal/test/integration/bench/bench_test.go @@ -42,6 +42,7 @@ var ( cpuProfile = flag.String("gopls_cpuprofile", "", "if set, the cpu profile file suffix; see \"Profiling\" in the package doc") memProfile = flag.String("gopls_memprofile", "", "if set, the mem profile file suffix; see \"Profiling\" in the package doc") allocProfile = flag.String("gopls_allocprofile", "", "if set, the alloc profile file suffix; see \"Profiling\" in the package doc") + blockProfile = flag.String("gopls_blockprofile", "", "if set, the block profile file suffix; see \"Profiling\" in the package doc") trace = flag.String("gopls_trace", "", "if set, the trace file suffix; see \"Profiling\" in the package doc") // If non-empty, tempDir is a temporary working dir that was created by this @@ -177,6 +178,9 @@ func profileArgs(name string, wantCPU bool) []string { if *allocProfile != "" { args = append(args, fmt.Sprintf("-profile.alloc=%s", qualifiedName(name, *allocProfile))) } + if *blockProfile != "" { + args = append(args, fmt.Sprintf("-profile.block=%s", qualifiedName(name, *blockProfile))) + } if *trace != "" { args = append(args, fmt.Sprintf("-profile.trace=%s", qualifiedName(name, *trace))) } diff --git a/gopls/internal/test/integration/bench/doc.go b/gopls/internal/test/integration/bench/doc.go index fff7bac1785..e60a3029569 100644 --- a/gopls/internal/test/integration/bench/doc.go +++ b/gopls/internal/test/integration/bench/doc.go @@ -16,8 +16,9 @@ // // Benchmark functions run gopls in a separate process, which means the normal // test flags for profiling aren't useful. Instead the -gopls_cpuprofile, -// -gopls_memprofile, -gopls_allocprofile, and -gopls_trace flags may be used -// to pass through profiling to the gopls subproces. +// -gopls_memprofile, -gopls_allocprofile, -gopls_blockprofile, and +// -gopls_trace flags may be used to pass through profiling to the gopls +// subproces. // // Each of these flags sets a suffix for the respective gopls profile, which is // named according to the schema ... For example, diff --git a/gopls/internal/test/integration/bench/iwl_test.go b/gopls/internal/test/integration/bench/iwl_test.go index ecf26f95463..09ccb301a58 100644 --- a/gopls/internal/test/integration/bench/iwl_test.go +++ b/gopls/internal/test/integration/bench/iwl_test.go @@ -15,6 +15,11 @@ import ( // BenchmarkInitialWorkspaceLoad benchmarks the initial workspace load time for // a new editing session. +// +// The OpenFiles variant of this test is more realistic: who cares if gopls is +// initialized if you can't use it? However, this test is left as is to +// preserve the validity of historical data, and to represent the baseline +// performance of validating the workspace state. func BenchmarkInitialWorkspaceLoad(b *testing.B) { repoNames := []string{ "google-cloud-go", @@ -37,13 +42,33 @@ func BenchmarkInitialWorkspaceLoad(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - doIWL(b, sharedEnv.Sandbox.GOPATH(), repo) + doIWL(b, sharedEnv.Sandbox.GOPATH(), repo, nil) } }) } } -func doIWL(b *testing.B, gopath string, repo *repo) { +// BenchmarkInitialWorkspaceLoadOpenFiles benchmarks the initial workspace load +// after opening one or more files. +// +// It may differ significantly from [BenchmarkInitialWorkspaceLoad], since +// there is various active state that is proportional to the number of open +// files. +func BenchmarkInitialWorkspaceLoadOpenFiles(b *testing.B) { + for _, t := range didChangeTests { + b.Run(t.repo, func(b *testing.B) { + repo := getRepo(b, t.repo) + sharedEnv := repo.sharedEnv(b) + b.ResetTimer() + + for range b.N { + doIWL(b, sharedEnv.Sandbox.GOPATH(), repo, []string{t.file}) + } + }) + } +} + +func doIWL(b *testing.B, gopath string, repo *repo, openfiles []string) { // Exclude the time to set up the env from the benchmark time, as this may // involve installing gopls and/or checking out the repo dir. b.StopTimer() @@ -52,11 +77,16 @@ func doIWL(b *testing.B, gopath string, repo *repo) { defer env.Close() b.StartTimer() - // Note: in the future, we may need to open a file in order to cause gopls to - // start loading the workspace. - + // TODO(rfindley): not awaiting the IWL here leads to much more volatile + // results. Investigate. env.Await(InitialWorkspaceLoad) + for _, f := range openfiles { + env.OpenFile(f) + } + + env.AfterChange() + if env.Editor.HasCommand(command.MemStats) { b.StopTimer() params := &protocol.ExecuteCommandParams{ diff --git a/gopls/internal/test/integration/bench/tests_test.go b/gopls/internal/test/integration/bench/tests_test.go new file mode 100644 index 00000000000..3bc69ef95e1 --- /dev/null +++ b/gopls/internal/test/integration/bench/tests_test.go @@ -0,0 +1,96 @@ +// 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 bench + +import ( + "encoding/json" + "testing" + + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" + "golang.org/x/tools/gopls/internal/test/integration" +) + +func BenchmarkPackagesCommand(b *testing.B) { + // By convention, x/benchmarks runs the gopls benchmarks with -short, so that + // we can use this flag to filter out benchmarks that should not be run by + // the perf builder. + // + // In this case, the benchmark must be skipped because the current baseline + // (gopls@v0.11.0) lacks the gopls.package command. + if testing.Short() { + b.Skip("not supported by the benchmark dashboard baseline") + } + + tests := []struct { + repo string + files []string + recurse bool + }{ + {"tools", []string{"internal/lsp/debounce_test.go"}, false}, + } + for _, test := range tests { + b.Run(test.repo, func(b *testing.B) { + args := command.PackagesArgs{ + Mode: command.NeedTests, + } + + env := getRepo(b, test.repo).sharedEnv(b) + for _, file := range test.files { + env.OpenFile(file) + defer closeBuffer(b, env, file) + args.Files = append(args.Files, env.Editor.DocumentURI(file)) + } + env.AfterChange() + + result := executePackagesCmd(b, env, args) // pre-warm + + // sanity check JSON {en,de}coding + var pkgs command.PackagesResult + data, err := json.Marshal(result) + if err != nil { + b.Fatal(err) + } + err = json.Unmarshal(data, &pkgs) + if err != nil { + b.Fatal(err) + } + var haveTest bool + for _, pkg := range pkgs.Packages { + for _, file := range pkg.TestFiles { + if len(file.Tests) > 0 { + haveTest = true + break + } + } + } + if !haveTest { + b.Fatalf("Expected tests") + } + + b.ResetTimer() + + if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "packages")); stopAndRecord != nil { + defer stopAndRecord() + } + + for i := 0; i < b.N; i++ { + executePackagesCmd(b, env, args) + } + }) + } +} + +func executePackagesCmd(t testing.TB, env *integration.Env, args command.PackagesArgs) any { + t.Helper() + cmd := command.NewPackagesCommand("Packages", args) + result, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{ + Command: command.Packages.String(), + Arguments: cmd.Arguments, + }) + if err != nil { + t.Fatal(err) + } + return result +} diff --git a/gopls/internal/test/integration/completion/fixedbugs_test.go b/gopls/internal/test/integration/completion/fixedbugs_test.go index 52695847c4d..faa5324e138 100644 --- a/gopls/internal/test/integration/completion/fixedbugs_test.go +++ b/gopls/internal/test/integration/completion/fixedbugs_test.go @@ -32,7 +32,7 @@ package if len(completions.Items) == 0 { t.Fatal("Completion() returned empty results") } - // Sanity check: we should get package clause compeltion. + // Sanity check: we should get package clause completion. if got, want := completions.Items[0].Label, "package playdos"; got != want { t.Errorf("Completion()[0].Label == %s, want %s", got, want) } diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index ae41bd409fa..876d055da21 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -14,6 +14,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "strings" "sync" @@ -22,7 +23,6 @@ import ( "golang.org/x/tools/gopls/internal/test/integration/fake/glob" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/pathutil" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/xcontext" @@ -42,13 +42,15 @@ type Editor struct { // TODO(rfindley): buffers should be keyed by protocol.DocumentURI. mu sync.Mutex - config EditorConfig // editor configuration - buffers map[string]buffer // open buffers (relative path -> buffer content) - serverCapabilities protocol.ServerCapabilities // capabilities / options - semTokOpts protocol.SemanticTokensOptions - watchPatterns []*glob.Glob // glob patterns to watch + config EditorConfig // editor configuration + buffers map[string]buffer // open buffers (relative path -> buffer content) + watchPatterns []*glob.Glob // glob patterns to watch suggestionUseReplaceMode bool + // These fields are populated by Connect. + serverCapabilities protocol.ServerCapabilities + semTokOpts protocol.SemanticTokensOptions + // Call metrics for the purpose of expectations. This is done in an ad-hoc // manner for now. Perhaps in the future we should do something more // systematic. Guarded with a separate mutex as calls may need to be accessed @@ -320,10 +322,8 @@ func (e *Editor) initialize(ctx context.Context) error { if err != nil { return fmt.Errorf("unmarshalling semantic tokens options: %v", err) } - e.mu.Lock() e.serverCapabilities = resp.Capabilities e.semTokOpts = semTokOpts - e.mu.Unlock() if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { return fmt.Errorf("initialized: %w", err) @@ -357,6 +357,14 @@ func clientCapabilities(cfg EditorConfig) (protocol.ClientCapabilities, error) { // Additional modifiers supported by this client: "interface", "struct", "signature", "pointer", "array", "map", "slice", "chan", "string", "number", "bool", "invalid", } + // Request that the server provide its complete list of code action kinds. + capabilities.TextDocument.CodeAction = protocol.CodeActionClientCapabilities{ + CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{ + CodeActionKind: protocol.ClientCodeActionKindOptions{ + ValueSet: []protocol.CodeActionKind{protocol.Empty}, // => all + }, + }, + } // The LSP tests have historically enabled this flag, // but really we should test both ways for older editors. capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true @@ -1026,16 +1034,6 @@ func (e *Editor) applyCodeActions(ctx context.Context, loc protocol.Location, di if action.Title == "" { return 0, fmt.Errorf("empty title for code action") } - var match bool - for _, o := range only { - if action.Kind == o { - match = true - break - } - } - if !match { - continue - } applied++ if err := e.ApplyCodeAction(ctx, action); err != nil { return 0, err @@ -1135,13 +1133,10 @@ func (e *Editor) RunGenerate(ctx context.Context, dir string) error { return nil } absDir := e.sandbox.Workdir.AbsPath(dir) - cmd, err := command.NewGenerateCommand("", command.GenerateArgs{ + cmd := command.NewGenerateCommand("", command.GenerateArgs{ Dir: protocol.URIFromPath(absDir), Recursive: false, }) - if err != nil { - return err - } params := &protocol.ExecuteCommandParams{ Command: cmd.Command, Arguments: cmd.Arguments, @@ -1583,6 +1578,7 @@ func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnost Context: protocol.CodeActionContext{ Diagnostics: diagnostics, TriggerKind: &trigger, + Only: []protocol.CodeActionKind{protocol.Empty}, // => all }, Range: loc.Range, // may be zero } @@ -1693,9 +1689,7 @@ type SemanticToken struct { // Note: previously this function elided comment, string, and number tokens. // Instead, filtering of token types should be done by the caller. func (e *Editor) interpretTokens(x []uint32, contents string) []SemanticToken { - e.mu.Lock() legend := e.semTokOpts.Legend - e.mu.Unlock() lines := strings.Split(contents, "\n") ans := []SemanticToken{} line, col := 1, 1 diff --git a/gopls/internal/test/integration/fake/workdir.go b/gopls/internal/test/integration/fake/workdir.go index 25b3cb5c557..be3cb3bcf15 100644 --- a/gopls/internal/test/integration/fake/workdir.go +++ b/gopls/internal/test/integration/fake/workdir.go @@ -13,13 +13,13 @@ import ( "os" "path/filepath" "runtime" + "slices" "sort" "strings" "sync" "time" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/robustio" ) diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go index b0325d0f872..7e5ac9aba62 100644 --- a/gopls/internal/test/integration/misc/codeactions_test.go +++ b/gopls/internal/test/integration/misc/codeactions_test.go @@ -6,13 +6,13 @@ package misc import ( "fmt" + "slices" "testing" "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" . "golang.org/x/tools/gopls/internal/test/integration" - "golang.org/x/tools/gopls/internal/util/slices" ) // This test exercises the filtering of code actions in generated files. @@ -68,8 +68,8 @@ func g() {} settings.GoDoc, settings.GoFreeSymbols, settings.GoplsDocFeatures, - protocol.RefactorExtract, - protocol.RefactorInline) + settings.RefactorExtractVariable, + settings.RefactorInlineCall) check("gen/a.go", settings.GoAssembly, settings.GoDoc, @@ -78,8 +78,18 @@ func g() {} }) } -// Test refactor.inline is not included in automatically triggered code action +// Test refactor.inline.call is not included in automatically triggered code action // unless users want refactoring. +// +// (The mechanism behind this behavior has changed. It was added when +// we used to interpret CodeAction(Only=[]) as "all kinds", which was +// a distracting nuisance (too many lightbulbs); this was fixed by +// adding special logic to refactor.inline.call to respect the trigger +// kind; but now we do this for all actions (for similar reasons) and +// interpret Only=[] as Only=[quickfix] unless triggerKind=invoked; +// except that the test client always requests CodeAction(Only=[""]). +// So, we should remove the special logic from refactorInlineCall +// and vary the Only parameter used by the test client.) func TestVSCodeIssue65167(t *testing.T) { const vim1 = `package main @@ -108,9 +118,9 @@ func Func() int { return 0 } actions := env.CodeAction(loc, nil, trigger) want := trigger != protocol.CodeActionAutomatic || selectedRange if got := slices.ContainsFunc(actions, func(act protocol.CodeAction) bool { - return act.Kind == protocol.RefactorInline + return act.Kind == settings.RefactorInlineCall }); got != want { - t.Errorf("got refactor.inline = %t, want %t", got, want) + t.Errorf("got refactor.inline.call = %t, want %t", got, want) } }) } diff --git a/gopls/internal/test/integration/misc/definition_test.go b/gopls/internal/test/integration/misc/definition_test.go index 6b364e2e9d5..71f255b52e2 100644 --- a/gopls/internal/test/integration/misc/definition_test.go +++ b/gopls/internal/test/integration/misc/definition_test.go @@ -5,9 +5,11 @@ package misc import ( + "fmt" "os" "path" "path/filepath" + "regexp" "strings" "testing" @@ -595,3 +597,48 @@ func _(err error) { } }) } + +func TestAssemblyDefinition(t *testing.T) { + // This test cannot be expressed as a marker test because + // the expect package ignores markers (@loc) within a .s file. + const src = ` +-- go.mod -- +module mod.com + +-- foo_darwin_arm64.s -- + +// assembly implementation +TEXT ·foo(SB),NOSPLIT,$0 + RET + +-- a.go -- +//go:build darwin && arm64 + +package a + +// Go declaration +func foo(int) int + +var _ = foo(123) // call +` + Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("a.go") + + locString := func(loc protocol.Location) string { + return fmt.Sprintf("%s:%s", filepath.Base(loc.URI.Path()), loc.Range) + } + + // Definition at the call"foo(123)" takes us to the Go declaration. + callLoc := env.RegexpSearch("a.go", regexp.QuoteMeta("foo(123)")) + declLoc := env.GoToDefinition(callLoc) + if got, want := locString(declLoc), "a.go:5:5-5:8"; got != want { + t.Errorf("Definition(call): got %s, want %s", got, want) + } + + // Definition a second time takes us to the assembly implementation. + implLoc := env.GoToDefinition(declLoc) + if got, want := locString(implLoc), "foo_darwin_arm64.s:2:6-2:9"; got != want { + t.Errorf("Definition(go decl): got %s, want %s", got, want) + } + }) +} diff --git a/gopls/internal/test/integration/misc/extract_test.go b/gopls/internal/test/integration/misc/extract_test.go index ec13856361e..569d53e8bba 100644 --- a/gopls/internal/test/integration/misc/extract_test.go +++ b/gopls/internal/test/integration/misc/extract_test.go @@ -7,6 +7,7 @@ package misc import ( "testing" + "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/test/compare" . "golang.org/x/tools/gopls/internal/test/integration" @@ -38,7 +39,7 @@ func Foo() int { // Find the extract function code action. var extractFunc *protocol.CodeAction for _, action := range actions { - if action.Kind == protocol.RefactorExtract && action.Title == "Extract function" { + if action.Kind == settings.RefactorExtractFunction { extractFunc = &action break } diff --git a/gopls/internal/test/integration/misc/fix_test.go b/gopls/internal/test/integration/misc/fix_test.go index acf896a9adb..5a01afe2400 100644 --- a/gopls/internal/test/integration/misc/fix_test.go +++ b/gopls/internal/test/integration/misc/fix_test.go @@ -7,6 +7,7 @@ package misc import ( "testing" + "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/test/compare" . "golang.org/x/tools/gopls/internal/test/integration" @@ -49,7 +50,7 @@ func Foo() { runner.Run(t, basic, func(t *testing.T, env *Env) { env.OpenFile("main.go") - fixes, err := env.Editor.CodeActions(env.Ctx, env.RegexpSearch("main.go", "Info{}"), nil, protocol.RefactorRewrite) + fixes, err := env.Editor.CodeActions(env.Ctx, env.RegexpSearch("main.go", "Info{}"), nil, settings.RefactorRewriteFillStruct) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/test/integration/misc/import_test.go b/gopls/internal/test/integration/misc/import_test.go index bf682306a7e..671d72d27b6 100644 --- a/gopls/internal/test/integration/misc/import_test.go +++ b/gopls/internal/test/integration/misc/import_test.go @@ -38,13 +38,10 @@ func main() { Run(t, "", func(t *testing.T, env *Env) { env.CreateBuffer("main.go", before) - cmd, err := command.NewAddImportCommand("Add Import", command.AddImportArgs{ + cmd := command.NewAddImportCommand("Add Import", command.AddImportArgs{ URI: env.Sandbox.Workdir.URI("main.go"), ImportPath: "bytes", }) - if err != nil { - t.Fatal(err) - } env.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: command.AddImport.String(), Arguments: cmd.Arguments, @@ -113,12 +110,9 @@ func TestFoo2(t *testing.T) {} Run(t, files, func(t *testing.T, env *Env) { for _, tt := range tests { - cmd, err := command.NewListImportsCommand("List Imports", command.URIArg{ + cmd := command.NewListImportsCommand("List Imports", command.URIArg{ URI: env.Sandbox.Workdir.URI(tt.filename), }) - if err != nil { - t.Fatal(err) - } var result command.ListImportsResult env.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: command.ListImports.String(), diff --git a/gopls/internal/test/integration/misc/prompt_test.go b/gopls/internal/test/integration/misc/prompt_test.go index b412d408d1c..9e87bd9ba36 100644 --- a/gopls/internal/test/integration/misc/prompt_test.go +++ b/gopls/internal/test/integration/misc/prompt_test.go @@ -471,15 +471,12 @@ func main() { "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 + cmd := command.NewMaybePromptForTelemetryCommand("prompt") + var err error env.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: cmd.Command, - }, &result) - if result != nil { + }, &err) + if err != nil { t.Fatal(err) } expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?") diff --git a/gopls/internal/test/integration/misc/staticcheck_test.go b/gopls/internal/test/integration/misc/staticcheck_test.go index 4970064c5f6..31302393252 100644 --- a/gopls/internal/test/integration/misc/staticcheck_test.go +++ b/gopls/internal/test/integration/misc/staticcheck_test.go @@ -7,7 +7,6 @@ package misc import ( "testing" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/testenv" . "golang.org/x/tools/gopls/internal/test/integration" @@ -23,12 +22,6 @@ func TestStaticcheckGenerics(t *testing.T) { // TODO(adonovan): reenable once dominikh/go-tools#1494 is fixed. t.Skip("disabled until buildir supports range-over-func (dominikh/go-tools#1494)") - // TODO(golang/go#65249): re-enable and fix this test once we - // update go.mod to go1.23 so that gotypesalias=1 becomes the default. - if aliases.Enabled() { - t.Skip("staticheck doesn't yet support aliases (dominikh/go-tools#1523)") - } - const files = ` -- go.mod -- module mod.com @@ -101,12 +94,6 @@ func TestStaticcheckRelatedInfo(t *testing.T) { // TODO(adonovan): reenable once dominikh/go-tools#1494 is fixed. t.Skip("disabled until buildir supports range-over-func (dominikh/go-tools#1494)") - // TODO(golang/go#65249): re-enable and fix this test once we - // update go.mod to go1.23 so that gotypesalias=1 becomes the default. - if aliases.Enabled() { - t.Skip("staticheck doesn't yet support aliases (dominikh/go-tools#1523)") - } - const files = ` -- go.mod -- module mod.test diff --git a/gopls/internal/test/integration/misc/vuln_test.go b/gopls/internal/test/integration/misc/vuln_test.go index 1ed54a7bbe8..7be02b3ceb3 100644 --- a/gopls/internal/test/integration/misc/vuln_test.go +++ b/gopls/internal/test/integration/misc/vuln_test.go @@ -33,13 +33,9 @@ go 1.12 package foo ` Run(t, files, func(t *testing.T, env *Env) { - cmd, err := command.NewRunGovulncheckCommand("Run Vulncheck Exp", command.VulncheckArgs{ + cmd := command.NewRunGovulncheckCommand("Run Vulncheck Exp", command.VulncheckArgs{ URI: "/invalid/file/url", // invalid arg }) - if err != nil { - t.Fatal(err) - } - params := &protocol.ExecuteCommandParams{ Command: command.RunGovulncheck.String(), Arguments: cmd.Arguments, @@ -274,12 +270,9 @@ func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulnc t.Helper() var result map[protocol.DocumentURI]*vulncheck.Result - fetchCmd, err := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{ + fetchCmd := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{ URI: env.Sandbox.Workdir.URI("go.mod"), }) - if err != nil { - t.Fatal(err) - } env.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: fetchCmd.Command, Arguments: fetchCmd.Arguments, diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index 8105fd06896..24518145721 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go @@ -285,7 +285,7 @@ func viewPkgDoc(t *testing.T, env *Env, loc protocol.Location) protocol.URI { Command: docAction.Command.Command, Arguments: docAction.Command.Arguments, } - var result command.DebuggingResult + var result any env.ExecuteCommand(params, &result) doc := shownDocument(t, env, "http:") @@ -484,7 +484,7 @@ func checkMatch(t *testing.T, want bool, got []byte, pattern string) { } } -// codeActionByKind returns the first action of the specified kind, or an error. +// codeActionByKind returns the first action of (exactly) the specified kind, or an error. func codeActionByKind(actions []protocol.CodeAction, kind protocol.CodeActionKind) (*protocol.CodeAction, error) { for _, act := range actions { if act.Kind == kind { diff --git a/gopls/internal/test/integration/workspace/modules_test.go b/gopls/internal/test/integration/workspace/modules_test.go index a3e8122bc4b..7eedcff688a 100644 --- a/gopls/internal/test/integration/workspace/modules_test.go +++ b/gopls/internal/test/integration/workspace/modules_test.go @@ -140,10 +140,7 @@ func Bar() func checkModules(t testing.TB, env *Env, dir protocol.DocumentURI, maxDepth int, want []command.Module) { t.Helper() - cmd, err := command.NewModulesCommand("Modules", command.ModulesArgs{Dir: dir, MaxDepth: maxDepth}) - if err != nil { - t.Fatal(err) - } + cmd := command.NewModulesCommand("Modules", command.ModulesArgs{Dir: dir, MaxDepth: maxDepth}) var result command.ModulesResult env.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: command.Modules.String(), diff --git a/gopls/internal/test/integration/workspace/packages_test.go b/gopls/internal/test/integration/workspace/packages_test.go index ebeb518c644..106734a1864 100644 --- a/gopls/internal/test/integration/workspace/packages_test.go +++ b/gopls/internal/test/integration/workspace/packages_test.go @@ -16,7 +16,7 @@ import ( ) func TestPackages(t *testing.T) { - const goModView = ` + const files = ` -- go.mod -- module foo @@ -37,8 +37,8 @@ func Baz() ` t.Run("file", func(t *testing.T) { - Run(t, goModView, func(t *testing.T, env *Env) { - checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("foo.go")}, false, []command.Package{ + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("foo.go")}, false, 0, []command.Package{ { Path: "foo", ModulePath: "foo", @@ -48,13 +48,13 @@ func Baz() Path: "foo", GoMod: env.Editor.DocumentURI("go.mod"), }, - }) + }, []string{}) }) }) t.Run("package", func(t *testing.T) { - Run(t, goModView, func(t *testing.T, env *Env) { - checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("bar")}, false, []command.Package{ + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("bar")}, false, 0, []command.Package{ { Path: "foo/bar", ModulePath: "foo", @@ -64,13 +64,13 @@ func Baz() Path: "foo", GoMod: env.Editor.DocumentURI("go.mod"), }, - }) + }, []string{}) }) }) t.Run("workspace", func(t *testing.T) { - Run(t, goModView, func(t *testing.T, env *Env) { - checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("")}, true, []command.Package{ + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("")}, true, 0, []command.Package{ { Path: "foo", ModulePath: "foo", @@ -84,17 +84,17 @@ func Baz() Path: "foo", GoMod: env.Editor.DocumentURI("go.mod"), }, - }) + }, []string{}) }) }) t.Run("nested module", func(t *testing.T) { - Run(t, goModView, func(t *testing.T, env *Env) { + Run(t, files, func(t *testing.T, env *Env) { // Load the nested module env.OpenFile("baz/baz.go") // Request packages using the URI of the nested module _directory_ - checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("baz")}, true, []command.Package{ + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("baz")}, true, 0, []command.Package{ { Path: "baz", ModulePath: "baz", @@ -104,18 +104,332 @@ func Baz() Path: "baz", GoMod: env.Editor.DocumentURI("baz/go.mod"), }, + }, []string{}) + }) + }) +} + +func TestPackagesWithTests(t *testing.T) { + const files = ` +-- go.mod -- +module foo + +-- foo.go -- +package foo +import "testing" +func Foo() +func TestFoo2(t *testing.T) + +-- foo_test.go -- +package foo +import "testing" +func TestFoo(t *testing.T) + +-- foo2_test.go -- +package foo_test +import "testing" +func TestBar(t *testing.T) {} + +-- baz/baz_test.go -- +package baz +import "testing" +func TestBaz(*testing.T) +func BenchmarkBaz(*testing.B) +func FuzzBaz(*testing.F) +func ExampleBaz() + +-- bat/go.mod -- +module bat + +-- bat/bat_test.go -- +package bat +import "testing" +func Test(*testing.T) +` + + t.Run("file", func(t *testing.T) { + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("foo_test.go")}, false, command.NeedTests, []command.Package{ + { + Path: "foo", + ModulePath: "foo", + }, + { + Path: "foo", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo_test.go"), + Tests: []command.TestCase{ + {Name: "TestFoo"}, + }, + }, + }, + }, + { + Path: "foo_test", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo2_test.go"), + Tests: []command.TestCase{ + {Name: "TestBar"}, + }, + }, + }, + }, + }, map[string]command.Module{ + "foo": { + Path: "foo", + GoMod: env.Editor.DocumentURI("go.mod"), + }, + }, []string{ + "func TestFoo(t *testing.T)", + "func TestBar(t *testing.T) {}", + }) + }) + }) + + t.Run("package", func(t *testing.T) { + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("baz")}, false, command.NeedTests, []command.Package{ + { + Path: "foo/baz", + ForTest: "foo/baz", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("baz/baz_test.go"), + Tests: []command.TestCase{ + {Name: "TestBaz"}, + {Name: "BenchmarkBaz"}, + {Name: "FuzzBaz"}, + {Name: "ExampleBaz"}, + }, + }, + }, + }, + }, map[string]command.Module{ + "foo": { + Path: "foo", + GoMod: env.Editor.DocumentURI("go.mod"), + }, + }, []string{ + "func TestBaz(*testing.T)", + "func BenchmarkBaz(*testing.B)", + "func FuzzBaz(*testing.F)", + "func ExampleBaz()", + }) + }) + }) + + t.Run("workspace", func(t *testing.T) { + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI(".")}, true, command.NeedTests, []command.Package{ + { + Path: "foo", + ModulePath: "foo", + }, + { + Path: "foo", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo_test.go"), + Tests: []command.TestCase{ + {Name: "TestFoo"}, + }, + }, + }, + }, + { + Path: "foo/baz", + ForTest: "foo/baz", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("baz/baz_test.go"), + Tests: []command.TestCase{ + {Name: "TestBaz"}, + {Name: "BenchmarkBaz"}, + {Name: "FuzzBaz"}, + {Name: "ExampleBaz"}, + }, + }, + }, + }, + { + Path: "foo_test", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo2_test.go"), + Tests: []command.TestCase{ + {Name: "TestBar"}, + }, + }, + }, + }, + }, map[string]command.Module{ + "foo": { + Path: "foo", + GoMod: env.Editor.DocumentURI("go.mod"), + }, + }, []string{ + "func TestFoo(t *testing.T)", + "func TestBaz(*testing.T)", + "func BenchmarkBaz(*testing.B)", + "func FuzzBaz(*testing.F)", + "func ExampleBaz()", + "func TestBar(t *testing.T) {}", + }) + }) + }) + + t.Run("nested module", func(t *testing.T) { + Run(t, files, func(t *testing.T, env *Env) { + // Load the nested module + env.OpenFile("bat/bat_test.go") + + // Request packages using the URI of the nested module _directory_ + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("bat")}, true, command.NeedTests, []command.Package{ + { + Path: "bat", + ForTest: "bat", + ModulePath: "bat", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("bat/bat_test.go"), + Tests: []command.TestCase{ + {Name: "Test"}, + }, + }, + }, + }, + }, map[string]command.Module{ + "bat": { + Path: "bat", + GoMod: env.Editor.DocumentURI("bat/go.mod"), + }, + }, []string{ + "func Test(*testing.T)", }) }) }) } -func checkPackages(t testing.TB, env *Env, files []protocol.DocumentURI, recursive bool, wantPkg []command.Package, wantModule map[string]command.Module) { - t.Helper() +func TestPackagesWithSubtests(t *testing.T) { + const files = ` +-- go.mod -- +module foo + +-- foo_test.go -- +package foo + +import "testing" - cmd, err := command.NewPackagesCommand("Packages", command.PackagesArgs{Files: files, Recursive: recursive}) - if err != nil { - t.Fatal(err) +// Verify that examples don't break subtest detection +func ExampleFoo() {} + +func TestFoo(t *testing.T) { + t.Run("Bar", func(t *testing.T) { + t.Run("Baz", func(t *testing.T) {}) + }) + t.Run("Bar", func(t *testing.T) {}) + t.Run("Bar", func(t *testing.T) {}) + t.Run("with space", func(t *testing.T) {}) + + var x X + y := func(t *testing.T) { + t.Run("VarSub", func(t *testing.T) {}) } + t.Run("SubtestFunc", SubtestFunc) + t.Run("SubtestMethod", x.SubtestMethod) + t.Run("SubtestVar", y) +} + +func SubtestFunc(t *testing.T) { + t.Run("FuncSub", func(t *testing.T) {}) +} + +type X int +func (X) SubtestMethod(t *testing.T) { + t.Run("MethodSub", func(t *testing.T) {}) +} +` + + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("foo_test.go")}, false, command.NeedTests, []command.Package{ + { + Path: "foo", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo_test.go"), + Tests: []command.TestCase{ + {Name: "ExampleFoo"}, + {Name: "TestFoo"}, + {Name: "TestFoo/Bar"}, + {Name: "TestFoo/Bar/Baz"}, + {Name: "TestFoo/Bar#01"}, + {Name: "TestFoo/Bar#02"}, + {Name: "TestFoo/with_space"}, + {Name: "TestFoo/SubtestFunc"}, + {Name: "TestFoo/SubtestFunc/FuncSub"}, + {Name: "TestFoo/SubtestMethod"}, + {Name: "TestFoo/SubtestMethod/MethodSub"}, + {Name: "TestFoo/SubtestVar"}, + // {Name: "TestFoo/SubtestVar/VarSub"}, // TODO + }, + }, + }, + }, + }, map[string]command.Module{ + "foo": { + Path: "foo", + GoMod: env.Editor.DocumentURI("go.mod"), + }, + }, []string{ + "func ExampleFoo() {}", + `func TestFoo(t *testing.T) { + t.Run("Bar", func(t *testing.T) { + t.Run("Baz", func(t *testing.T) {}) + }) + t.Run("Bar", func(t *testing.T) {}) + t.Run("Bar", func(t *testing.T) {}) + t.Run("with space", func(t *testing.T) {}) + + var x X + y := func(t *testing.T) { + t.Run("VarSub", func(t *testing.T) {}) + } + t.Run("SubtestFunc", SubtestFunc) + t.Run("SubtestMethod", x.SubtestMethod) + t.Run("SubtestVar", y) +}`, + "t.Run(\"Bar\", func(t *testing.T) {\n\t\tt.Run(\"Baz\", func(t *testing.T) {})\n\t})", + `t.Run("Baz", func(t *testing.T) {})`, + `t.Run("Bar", func(t *testing.T) {})`, + `t.Run("Bar", func(t *testing.T) {})`, + `t.Run("with space", func(t *testing.T) {})`, + `t.Run("SubtestFunc", SubtestFunc)`, + `t.Run("FuncSub", func(t *testing.T) {})`, + `t.Run("SubtestMethod", x.SubtestMethod)`, + `t.Run("MethodSub", func(t *testing.T) {})`, + `t.Run("SubtestVar", y)`, + }) + }) +} + +func checkPackages(t testing.TB, env *Env, files []protocol.DocumentURI, recursive bool, mode command.PackagesMode, wantPkg []command.Package, wantModule map[string]command.Module, wantSource []string) { + t.Helper() + + cmd := command.NewPackagesCommand("Packages", command.PackagesArgs{Files: files, Recursive: recursive, Mode: mode}) var result command.PackagesResult env.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: command.Packages.String(), @@ -126,9 +440,31 @@ func checkPackages(t testing.TB, env *Env, files []protocol.DocumentURI, recursi // consistency sort.Slice(result.Packages, func(i, j int) bool { a, b := result.Packages[i], result.Packages[j] - return strings.Compare(a.Path, b.Path) < 0 + c := strings.Compare(a.Path, b.Path) + if c != 0 { + return c < 0 + } + return strings.Compare(a.ForTest, b.ForTest) < 0 }) + // Instead of testing the exact values of the test locations (which would + // make these tests significantly more trouble to maintain), verify the + // source range they refer to. + gotSource := []string{} // avoid issues with comparing null to [] + for i := range result.Packages { + pkg := &result.Packages[i] + for i := range pkg.TestFiles { + file := &pkg.TestFiles[i] + env.OpenFile(file.URI.Path()) + + for i := range file.Tests { + test := &file.Tests[i] + gotSource = append(gotSource, env.FileContentAt(test.Loc)) + test.Loc = protocol.Location{} + } + } + } + if diff := cmp.Diff(wantPkg, result.Packages); diff != "" { t.Errorf("Packages(%v) returned unexpected packages (-want +got):\n%s", files, diff) } @@ -136,4 +472,11 @@ func checkPackages(t testing.TB, env *Env, files []protocol.DocumentURI, recursi if diff := cmp.Diff(wantModule, result.Module); diff != "" { t.Errorf("Packages(%v) returned unexpected modules (-want +got):\n%s", files, diff) } + + // Don't check the source if the response is incorrect + if !t.Failed() { + if diff := cmp.Diff(wantSource, gotSource); diff != "" { + t.Errorf("Packages(%v) returned unexpected test case ranges (-want +got):\n%s", files, diff) + } + } } diff --git a/gopls/internal/test/integration/wrappers.go b/gopls/internal/test/integration/wrappers.go index 3247fac1761..ddff4da979b 100644 --- a/gopls/internal/test/integration/wrappers.go +++ b/gopls/internal/test/integration/wrappers.go @@ -136,6 +136,21 @@ func (e *Env) FileContent(name string) string { return string(content) } +// FileContentAt returns the file content at the given location, using the +// file's mapper. +func (e *Env) FileContentAt(location protocol.Location) string { + e.T.Helper() + mapper, err := e.Editor.Mapper(location.URI.Path()) + if err != nil { + e.T.Fatal(err) + } + start, end, err := mapper.RangeOffsets(location.Range) + if err != nil { + e.T.Fatal(err) + } + return string(mapper.Content[start:end]) +} + // RegexpSearch returns the starting position of the first match for re in the // buffer specified by name, calling t.Fatal on any error. It first searches // for the position in open buffers, then in workspace files. @@ -407,10 +422,7 @@ func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result inter // Views returns the server's views. func (e *Env) Views() []command.View { var summaries []command.View - cmd, err := command.NewViewsCommand("") - if err != nil { - e.T.Fatal(err) - } + cmd := command.NewViewsCommand("") e.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: cmd.Command, Arguments: cmd.Arguments, diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go index f81604c913d..bd23a4f12ef 100644 --- a/gopls/internal/test/marker/doc.go +++ b/gopls/internal/test/marker/doc.go @@ -100,17 +100,15 @@ The following markers are supported within marker tests: completion candidate produced at the given location with provided label results in the given golden state. - - codeaction(start, end, kind, golden, ...titles): specifies a code action + - codeaction(start, end, kind, golden): specifies a code action to request for the given range. To support multi-line ranges, the range is defined to be between start.Start and end.End. The golden directory contains changed file content after the code action is applied. - If titles are provided, they are used to filter the matching code - action. - TODO(rfindley): consolidate with codeactionedit, via a @loc2 marker that - allows binding multi-line locations. + TODO(rfindley): now that 'location' supports multi-line matches, replace + uses of 'codeaction' with codeactionedit. - - codeactionedit(range, kind, golden, ...titles): a shorter form of + - codeactionedit(location, kind, golden): a shorter form of codeaction. Invokes a code action of the given kind for the given in-line range, and compares the resulting formatted unified *edits* (notably, not the full file content) with the golden directory. @@ -222,12 +220,12 @@ The following markers are supported within marker tests: the active parameter (an index) highlighted. - suggestedfix(location, regexp, golden): like diag, the location and - regexp identify an expected diagnostic. This diagnostic must - to have exactly one associated code action of the specified kind. + regexp identify an expected diagnostic, which must have exactly one + associated "quickfix" code action. This action is executed for its editing effects on the source files. Like rename, the golden directory contains the expected transformed files. - - suggestedfixerr(location, regexp, kind, wantError): specifies that the + - suggestedfixerr(location, regexp, wantError): specifies that the suggestedfix operation should fail with an error that matches the expectation. (Failures in the computation to offer a fix do not generally result in LSP errors, so this marker is not appropriate for testing them.) @@ -292,11 +290,15 @@ test function. Additional value conversions may occur for these argument -> parameter type pairs: - string->regexp: the argument is parsed as a regular expressions. - string->location: the argument is converted to the location of the first - instance of the argument in the partial line preceding the note. + instance of the argument in the file content starting from the beginning of + the line containing the note. Multi-line matches are permitted, but the + match must begin before the note. - regexp->location: the argument is converted to the location of the first - match for the argument in the partial line preceding the note. If the - regular expression contains exactly one subgroup, the position of the - subgroup is used rather than the position of the submatch. + match for the argument in the file content starting from the beginning of + the line containing the note. Multi-line matches are permitted, but the + match must begin before the note. If the regular expression contains + exactly one subgroup, the position of the subgroup is used rather than the + position of the submatch. - name->location: the argument is replaced by the named location. - name->Golden: the argument is used to look up golden content prefixed by @. @@ -336,12 +338,12 @@ files, and sandboxed directory. Argument converters translate the "b" and "abc" arguments into locations by interpreting each one as a substring (or as a regular expression, if of the -form re"a|b") and finding the location of its first occurrence on the preceding -portion of the line, and the abc identifier into a the golden content contained -in the file @abc. Then the hoverMarker method executes a textDocument/hover LSP -request at the src position, and ensures the result spans "abc", with the -markdown content from @abc. (Note that the markdown content includes the expect -annotation as the doc comment.) +form re"a|b") and finding the location of its first occurrence starting on the +preceding portion of the line, and the abc identifier into a the golden content +contained in the file @abc. Then the hoverMarker method executes a +textDocument/hover LSP request at the src position, and ensures the result +spans "abc", with the markdown content from @abc. (Note that the markdown +content includes the expect annotation as the doc comment.) The next hover on the same line asserts the same result, but initiates the hover immediately after "abc" in the source. This tests that we find the diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index d3a7685b4dd..1478fe631c7 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -23,6 +23,7 @@ import ( "reflect" "regexp" "runtime" + "slices" "sort" "strings" "testing" @@ -30,7 +31,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "golang.org/x/tools/go/expect" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" @@ -41,7 +41,6 @@ import ( "golang.org/x/tools/gopls/internal/test/integration/fake" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/jsonrpc2" @@ -1048,8 +1047,16 @@ var ( // // Converters should return an error rather than calling marker.errorf(). var customConverters = map[reflect.Type]func(marker, any) (any, error){ - reflect.TypeOf(protocol.Location{}): convertLocation, - reflect.TypeOf(completionLabel("")): convertCompletionLabel, + reflect.TypeOf(protocol.Location{}): converter(convertLocation), + reflect.TypeOf(completionLabel("")): converter(convertCompletionLabel), +} + +// converter transforms a typed argument conversion function to an untyped +// conversion function. +func converter[T any](f func(marker, any) (T, error)) func(marker, any) (any, error) { + return func(m marker, arg any) (any, error) { + return f(m, arg) + } } func convert(mark marker, arg any, paramType reflect.Type) (any, error) { @@ -1087,26 +1094,64 @@ func convert(mark marker, arg any, paramType reflect.Type) (any, 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) (any, error) { +func convertLocation(mark marker, arg any) (protocol.Location, error) { + // matchContent is used to match the given argument against the file content + // starting at the marker line. + var matchContent func([]byte) (int, int, error) + switch arg := arg.(type) { case protocol.Location: - return arg, nil + return arg, nil // nothing to do case string: - startOff, preceding, m, err := linePreceding(mark.run, mark.note.Pos) - if err != nil { - return protocol.Location{}, err - } - idx := bytes.Index(preceding, []byte(arg)) - if idx < 0 { - return protocol.Location{}, fmt.Errorf("substring %q not found in %q", arg, preceding) + matchContent = func(content []byte) (int, int, error) { + idx := bytes.Index(content, []byte(arg)) + if idx < 0 { + return 0, 0, fmt.Errorf("substring %q not found", arg) + } + return idx, idx + len(arg), nil } - off := startOff + idx - return m.OffsetLocation(off, off+len(arg)) case *regexp.Regexp: - return findRegexpInLine(mark.run, mark.note.Pos, arg) + matchContent = func(content []byte) (int, int, error) { + matches := arg.FindSubmatchIndex(content) + if len(matches) == 0 { + return 0, 0, fmt.Errorf("no match for regexp %q", arg) + } + switch len(matches) { + case 2: + // no subgroups: return the range of the regexp expression + return matches[0], matches[1], nil + case 4: + // one subgroup: return its range + return matches[2], matches[3], nil + default: + return 0, 0, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", arg, len(matches)/2-1) + } + } default: return protocol.Location{}, fmt.Errorf("cannot convert argument type %T to location (must be a string or regexp to match the preceding line)", arg) } + + // Now use matchFunc to match a range starting on the marker line. + + file := mark.run.test.fset.File(mark.note.Pos) + posn := safetoken.Position(file, mark.note.Pos) + lineStart := file.LineStart(posn.Line) + lineStartOff, lineEndOff, err := safetoken.Offsets(file, lineStart, mark.note.Pos) + if err != nil { + return protocol.Location{}, err + } + m := mark.mapper() + start, end, err := matchContent(m.Content[lineStartOff:]) + if err != nil { + return protocol.Location{}, err + } + startOff, endOff := lineStartOff+start, lineStartOff+end + if startOff > lineEndOff { + // The start of the match must be between the start of the line and the + // marker position (inclusive). + return protocol.Location{}, fmt.Errorf("no matching range found starting on the current line") + } + return m.OffsetLocation(startOff, endOff) } // completionLabel is a special parameter type that may be converted from a @@ -1123,7 +1168,7 @@ type completionLabel string // // This allows us to stage a migration of the "snippet" marker to a simpler // model where the completion label can just be listed explicitly. -func convertCompletionLabel(mark marker, arg any) (any, error) { +func convertCompletionLabel(mark marker, arg any) (completionLabel, error) { switch arg := arg.(type) { case string: return completionLabel(arg), nil @@ -1134,50 +1179,6 @@ func convertCompletionLabel(mark marker, arg any) (any, error) { } } -// findRegexpInLine searches the partial line preceding pos for a match for the -// regular expression re, returning a location spanning the first match. If re -// contains exactly one subgroup, the position of this subgroup match is -// returned rather than the position of the full match. -func findRegexpInLine(run *markerTestRun, pos token.Pos, re *regexp.Regexp) (protocol.Location, error) { - startOff, preceding, m, err := linePreceding(run, pos) - if err != nil { - return protocol.Location{}, err - } - - matches := re.FindSubmatchIndex(preceding) - if len(matches) == 0 { - return protocol.Location{}, fmt.Errorf("no match for regexp %q found in %q", re, string(preceding)) - } - var start, end int - switch len(matches) { - case 2: - // no subgroups: return the range of the regexp expression - start, end = matches[0], matches[1] - case 4: - // one subgroup: return its range - start, end = matches[2], matches[3] - default: - return protocol.Location{}, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", re, len(matches)/2-1) - } - - return m.OffsetLocation(start+startOff, end+startOff) -} - -func linePreceding(run *markerTestRun, pos token.Pos) (int, []byte, *protocol.Mapper, error) { - file := run.test.fset.File(pos) - posn := safetoken.Position(file, pos) - lineStart := file.LineStart(posn.Line) - startOff, endOff, err := safetoken.Offsets(file, lineStart, pos) - if err != nil { - return 0, nil, nil, err - } - m, err := run.env.Editor.Mapper(file.Name()) - if err != nil { - return 0, nil, nil, err - } - return startOff, m.Content[startOff:endOff], m, nil -} - // convertStringMatcher converts a string, regexp, or identifier // argument into a stringMatcher. The string is a substring of the // expected error, the regexp is a pattern than matches the expected @@ -1412,7 +1413,7 @@ func snippetMarker(mark marker, src protocol.Location, label completionLabel, wa return } if got != want { - mark.errorf("snippets do not match: got %q, want %q", got, want) + mark.errorf("snippets do not match: got:\n%q\nwant:\n%q", got, want) } } @@ -1780,20 +1781,26 @@ func tokenMarker(mark marker, loc protocol.Location, tokenType, mod string) { func signatureMarker(mark marker, src protocol.Location, label string, active int64) { got := mark.run.env.SignatureHelp(src) + var gotLabels []string // for better error messages + if got != nil { + for _, s := range got.Signatures { + gotLabels = append(gotLabels, s.Label) + } + } if label == "" { // A null result is expected. // (There's no point having a @signatureerr marker // because the server handler suppresses all errors.) - if got != nil && len(got.Signatures) > 0 { - mark.errorf("signatureHelp = %v, want 0 signatures", got) + if got != nil && len(gotLabels) > 0 { + mark.errorf("signatureHelp = %v, want 0 signatures", gotLabels) } return } if got == nil || len(got.Signatures) != 1 { - mark.errorf("signatureHelp = %v, want exactly 1 signature", got) + mark.errorf("signatureHelp = %v, want exactly 1 signature", gotLabels) return } - if got := got.Signatures[0].Label; got != label { + if got := gotLabels[0]; got != label { mark.errorf("signatureHelp: got label %q, want %q", got, label) } if got := int64(got.ActiveParameter); got != active { @@ -1920,13 +1927,13 @@ func changedFiles(env *integration.Env, changes []protocol.DocumentChange) (map[ return result, nil } -func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden, titles ...string) { +func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden) { // Request the range from start.Start to end.End. loc := start loc.Range.End = end.Range.End // Apply the fix it suggests. - changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) + changed, err := codeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(actionKind), nil) if err != nil { mark.errorf("codeAction failed: %v", err) return @@ -1936,8 +1943,8 @@ func codeActionMarker(mark marker, start, end protocol.Location, actionKind stri checkChangedFiles(mark, changed, g) } -func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden, titles ...string) { - changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) +func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden) { + changed, err := codeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(actionKind), nil) if err != nil { mark.errorf("codeAction failed: %v", err) return @@ -1949,7 +1956,7 @@ func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr stringMatcher) { loc := start loc.Range.End = end.Range.End - _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, nil) + _, err := codeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(actionKind), nil) wantErr.checkErr(mark, err) } @@ -2021,8 +2028,10 @@ func (mark marker) consumeExtraNotes(name string, f func(marker)) { // 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 -// action of the specified kind suggested by the matched diagnostic. +// the expectation of a diagnostic, but then it applies the "quickfix" +// code action (which must be unique) suggested by the matched diagnostic. +// +// TODO(adonovan): rename to @quickfix, since that's the LSP term. func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) { loc.Range.End = loc.Range.Start // diagnostics ignore end position. // Find and remove the matching diagnostic. @@ -2033,7 +2042,7 @@ func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, g } // Apply the fix it suggests. - changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) + changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag) if err != nil { mark.errorf("suggestedfix failed: %v. (Use @suggestedfixerr for expected errors.)", err) return @@ -2053,7 +2062,7 @@ func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp } // Apply the fix it suggests. - _, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) + _, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag) wantErr.checkErr(mark, err) } @@ -2064,8 +2073,8 @@ func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp // The resulting map contains resulting file contents after the code action is // applied. Currently, this function does not support code actions that return // edits directly; it only supports code action commands. -func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) (map[string][]byte, error) { - changes, err := codeActionChanges(env, uri, rng, actionKind, diag, titles) +func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (map[string][]byte, error) { + changes, err := codeActionChanges(env, uri, rng, kind, diag) if err != nil { return nil, err } @@ -2075,16 +2084,15 @@ func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Ran // codeActionChanges executes a textDocument/codeAction request for the // specified location and kind, and captures the resulting document changes. // If diag is non-nil, it is used as the code action context. -// If titles is non-empty, the code action title must be present among the provided titles. -func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) ([]protocol.DocumentChange, error) { +func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) ([]protocol.DocumentChange, error) { // Request all code actions that apply to the diagnostic. - // (The protocol supports filtering using Context.Only={actionKind} - // but we can give a better error if we don't filter.) + // A production client would set Only=[kind], + // but we can give a better error if we don't filter. params := &protocol.CodeActionParams{ TextDocument: protocol.TextDocumentIdentifier{URI: uri}, Range: rng, Context: protocol.CodeActionContext{ - Only: nil, // => all kinds + Only: []protocol.CodeActionKind{protocol.Empty}, // => all }, } if diag != nil { @@ -2096,27 +2104,19 @@ func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng proto return nil, err } - // Find the sole candidates CodeAction of the specified kind (e.g. refactor.rewrite). + // Find the sole candidate CodeAction of exactly the specified kind + // (e.g. refactor.inline.call). var candidates []protocol.CodeAction for _, act := range actions { - if act.Kind == protocol.CodeActionKind(actionKind) { - if len(titles) > 0 { - for _, f := range titles { - if act.Title == f { - candidates = append(candidates, act) - break - } - } - } else { - candidates = append(candidates, act) - } + if act.Kind == kind { + candidates = append(candidates, act) } } if len(candidates) != 1 { for _, act := range actions { env.T.Logf("found CodeAction Kind=%s Title=%q", act.Kind, act.Title) } - return nil, fmt.Errorf("found %d CodeActions of kind %s matching filters %v for this diagnostic, want 1", len(candidates), actionKind, titles) + return nil, fmt.Errorf("found %d CodeActions of kind %s for this diagnostic, want 1", len(candidates), kind) } action := candidates[0] diff --git a/gopls/internal/test/marker/testdata/codeaction/change_quote.txt b/gopls/internal/test/marker/testdata/codeaction/change_quote.txt index 0fa144c1e56..a3b4f8d4c83 100644 --- a/gopls/internal/test/marker/testdata/codeaction/change_quote.txt +++ b/gopls/internal/test/marker/testdata/codeaction/change_quote.txt @@ -17,53 +17,53 @@ import ( func foo() { var s string - s = "hello" //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") - s = `hello` //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") - s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") - s = `hello world` //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") - s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") + s = "hello" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a1) + s = `hello` //@codeactionedit("`", "refactor.rewrite.changeQuote", a2) + s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a3) + s = `hello world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a4) + s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a5) // add a comment to avoid affect diff compute s = `hello -world` //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") - s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") - s = `hello"world` //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") - s = "hello\x1bworld" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") - s = "hello`world" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") - s = "hello\x7fworld" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") +world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a6) + s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a7) + s = `hello"world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a8) + s = "hello\x1bworld" //@codeactionerr(`"`, "", "refactor.rewrite.changeQuote", re"found 0 CodeActions") + s = "hello`world" //@codeactionerr(`"`, "", "refactor.rewrite.changeQuote", re"found 0 CodeActions") + s = "hello\x7fworld" //@codeactionerr(`"`, "", "refactor.rewrite.changeQuote", re"found 0 CodeActions") fmt.Println(s) } -- @a1/a.go -- @@ -9 +9 @@ -- s = "hello" //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") -+ s = `hello` //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") +- s = "hello" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a1) ++ s = `hello` //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a1) -- @a2/a.go -- @@ -10 +10 @@ -- s = `hello` //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") -+ s = "hello" //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") +- s = `hello` //@codeactionedit("`", "refactor.rewrite.changeQuote", a2) ++ s = "hello" //@codeactionedit("`", "refactor.rewrite.changeQuote", a2) -- @a3/a.go -- @@ -11 +11 @@ -- s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") -+ s = `hello world` //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") +- s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a3) ++ s = `hello world` //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a3) -- @a4/a.go -- @@ -12 +12 @@ -- s = `hello world` //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") -+ s = "hello\tworld" //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") +- s = `hello world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a4) ++ s = "hello\tworld" //@codeactionedit("`", "refactor.rewrite.changeQuote", a4) -- @a5/a.go -- @@ -13 +13,2 @@ -- s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") +- s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a5) + s = `hello -+world` //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") ++world` //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a5) -- @a6/a.go -- @@ -15,2 +15 @@ - s = `hello --world` //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") -+ s = "hello\nworld" //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") +-world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a6) ++ s = "hello\nworld" //@codeactionedit("`", "refactor.rewrite.changeQuote", a6) -- @a7/a.go -- @@ -17 +17 @@ -- s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") -+ s = `hello"world` //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") +- s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a7) ++ s = `hello"world` //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a7) -- @a8/a.go -- @@ -18 +18 @@ -- s = `hello"world` //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") -+ s = "hello\"world" //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") +- s = `hello"world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a8) ++ s = "hello\"world" //@codeactionedit("`", "refactor.rewrite.changeQuote", a8) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt index d5dbe931226..d035119bc3a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt @@ -9,17 +9,17 @@ go 1.18 -- a/a.go -- package a -//@codeactionedit(block, "refactor.extract", out, "Extract function") +//@codeactionedit(block, "refactor.extract.function", out) func _() { var logf func(string, ...any) - { println(logf) } //@loc(block, re`{.*}`) + { println(logf) } //@loc(block, re`{[^}]*}`) } -- @out/a/a.go -- @@ -7 +7 @@ -- { println(logf) } //@loc(block, re`{.*}`) -+ { newFunction(logf) } //@loc(block, re`{.*}`) +- { println(logf) } //@loc(block, re`{[^}]*}`) ++ { newFunction(logf) } //@loc(block, re`{[^}]*}`) @@ -10 +10,4 @@ +func newFunction(logf func( string, ...any)) { + println(logf) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt index 943a3ac672c..7cb22d1577d 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt @@ -6,18 +6,18 @@ This test exercises function and method extraction. -- basic.go -- package extract -//@codeactionedit(A_XLessThanYP, "refactor.extract", meth1, "Extract method") -//@codeactionedit(A_XLessThanYP, "refactor.extract", func1, "Extract function") -//@codeactionedit(A_AddP1, "refactor.extract", meth2, "Extract method") -//@codeactionedit(A_AddP1, "refactor.extract", func2, "Extract function") -//@codeactionedit(A_AddP2, "refactor.extract", meth3, "Extract method") -//@codeactionedit(A_AddP2, "refactor.extract", func3, "Extract function") -//@codeactionedit(A_XLessThanY, "refactor.extract", meth4, "Extract method") -//@codeactionedit(A_XLessThanY, "refactor.extract", func4, "Extract function") -//@codeactionedit(A_Add1, "refactor.extract", meth5, "Extract method") -//@codeactionedit(A_Add1, "refactor.extract", func5, "Extract function") -//@codeactionedit(A_Add2, "refactor.extract", meth6, "Extract method") -//@codeactionedit(A_Add2, "refactor.extract", func6, "Extract function") +//@codeactionedit(A_XLessThanYP, "refactor.extract.method", meth1) +//@codeactionedit(A_XLessThanYP, "refactor.extract.function", func1) +//@codeactionedit(A_AddP1, "refactor.extract.method", meth2) +//@codeactionedit(A_AddP1, "refactor.extract.function", func2) +//@codeactionedit(A_AddP2, "refactor.extract.method", meth3) +//@codeactionedit(A_AddP2, "refactor.extract.function", func3) +//@codeactionedit(A_XLessThanY, "refactor.extract.method", meth4) +//@codeactionedit(A_XLessThanY, "refactor.extract.function", func4) +//@codeactionedit(A_Add1, "refactor.extract.method", meth5) +//@codeactionedit(A_Add1, "refactor.extract.function", func5) +//@codeactionedit(A_Add2, "refactor.extract.method", meth6) +//@codeactionedit(A_Add2, "refactor.extract.function", func6) type A struct { x int @@ -30,7 +30,7 @@ func (a *A) XLessThanYP() bool { func (a *A) AddP() int { sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) - return sum //@loc(A_AddP2, re`return.*sum`) + return sum //@loc(A_AddP2, re`return.*?sum`) } func (a A) XLessThanY() bool { @@ -39,7 +39,7 @@ func (a A) XLessThanY() bool { func (a A) Add() int { sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) - return sum //@loc(A_Add2, re`return.*sum`) + return sum //@loc(A_Add2, re`return.*?sum`) } -- @func1/basic.go -- @@ -63,8 +63,8 @@ func (a A) Add() int { + -- @func3/basic.go -- @@ -27 +27 @@ -- return sum //@loc(A_AddP2, re`return.*sum`) -+ return newFunction(sum) //@loc(A_AddP2, re`return.*sum`) +- return sum //@loc(A_AddP2, re`return.*?sum`) ++ return newFunction(sum) //@loc(A_AddP2, re`return.*?sum`) @@ -30 +30,4 @@ +func newFunction(sum int) int { + return sum @@ -91,8 +91,8 @@ func (a A) Add() int { + -- @func6/basic.go -- @@ -36 +36 @@ -- return sum //@loc(A_Add2, re`return.*sum`) -+ return newFunction(sum) //@loc(A_Add2, re`return.*sum`) +- return sum //@loc(A_Add2, re`return.*?sum`) ++ return newFunction(sum) //@loc(A_Add2, re`return.*?sum`) @@ -39 +39,4 @@ +func newFunction(sum int) int { + return sum @@ -119,8 +119,8 @@ func (a A) Add() int { + -- @meth3/basic.go -- @@ -27 +27 @@ -- return sum //@loc(A_AddP2, re`return.*sum`) -+ return a.newMethod(sum) //@loc(A_AddP2, re`return.*sum`) +- return sum //@loc(A_AddP2, re`return.*?sum`) ++ return a.newMethod(sum) //@loc(A_AddP2, re`return.*?sum`) @@ -30 +30,4 @@ +func (*A) newMethod(sum int) int { + return sum @@ -147,8 +147,8 @@ func (a A) Add() int { + -- @meth6/basic.go -- @@ -36 +36 @@ -- return sum //@loc(A_Add2, re`return.*sum`) -+ return a.newMethod(sum) //@loc(A_Add2, re`return.*sum`) +- return sum //@loc(A_Add2, re`return.*?sum`) ++ return a.newMethod(sum) //@loc(A_Add2, re`return.*?sum`) @@ -39 +39,4 @@ +func (A) newMethod(sum int) int { + return sum @@ -157,18 +157,23 @@ func (a A) Add() int { -- context.go -- package extract -import "context" +import ( + "context" + "testing" +) -//@codeactionedit(B_AddP, "refactor.extract", contextMeth1, "Extract method") -//@codeactionedit(B_AddP, "refactor.extract", contextFunc1, "Extract function") -//@codeactionedit(B_LongList, "refactor.extract", contextMeth2, "Extract method") -//@codeactionedit(B_LongList, "refactor.extract", contextFunc2, "Extract function") +//@codeactionedit(B_AddP, "refactor.extract.method", contextMeth1) +//@codeactionedit(B_AddP, "refactor.extract.function", contextFunc1) +//@codeactionedit(B_LongList, "refactor.extract.method", contextMeth2) +//@codeactionedit(B_LongList, "refactor.extract.function", contextFunc2) +//@codeactionedit(B_AddPWithB, "refactor.extract.function", contextFuncB) +//@codeactionedit(B_LongListWithT, "refactor.extract.function", contextFuncT) type B struct { x int y int } - + func (b *B) AddP(ctx context.Context) (int, error) { sum := b.x + b.y return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) @@ -180,39 +185,72 @@ func (b *B) LongList(ctx context.Context) (int, error) { p3 := 1 return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) } + +func (b *B) AddPWithB(ctx context.Context, tB *testing.B) (int, error) { + sum := b.x + b.y //@loc(B_AddPWithB, re`(?s:^.*?Err\(\))`) + tB.Skip() + return sum, ctx.Err() +} + +func (b *B) LongListWithT(ctx context.Context, t *testing.T) (int, error) { + p1 := 1 + p2 := 1 + p3 := 1 + p4 := p1 + p2 //@loc(B_LongListWithT, re`(?s:^.*?Err\(\))`) + t.Skip() + return p4 + p3, ctx.Err() +} -- @contextMeth1/context.go -- -@@ -17 +17 @@ +@@ -22 +22 @@ - return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) + return b.newMethod(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) -@@ -20 +20,4 @@ +@@ -25 +25,4 @@ +func (*B) newMethod(ctx context.Context, sum int) (int, error) { + return sum, ctx.Err() +} + -- @contextMeth2/context.go -- -@@ -24 +24 @@ +@@ -29 +29 @@ - return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) + return b.newMethod(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) -@@ -26 +26,4 @@ -+ +@@ -32 +32,4 @@ +func (*B) newMethod(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { + return p1 + p2 + p3, ctx.Err() +} ++ -- @contextFunc2/context.go -- -@@ -24 +24 @@ +@@ -29 +29 @@ - return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) + return newFunction(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) -@@ -26 +26,4 @@ -+ +@@ -32 +32,4 @@ +func newFunction(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { + return p1 + p2 + p3, ctx.Err() +} ++ -- @contextFunc1/context.go -- -@@ -17 +17 @@ +@@ -22 +22 @@ - return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) + return newFunction(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) -@@ -20 +20,4 @@ +@@ -25 +25,4 @@ +func newFunction(ctx context.Context, sum int) (int, error) { + return sum, ctx.Err() +} + +-- @contextFuncB/context.go -- +@@ -33 +33,6 @@ +- sum := b.x + b.y //@loc(B_AddPWithB, re`(?s:^.*?Err\(\))`) ++ //@loc(B_AddPWithB, re`(?s:^.*?Err\(\))`) ++ return newFunction(ctx, tB, b) ++} ++ ++func newFunction(ctx context.Context, tB *testing.B, b *B) (int, error) { ++ sum := b.x + b.y +-- @contextFuncT/context.go -- +@@ -42 +42,6 @@ +- p4 := p1 + p2 //@loc(B_LongListWithT, re`(?s:^.*?Err\(\))`) ++ //@loc(B_LongListWithT, re`(?s:^.*?Err\(\))`) ++ return newFunction(ctx, t, p1, p2, p3) ++} ++ ++func newFunction(ctx context.Context, t *testing.T, p1 int, p2 int, p3 int) (int, error) { ++ p4 := p1 + p2 diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt index c3e75defc51..259b84a09a3 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt @@ -16,7 +16,7 @@ import ( func f() io.Reader func main() { - switch r := f().(type) { //@codeactionedit("f()", "refactor.extract", type_switch_func_call) + switch r := f().(type) { //@codeactionedit("f()", "refactor.extract.variable", type_switch_func_call) default: _ = r } @@ -24,6 +24,6 @@ func main() { -- @type_switch_func_call/extract_switch.go -- @@ -10 +10,2 @@ -- switch r := f().(type) { //@codeactionedit("f()", "refactor.extract", type_switch_func_call) +- switch r := f().(type) { //@codeactionedit("f()", "refactor.extract.variable", type_switch_func_call) + x := f() -+ switch r := x.(type) { //@codeactionedit("f()", "refactor.extract", type_switch_func_call) ++ switch r := x.(type) { //@codeactionedit("f()", "refactor.extract.variable", type_switch_func_call) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt index 685b4ff9372..8c500d02c1e 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt @@ -8,41 +8,41 @@ See extract_variable_resolve.txt for the same test with resolve support. package extract func _() { - var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) - var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) + var _ = 1 + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) + var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) } -- @basic_lit1/basic_lit.go -- @@ -4 +4,2 @@ -- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) + x := 1 -+ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) ++ var _ = x + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) -- @basic_lit2/basic_lit.go -- @@ -5 +5,2 @@ -- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) + x := 3 + 4 -+ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) ++ var _ = x //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) -- func_call.go -- package extract import "strconv" func _() { - x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) + x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) str := "1" - b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) + b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) } -- @func_call1/func_call.go -- @@ -6 +6,2 @@ -- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) + x := append([]int{}, 1) -+ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) ++ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) -- @func_call2/func_call.go -- @@ -8 +8,2 @@ -- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) + x, x1 := strconv.Atoi(str) -+ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) ++ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) -- scope.go -- package extract @@ -51,20 +51,20 @@ import "go/ast" func _() { x0 := 0 if true { - y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) + y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) } if true { - x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) + x1 := !false //@codeactionedit("!false", "refactor.extract.variable", scope2) } } -- @scope1/scope.go -- @@ -8 +8,2 @@ -- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) + x := ast.CompositeLit{} -+ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) ++ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) -- @scope2/scope.go -- @@ -11 +11,2 @@ -- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) +- x1 := !false //@codeactionedit("!false", "refactor.extract.variable", scope2) + x := !false -+ x1 := x //@codeactionedit("!false", "refactor.extract", scope2) ++ x1 := x //@codeactionedit("!false", "refactor.extract.variable", scope2) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt index dc6ad787afb..b3a9a67059f 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt @@ -19,41 +19,41 @@ See extract_variable.txt for the same test without resolve support. package extract func _() { - var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) - var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) + var _ = 1 + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) + var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) } -- @basic_lit1/basic_lit.go -- @@ -4 +4,2 @@ -- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) + x := 1 -+ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) ++ var _ = x + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) -- @basic_lit2/basic_lit.go -- @@ -5 +5,2 @@ -- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) + x := 3 + 4 -+ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) ++ var _ = x //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) -- func_call.go -- package extract import "strconv" func _() { - x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) + x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) str := "1" - b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) + b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) } -- @func_call1/func_call.go -- @@ -6 +6,2 @@ -- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) + x := append([]int{}, 1) -+ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) ++ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) -- @func_call2/func_call.go -- @@ -8 +8,2 @@ -- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) + x, x1 := strconv.Atoi(str) -+ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) ++ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) -- scope.go -- package extract @@ -62,20 +62,20 @@ import "go/ast" func _() { x0 := 0 if true { - y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) + y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) } if true { - x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) + x1 := !false //@codeactionedit("!false", "refactor.extract.variable", scope2) } } -- @scope1/scope.go -- @@ -8 +8,2 @@ -- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) + x := ast.CompositeLit{} -+ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) ++ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) -- @scope2/scope.go -- @@ -11 +11,2 @@ -- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) +- x1 := !false //@codeactionedit("!false", "refactor.extract.variable", scope2) + x := !false -+ x1 := x //@codeactionedit("!false", "refactor.extract", scope2) ++ x1 := x //@codeactionedit("!false", "refactor.extract.variable", scope2) diff --git a/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt b/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt index 0226d8207d1..158a9f9a22c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt @@ -12,28 +12,28 @@ go 1.18 package main // docs -func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration) +func fn() {} //@codeactionedit("func", "refactor.extract.toNewFile", function_declaration) -func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name) +func fn2() {} //@codeactionedit("fn2", "refactor.extract.toNewFile", only_select_func_name) -func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name) +func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract.toNewFile", zero_width_selection_on_func_name) // docs -type T int //@codeactionedit("type", "refactor.extract", type_declaration) +type T int //@codeactionedit("type", "refactor.extract.toNewFile", type_declaration) // docs -var V int //@codeactionedit("var", "refactor.extract", var_declaration) +var V int //@codeactionedit("var", "refactor.extract.toNewFile", var_declaration) // docs -const K = "" //@codeactionedit("const", "refactor.extract", const_declaration) +const K = "" //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration) -const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs) +const ( //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration_multiple_specs) P = iota Q R ) -func fnA () {} //@codeaction("func", mdEnd, "refactor.extract", multiple_declarations) +func fnA () {} //@codeaction("func", mdEnd, "refactor.extract.toNewFile", multiple_declarations) // unattached comment @@ -45,13 +45,13 @@ func fnB () {} //@loc(mdEnd, "}") -- existing2.1.go -- -- b.go -- package main -func existing() {} //@codeactionedit("func", "refactor.extract", file_name_conflict) -func existing2() {} //@codeactionedit("func", "refactor.extract", file_name_conflict_again) +func existing() {} //@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict) +func existing2() {} //@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict_again) -- single_import.go -- package main import "fmt" -func F() { //@codeactionedit("func", "refactor.extract", single_import) +func F() { //@codeactionedit("func", "refactor.extract.toNewFile", single_import) fmt.Println() } @@ -65,24 +65,24 @@ import ( func init(){ log.Println() } -func F() { //@codeactionedit("func", "refactor.extract", multiple_imports) +func F() { //@codeactionedit("func", "refactor.extract.toNewFile", multiple_imports) fmt.Println() } -func g() string{ //@codeactionedit("func", "refactor.extract", renamed_import) +func g() string{ //@codeactionedit("func", "refactor.extract.toNewFile", renamed_import) return time1.Now().string() } -- blank_import.go -- package main import _ "fmt" -func F() {} //@codeactionedit("func", "refactor.extract", blank_import) +func F() {} //@codeactionedit("func", "refactor.extract.toNewFile", blank_import) -- @blank_import/blank_import.go -- @@ -3 +3 @@ --func F() {} //@codeactionedit("func", "refactor.extract", blank_import) -+//@codeactionedit("func", "refactor.extract", blank_import) +-func F() {} //@codeactionedit("func", "refactor.extract.toNewFile", blank_import) ++//@codeactionedit("func", "refactor.extract.toNewFile", blank_import) -- @blank_import/f.go -- @@ -0,0 +1,3 @@ +package main @@ -91,8 +91,8 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) -- @const_declaration/a.go -- @@ -16,2 +16 @@ -// docs --const K = "" //@codeactionedit("const", "refactor.extract", const_declaration) -+//@codeactionedit("const", "refactor.extract", const_declaration) +-const K = "" //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration) ++//@codeactionedit("const", "refactor.extract.toNewFile", const_declaration) -- @const_declaration/k.go -- @@ -0,0 +1,4 @@ +package main @@ -101,7 +101,7 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) +const K = "" -- @const_declaration_multiple_specs/a.go -- @@ -19,6 +19 @@ --const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs) +-const ( //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration_multiple_specs) - P = iota - Q - R @@ -111,15 +111,15 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) @@ -0,0 +1,7 @@ +package main + -+const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs) ++const ( //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration_multiple_specs) + P = iota + Q + R +) -- @file_name_conflict/b.go -- @@ -2 +2 @@ --func existing() {} //@codeactionedit("func", "refactor.extract", file_name_conflict) -+//@codeactionedit("func", "refactor.extract", file_name_conflict) +-func existing() {} //@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict) ++//@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict) -- @file_name_conflict/existing.1.go -- @@ -0,0 +1,3 @@ +package main @@ -127,8 +127,8 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) +func existing() {} -- @file_name_conflict_again/b.go -- @@ -3 +3 @@ --func existing2() {} //@codeactionedit("func", "refactor.extract", file_name_conflict_again) -+//@codeactionedit("func", "refactor.extract", file_name_conflict_again) +-func existing2() {} //@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict_again) ++//@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict_again) -- @file_name_conflict_again/existing2.2.go -- @@ -0,0 +1,3 @@ +package main @@ -137,8 +137,8 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) -- @function_declaration/a.go -- @@ -3,2 +3 @@ -// docs --func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration) -+//@codeactionedit("func", "refactor.extract", function_declaration) +-func fn() {} //@codeactionedit("func", "refactor.extract.toNewFile", function_declaration) ++//@codeactionedit("func", "refactor.extract.toNewFile", function_declaration) -- @function_declaration/fn.go -- @@ -0,0 +1,4 @@ +package main @@ -149,22 +149,22 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) package main // docs -func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration) +func fn() {} //@codeactionedit("func", "refactor.extract.toNewFile", function_declaration) -func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name) +func fn2() {} //@codeactionedit("fn2", "refactor.extract.toNewFile", only_select_func_name) -func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name) +func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract.toNewFile", zero_width_selection_on_func_name) // docs -type T int //@codeactionedit("type", "refactor.extract", type_declaration) +type T int //@codeactionedit("type", "refactor.extract.toNewFile", type_declaration) // docs -var V int //@codeactionedit("var", "refactor.extract", var_declaration) +var V int //@codeactionedit("var", "refactor.extract.toNewFile", var_declaration) // docs -const K = "" //@codeactionedit("const", "refactor.extract", const_declaration) +const K = "" //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration) -const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs) +const ( //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration_multiple_specs) P = iota Q R @@ -176,7 +176,7 @@ const ( //@codeactionedit("const", "refactor.extract", const_declaration_multipl -- @multiple_declarations/fna.go -- package main -func fnA() {} //@codeaction("func", mdEnd, "refactor.extract", multiple_declarations) +func fnA() {} //@codeaction("func", mdEnd, "refactor.extract.toNewFile", multiple_declarations) // unattached comment @@ -189,7 +189,7 @@ func fnB() {} + "fmt" +) + -+func F() { //@codeactionedit("func", "refactor.extract", multiple_imports) ++func F() { //@codeactionedit("func", "refactor.extract.toNewFile", multiple_imports) + fmt.Println() +} -- @multiple_imports/multiple_imports.go -- @@ -197,13 +197,13 @@ func fnB() {} - "fmt" + @@ -10,3 +10 @@ --func F() { //@codeactionedit("func", "refactor.extract", multiple_imports) +-func F() { //@codeactionedit("func", "refactor.extract.toNewFile", multiple_imports) - fmt.Println() -} -- @only_select_func_name/a.go -- @@ -6 +6 @@ --func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name) -+//@codeactionedit("fn2", "refactor.extract", only_select_func_name) +-func fn2() {} //@codeactionedit("fn2", "refactor.extract.toNewFile", only_select_func_name) ++//@codeactionedit("fn2", "refactor.extract.toNewFile", only_select_func_name) -- @only_select_func_name/fn2.go -- @@ -0,0 +1,3 @@ +package main @@ -217,20 +217,20 @@ func fnB() {} + "fmt" +) + -+func F() { //@codeactionedit("func", "refactor.extract", single_import) ++func F() { //@codeactionedit("func", "refactor.extract.toNewFile", single_import) + fmt.Println() +} -- @single_import/single_import.go -- @@ -2,4 +2 @@ -import "fmt" --func F() { //@codeactionedit("func", "refactor.extract", single_import) +-func F() { //@codeactionedit("func", "refactor.extract.toNewFile", single_import) - fmt.Println() -} -- @type_declaration/a.go -- @@ -10,2 +10 @@ -// docs --type T int //@codeactionedit("type", "refactor.extract", type_declaration) -+//@codeactionedit("type", "refactor.extract", type_declaration) +-type T int //@codeactionedit("type", "refactor.extract.toNewFile", type_declaration) ++//@codeactionedit("type", "refactor.extract.toNewFile", type_declaration) -- @type_declaration/t.go -- @@ -0,0 +1,4 @@ +package main @@ -240,8 +240,8 @@ func fnB() {} -- @var_declaration/a.go -- @@ -13,2 +13 @@ -// docs --var V int //@codeactionedit("var", "refactor.extract", var_declaration) -+//@codeactionedit("var", "refactor.extract", var_declaration) +-var V int //@codeactionedit("var", "refactor.extract.toNewFile", var_declaration) ++//@codeactionedit("var", "refactor.extract.toNewFile", var_declaration) -- @var_declaration/v.go -- @@ -0,0 +1,4 @@ +package main @@ -250,8 +250,8 @@ func fnB() {} +var V int -- @zero_width_selection_on_func_name/a.go -- @@ -8 +8 @@ --func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name) -+//@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name) +-func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract.toNewFile", zero_width_selection_on_func_name) ++//@codeactionedit(re`()fn3`, "refactor.extract.toNewFile", zero_width_selection_on_func_name) -- @zero_width_selection_on_func_name/fn3.go -- @@ -0,0 +1,3 @@ +package main @@ -265,7 +265,7 @@ func fnB() {} + time1 "time" +) + -+func g() string { //@codeactionedit("func", "refactor.extract", renamed_import) ++func g() string { //@codeactionedit("func", "refactor.extract.toNewFile", renamed_import) + return time1.Now().string() +} -- @renamed_import/multiple_imports.go -- @@ -273,7 +273,7 @@ func fnB() {} - time1 "time" + @@ -13,4 +13 @@ --func g() string{ //@codeactionedit("func", "refactor.extract", renamed_import) +-func g() string{ //@codeactionedit("func", "refactor.extract.toNewFile", renamed_import) - return time1.Now().string() -} - diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt index deac1d78507..2b947bf8bbc 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt @@ -28,49 +28,49 @@ type basicStruct struct { foo int } -var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) type nestedStruct struct { bar string basic basicStruct } -var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) -var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +var _ = data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) -- @a1/a.go -- @@ -11 +11,3 @@ --var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +-var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) +var _ = basicStruct{ + foo: 0, -+} //@codeactionedit("}", "refactor.rewrite", a1) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) -- @a2/a.go -- @@ -18 +18,4 @@ --var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +-var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) +var _ = twoArgStruct{ + foo: 0, + bar: "", -+} //@codeactionedit("}", "refactor.rewrite", a2) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) -- @a3/a.go -- @@ -25 +25,4 @@ --var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +-var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) +var _ = nestedStruct{ + bar: "", + basic: basicStruct{}, -+} //@codeactionedit("}", "refactor.rewrite", a3) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) -- @a4/a.go -- @@ -27 +27,3 @@ --var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +-var _ = data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) +var _ = data.B{ + ExportedInt: 0, -+} //@codeactionedit("}", "refactor.rewrite", a4) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) -- a2.go -- package fillstruct @@ -82,57 +82,57 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) type funStructComplex struct { fn func(i int, s string) (string, int) } -var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) -- @a21/a2.go -- @@ -11 +11,7 @@ --var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +-var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) +var _ = typedStruct{ + m: map[string]int{}, + s: []int{}, + c: make(chan int), + c1: make(<-chan int), + a: [2]string{}, -+} //@codeactionedit("}", "refactor.rewrite", a21) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) -- @a22/a2.go -- @@ -17 +17,4 @@ --var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +-var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) +var _ = funStruct{ + fn: func(i int) int { + }, -+} //@codeactionedit("}", "refactor.rewrite", a22) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) -- @a23/a2.go -- @@ -23 +23,4 @@ --var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +-var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) +var _ = funStructComplex{ + fn: func(i int, s string) (string, int) { + }, -+} //@codeactionedit("}", "refactor.rewrite", a23) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) -- @a24/a2.go -- @@ -29 +29,4 @@ --var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +-var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) +var _ = funStructEmpty{ + fn: func() { + }, -+} //@codeactionedit("}", "refactor.rewrite", a24) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) -- a3.go -- package fillstruct @@ -150,7 +150,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +var _ = Bar{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -161,7 +161,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) type pointerBuiltinStruct struct { b *bool @@ -169,23 +169,23 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) var _ = []ast.BasicLit{ - {}, //@codeactionedit("}", "refactor.rewrite", a34) + {}, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) } -var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) -- @a31/a3.go -- @@ -17 +17,4 @@ --var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +-var _ = Bar{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) +var _ = Bar{ + X: &Foo{}, + Y: &Foo{}, -+} //@codeactionedit("}", "refactor.rewrite", a31) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) -- @a32/a3.go -- @@ -28 +28,9 @@ --var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +-var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) +var _ = importedStruct{ + m: map[*ast.CompositeLit]ast.Field{}, + s: []ast.BadExpr{}, @@ -194,31 +194,31 @@ var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) + fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { + }, + st: ast.CompositeLit{}, -+} //@codeactionedit("}", "refactor.rewrite", a32) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) -- @a33/a3.go -- @@ -36 +36,5 @@ --var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +-var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) +var _ = pointerBuiltinStruct{ + b: new(bool), + s: new(string), + i: new(int), -+} //@codeactionedit("}", "refactor.rewrite", a33) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) -- @a34/a3.go -- @@ -39 +39,5 @@ -- {}, //@codeactionedit("}", "refactor.rewrite", a34) +- {}, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) + { + ValuePos: 0, + Kind: 0, + Value: "", -+ }, //@codeactionedit("}", "refactor.rewrite", a34) ++ }, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) -- @a35/a3.go -- @@ -42 +42,5 @@ --var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +-var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) +var _ = []ast.BasicLit{{ + ValuePos: 0, + Kind: 0, + Value: "", -+}} //@codeactionedit("}", "refactor.rewrite", a35) ++}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) -- a4.go -- package fillstruct @@ -244,49 +244,49 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) + var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) var s string - var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) + var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) + var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) var node *ast.CompositeLit - var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) + var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) } -- @a41/a4.go -- @@ -25 +25,3 @@ -- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) +- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) + var _ = iStruct{ + X: x, -+ } //@codeactionedit("}", "refactor.rewrite", a41) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) -- @a42/a4.go -- @@ -28 +28,3 @@ -- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) +- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) + var _ = sStruct{ + str: s, -+ } //@codeactionedit("}", "refactor.rewrite", a42) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) -- @a43/a4.go -- @@ -35 +35,5 @@ -- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) +- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) + var _ = multiFill{ + num: n, + strin: s, + arr: []int{}, -+ } //@codeactionedit("}", "refactor.rewrite", a43) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) -- @a45/a4.go -- @@ -38 +38,3 @@ -- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) +- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) + var _ = assignStruct{ + n: node, -+ } //@codeactionedit("}", "refactor.rewrite", a45) --- fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) +-- fillStruct.go -- package fillstruct type StructA struct { @@ -306,43 +306,43 @@ type StructA3 struct { } func fill() { - a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) - b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) - c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) + a := StructA{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) + b := StructA2{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) + c := StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) if true { - _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) + _ = StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) } } --- @fill_struct1/fill_struct.go -- +-- @fillStruct1/fillStruct.go -- @@ -20 +20,7 @@ -- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) +- a := StructA{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) + a := StructA{ + unexportedIntField: 0, + ExportedIntField: 0, + MapA: map[int]string{}, + Array: []int{}, + StructB: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct1) --- @fill_struct2/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) +-- @fillStruct2/fillStruct.go -- @@ -21 +21,3 @@ -- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) +- b := StructA2{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) + b := StructA2{ + B: &StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct2) --- @fill_struct3/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) +-- @fillStruct3/fillStruct.go -- @@ -22 +22,3 @@ -- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) +- c := StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) + c := StructA3{ + B: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct3) --- @fill_struct4/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) +-- @fillStruct4/fillStruct.go -- @@ -24 +24,3 @@ -- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) +- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) + _ = StructA3{ + B: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct4) --- fill_struct_anon.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) +-- fillStruct_anon.go -- package fillstruct type StructAnon struct { @@ -355,17 +355,17 @@ type StructAnon struct { } func fill() { - _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) + _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) } --- @fill_struct_anon/fill_struct_anon.go -- +-- @fillStruct_anon/fillStruct_anon.go -- @@ -13 +13,5 @@ -- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) + _ := StructAnon{ + a: struct{}{}, + b: map[string]interface{}{}, + c: map[string]struct{d int; e bool}{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) --- fill_struct_nested.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) +-- fillStruct_nested.go -- package fillstruct type StructB struct { @@ -378,17 +378,17 @@ type StructC struct { func nested() { c := StructB{ - StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) + StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) } } --- @fill_nested/fill_struct_nested.go -- +-- @fill_nested/fillStruct_nested.go -- @@ -13 +13,3 @@ -- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) +- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) + StructC: StructC{ + unexportedInt: 0, -+ }, //@codeactionedit("}", "refactor.rewrite", fill_nested) --- fill_struct_package.go -- ++ }, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) +-- fillStruct_package.go -- package fillstruct import ( @@ -398,26 +398,26 @@ import ( ) func unexported() { - a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) - _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) + a := data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) + _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) } --- @fill_struct_package1/fill_struct_package.go -- +-- @fillStruct_package1/fillStruct_package.go -- @@ -10 +10,3 @@ -- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +- a := data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) + a := data.B{ + ExportedInt: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) --- @fill_struct_package2/fill_struct_package.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) +-- @fillStruct_package2/fillStruct_package.go -- @@ -11 +11,7 @@ -- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) + _ = h2.Client{ + Transport: nil, + CheckRedirect: func(req *h2.Request, via []*h2.Request) error { + }, + Jar: nil, + Timeout: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) --- fill_struct_partial.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) +-- fillStruct_partial.go -- package fillstruct type StructPartialA struct { @@ -434,22 +434,22 @@ type StructPartialB struct { func fill() { a := StructPartialA{ PrefilledInt: 5, - } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1) + } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_partial1) b := StructPartialB{ /* this comment should disappear */ PrefilledInt: 7, // This comment should be blown away. /* As should this one */ - } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2) + } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_partial2) } --- @fill_struct_partial1/fill_struct_partial.go -- +-- @fillStruct_partial1/fillStruct_partial.go -- @@ -16 +16,3 @@ - PrefilledInt: 5, + PrefilledInt: 5, + UnfilledInt: 0, + StructPartialB: StructPartialB{}, --- @fill_struct_partial2/fill_struct_partial.go -- +-- @fillStruct_partial2/fillStruct_partial.go -- @@ -19,4 +19,2 @@ - /* this comment should disappear */ - PrefilledInt: 7, // This comment should be blown away. @@ -457,7 +457,7 @@ func fill() { - this one */ + PrefilledInt: 7, + UnfilledInt: 0, --- fill_struct_spaces.go -- +-- fillStruct_spaces.go -- package fillstruct type StructD struct { @@ -465,16 +465,16 @@ type StructD struct { } func spaces() { - d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) + d := StructD{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) } --- @fill_struct_spaces/fill_struct_spaces.go -- +-- @fillStruct_spaces/fillStruct_spaces.go -- @@ -8 +8,3 @@ -- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +- d := StructD{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) + d := StructD{ + ExportedIntField: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) --- fill_struct_unsafe.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) +-- fillStruct_unsafe.go -- package fillstruct import "unsafe" @@ -485,16 +485,16 @@ type unsafeStruct struct { } func fill() { - _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) + _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) } --- @fill_struct_unsafe/fill_struct_unsafe.go -- +-- @fillStruct_unsafe/fillStruct_unsafe.go -- @@ -11 +11,4 @@ -- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) + _ := unsafeStruct{ + x: 0, + p: nil, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) -- typeparams.go -- package fillstruct @@ -506,59 +506,59 @@ type basicStructWithTypeParams[T any] struct { foo T } -var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) type twoArgStructWithTypeParams[F, B any] struct { foo F bar B } -var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) var _ = twoArgStructWithTypeParams[int, string]{ bar: "bar", -} //@codeactionedit("}", "refactor.rewrite", typeparams3) +} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams3) type nestedStructWithTypeParams struct { bar string basic basicStructWithTypeParams[int] } -var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) func _[T any]() { type S struct{ t T } - _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) + _ = S{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) } -- @typeparams1/typeparams.go -- @@ -11 +11,3 @@ --var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +-var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) +var _ = basicStructWithTypeParams[int]{ + foo: 0, -+} //@codeactionedit("}", "refactor.rewrite", typeparams1) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) -- @typeparams2/typeparams.go -- @@ -18 +18,4 @@ --var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +-var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) +var _ = twoArgStructWithTypeParams[string, int]{ + foo: "", + bar: 0, -+} //@codeactionedit("}", "refactor.rewrite", typeparams2) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) -- @typeparams3/typeparams.go -- @@ -21 +21 @@ + foo: 0, -- @typeparams4/typeparams.go -- @@ -29 +29,4 @@ --var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +-var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) +var _ = nestedStructWithTypeParams{ + bar: "", + basic: basicStructWithTypeParams{}, -+} //@codeactionedit("}", "refactor.rewrite", typeparams4) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) -- @typeparams5/typeparams.go -- @@ -33 +33,3 @@ -- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +- _ = S{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) + _ = S{ + t: *new(T), -+ } //@codeactionedit("}", "refactor.rewrite", typeparams5) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) -- issue63921.go -- package fillstruct @@ -571,5 +571,5 @@ type invalidStruct struct { func _() { // Note: the golden content for issue63921 is empty: fillstruct produces no // edits, but does not panic. - invalidStruct{} //@codeactionedit("}", "refactor.rewrite", issue63921) + invalidStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", issue63921) } diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt index e553d1c5993..24e7a9126e2 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt @@ -39,49 +39,49 @@ type basicStruct struct { foo int } -var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) type nestedStruct struct { bar string basic basicStruct } -var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) -var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +var _ = data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) -- @a1/a.go -- @@ -11 +11,3 @@ --var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +-var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) +var _ = basicStruct{ + foo: 0, -+} //@codeactionedit("}", "refactor.rewrite", a1) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) -- @a2/a.go -- @@ -18 +18,4 @@ --var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +-var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) +var _ = twoArgStruct{ + foo: 0, + bar: "", -+} //@codeactionedit("}", "refactor.rewrite", a2) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) -- @a3/a.go -- @@ -25 +25,4 @@ --var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +-var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) +var _ = nestedStruct{ + bar: "", + basic: basicStruct{}, -+} //@codeactionedit("}", "refactor.rewrite", a3) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) -- @a4/a.go -- @@ -27 +27,3 @@ --var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +-var _ = data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) +var _ = data.B{ + ExportedInt: 0, -+} //@codeactionedit("}", "refactor.rewrite", a4) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) -- a2.go -- package fillstruct @@ -93,57 +93,57 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) type funStructComplex struct { fn func(i int, s string) (string, int) } -var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) -- @a21/a2.go -- @@ -11 +11,7 @@ --var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +-var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) +var _ = typedStruct{ + m: map[string]int{}, + s: []int{}, + c: make(chan int), + c1: make(<-chan int), + a: [2]string{}, -+} //@codeactionedit("}", "refactor.rewrite", a21) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) -- @a22/a2.go -- @@ -17 +17,4 @@ --var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +-var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) +var _ = funStruct{ + fn: func(i int) int { + }, -+} //@codeactionedit("}", "refactor.rewrite", a22) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) -- @a23/a2.go -- @@ -23 +23,4 @@ --var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +-var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) +var _ = funStructComplex{ + fn: func(i int, s string) (string, int) { + }, -+} //@codeactionedit("}", "refactor.rewrite", a23) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) -- @a24/a2.go -- @@ -29 +29,4 @@ --var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +-var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) +var _ = funStructEmpty{ + fn: func() { + }, -+} //@codeactionedit("}", "refactor.rewrite", a24) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) -- a3.go -- package fillstruct @@ -161,7 +161,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +var _ = Bar{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -172,7 +172,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) type pointerBuiltinStruct struct { b *bool @@ -180,23 +180,23 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) var _ = []ast.BasicLit{ - {}, //@codeactionedit("}", "refactor.rewrite", a34) + {}, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) } -var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) -- @a31/a3.go -- @@ -17 +17,4 @@ --var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +-var _ = Bar{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) +var _ = Bar{ + X: &Foo{}, + Y: &Foo{}, -+} //@codeactionedit("}", "refactor.rewrite", a31) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) -- @a32/a3.go -- @@ -28 +28,9 @@ --var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +-var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) +var _ = importedStruct{ + m: map[*ast.CompositeLit]ast.Field{}, + s: []ast.BadExpr{}, @@ -205,31 +205,31 @@ var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) + fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { + }, + st: ast.CompositeLit{}, -+} //@codeactionedit("}", "refactor.rewrite", a32) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) -- @a33/a3.go -- @@ -36 +36,5 @@ --var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +-var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) +var _ = pointerBuiltinStruct{ + b: new(bool), + s: new(string), + i: new(int), -+} //@codeactionedit("}", "refactor.rewrite", a33) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) -- @a34/a3.go -- @@ -39 +39,5 @@ -- {}, //@codeactionedit("}", "refactor.rewrite", a34) +- {}, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) + { + ValuePos: 0, + Kind: 0, + Value: "", -+ }, //@codeactionedit("}", "refactor.rewrite", a34) ++ }, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) -- @a35/a3.go -- @@ -42 +42,5 @@ --var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +-var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) +var _ = []ast.BasicLit{{ + ValuePos: 0, + Kind: 0, + Value: "", -+}} //@codeactionedit("}", "refactor.rewrite", a35) ++}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) -- a4.go -- package fillstruct @@ -255,49 +255,49 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) + var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) var s string - var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) + var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) + var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) var node *ast.CompositeLit - var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) + var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) } -- @a41/a4.go -- @@ -25 +25,3 @@ -- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) +- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) + var _ = iStruct{ + X: x, -+ } //@codeactionedit("}", "refactor.rewrite", a41) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) -- @a42/a4.go -- @@ -28 +28,3 @@ -- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) +- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) + var _ = sStruct{ + str: s, -+ } //@codeactionedit("}", "refactor.rewrite", a42) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) -- @a43/a4.go -- @@ -35 +35,5 @@ -- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) +- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) + var _ = multiFill{ + num: n, + strin: s, + arr: []int{}, -+ } //@codeactionedit("}", "refactor.rewrite", a43) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) -- @a45/a4.go -- @@ -38 +38,3 @@ -- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) +- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) + var _ = assignStruct{ + n: node, -+ } //@codeactionedit("}", "refactor.rewrite", a45) --- fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) +-- fillStruct.go -- package fillstruct type StructA struct { @@ -317,43 +317,43 @@ type StructA3 struct { } func fill() { - a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) - b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) - c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) + a := StructA{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) + b := StructA2{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) + c := StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) if true { - _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) + _ = StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) } } --- @fill_struct1/fill_struct.go -- +-- @fillStruct1/fillStruct.go -- @@ -20 +20,7 @@ -- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) +- a := StructA{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) + a := StructA{ + unexportedIntField: 0, + ExportedIntField: 0, + MapA: map[int]string{}, + Array: []int{}, + StructB: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct1) --- @fill_struct2/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) +-- @fillStruct2/fillStruct.go -- @@ -21 +21,3 @@ -- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) +- b := StructA2{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) + b := StructA2{ + B: &StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct2) --- @fill_struct3/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) +-- @fillStruct3/fillStruct.go -- @@ -22 +22,3 @@ -- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) +- c := StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) + c := StructA3{ + B: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct3) --- @fill_struct4/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) +-- @fillStruct4/fillStruct.go -- @@ -24 +24,3 @@ -- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) +- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) + _ = StructA3{ + B: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct4) --- fill_struct_anon.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) +-- fillStruct_anon.go -- package fillstruct type StructAnon struct { @@ -366,17 +366,17 @@ type StructAnon struct { } func fill() { - _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) + _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) } --- @fill_struct_anon/fill_struct_anon.go -- +-- @fillStruct_anon/fillStruct_anon.go -- @@ -13 +13,5 @@ -- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) + _ := StructAnon{ + a: struct{}{}, + b: map[string]interface{}{}, + c: map[string]struct{d int; e bool}{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) --- fill_struct_nested.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) +-- fillStruct_nested.go -- package fillstruct type StructB struct { @@ -389,17 +389,17 @@ type StructC struct { func nested() { c := StructB{ - StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) + StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) } } --- @fill_nested/fill_struct_nested.go -- +-- @fill_nested/fillStruct_nested.go -- @@ -13 +13,3 @@ -- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) +- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) + StructC: StructC{ + unexportedInt: 0, -+ }, //@codeactionedit("}", "refactor.rewrite", fill_nested) --- fill_struct_package.go -- ++ }, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) +-- fillStruct_package.go -- package fillstruct import ( @@ -409,26 +409,26 @@ import ( ) func unexported() { - a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) - _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) + a := data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) + _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) } --- @fill_struct_package1/fill_struct_package.go -- +-- @fillStruct_package1/fillStruct_package.go -- @@ -10 +10,3 @@ -- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +- a := data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) + a := data.B{ + ExportedInt: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) --- @fill_struct_package2/fill_struct_package.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) +-- @fillStruct_package2/fillStruct_package.go -- @@ -11 +11,7 @@ -- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) + _ = h2.Client{ + Transport: nil, + CheckRedirect: func(req *h2.Request, via []*h2.Request) error { + }, + Jar: nil, + Timeout: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) --- fill_struct_partial.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) +-- fillStruct_partial.go -- package fillstruct type StructPartialA struct { @@ -445,22 +445,22 @@ type StructPartialB struct { func fill() { a := StructPartialA{ PrefilledInt: 5, - } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1) + } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_partial1) b := StructPartialB{ /* this comment should disappear */ PrefilledInt: 7, // This comment should be blown away. /* As should this one */ - } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2) + } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_partial2) } --- @fill_struct_partial1/fill_struct_partial.go -- +-- @fillStruct_partial1/fillStruct_partial.go -- @@ -16 +16,3 @@ - PrefilledInt: 5, + PrefilledInt: 5, + UnfilledInt: 0, + StructPartialB: StructPartialB{}, --- @fill_struct_partial2/fill_struct_partial.go -- +-- @fillStruct_partial2/fillStruct_partial.go -- @@ -19,4 +19,2 @@ - /* this comment should disappear */ - PrefilledInt: 7, // This comment should be blown away. @@ -468,7 +468,7 @@ func fill() { - this one */ + PrefilledInt: 7, + UnfilledInt: 0, --- fill_struct_spaces.go -- +-- fillStruct_spaces.go -- package fillstruct type StructD struct { @@ -476,16 +476,16 @@ type StructD struct { } func spaces() { - d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) + d := StructD{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) } --- @fill_struct_spaces/fill_struct_spaces.go -- +-- @fillStruct_spaces/fillStruct_spaces.go -- @@ -8 +8,3 @@ -- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +- d := StructD{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) + d := StructD{ + ExportedIntField: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) --- fill_struct_unsafe.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) +-- fillStruct_unsafe.go -- package fillstruct import "unsafe" @@ -496,16 +496,16 @@ type unsafeStruct struct { } func fill() { - _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) + _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) } --- @fill_struct_unsafe/fill_struct_unsafe.go -- +-- @fillStruct_unsafe/fillStruct_unsafe.go -- @@ -11 +11,4 @@ -- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) + _ := unsafeStruct{ + x: 0, + p: nil, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) -- typeparams.go -- package fillstruct @@ -517,59 +517,59 @@ type basicStructWithTypeParams[T any] struct { foo T } -var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) type twoArgStructWithTypeParams[F, B any] struct { foo F bar B } -var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) var _ = twoArgStructWithTypeParams[int, string]{ bar: "bar", -} //@codeactionedit("}", "refactor.rewrite", typeparams3) +} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams3) type nestedStructWithTypeParams struct { bar string basic basicStructWithTypeParams[int] } -var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) func _[T any]() { type S struct{ t T } - _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) + _ = S{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) } -- @typeparams1/typeparams.go -- @@ -11 +11,3 @@ --var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +-var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) +var _ = basicStructWithTypeParams[int]{ + foo: 0, -+} //@codeactionedit("}", "refactor.rewrite", typeparams1) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) -- @typeparams2/typeparams.go -- @@ -18 +18,4 @@ --var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +-var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) +var _ = twoArgStructWithTypeParams[string, int]{ + foo: "", + bar: 0, -+} //@codeactionedit("}", "refactor.rewrite", typeparams2) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) -- @typeparams3/typeparams.go -- @@ -21 +21 @@ + foo: 0, -- @typeparams4/typeparams.go -- @@ -29 +29,4 @@ --var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +-var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) +var _ = nestedStructWithTypeParams{ + bar: "", + basic: basicStructWithTypeParams{}, -+} //@codeactionedit("}", "refactor.rewrite", typeparams4) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) -- @typeparams5/typeparams.go -- @@ -33 +33,3 @@ -- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +- _ = S{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) + _ = S{ + t: *new(T), -+ } //@codeactionedit("}", "refactor.rewrite", typeparams5) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) -- issue63921.go -- package fillstruct @@ -582,5 +582,5 @@ type invalidStruct struct { func _() { // Note: the golden content for issue63921 is empty: fillstruct produces no // edits, but does not panic. - invalidStruct{} //@codeactionedit("}", "refactor.rewrite", issue63921) + invalidStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", issue63921) } diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt index 2c1b19e130c..0d92b05fc41 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt @@ -50,19 +50,19 @@ func (notificationTwo) isNotification() {} func doSwitch() { var b data.TypeB switch b { - case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) + case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a1) } var a typeA switch a { - case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) + case typeAThree: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a2) } var n notification - switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) + switch n.(type) { //@codeactionedit("{", "refactor.rewrite.fillSwitch", a3) } - switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) + switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite.fillSwitch", a4) } var s struct { @@ -70,7 +70,7 @@ func doSwitch() { } switch s.a { - case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) + case typeAThree: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a5) } } -- @a1/a.go -- diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt index 504acd6043e..84464417b81 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt @@ -61,19 +61,19 @@ func (notificationTwo) isNotification() {} func doSwitch() { var b data.TypeB switch b { - case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) + case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a1) } var a typeA switch a { - case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) + case typeAThree: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a2) } var n notification - switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) + switch n.(type) { //@codeactionedit("{", "refactor.rewrite.fillSwitch", a3) } - switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) + switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite.fillSwitch", a4) } var s struct { @@ -81,7 +81,7 @@ func doSwitch() { } switch s.a { - case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) + case typeAThree: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a5) } } -- @a1/a.go -- diff --git a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt index b37009c78d9..1b9f487c49d 100644 --- a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt +++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt @@ -8,16 +8,16 @@ go 1.18 -- basic.go -- package extract -func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) - a := 1 //@codeaction("a", end, "refactor.extract", inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract.function", outer) + a := 1 //@codeaction("a", end, "refactor.extract.function", inner) _ = a + 4 //@loc(end, "4") } //@loc(closeBracket, "}") -- @inner/basic.go -- package extract -func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) - //@codeaction("a", end, "refactor.extract", inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract.function", outer) + //@codeaction("a", end, "refactor.extract.function", inner) newFunction() //@loc(end, "4") } @@ -29,8 +29,8 @@ func newFunction() { -- @outer/basic.go -- package extract -func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) - //@codeaction("a", end, "refactor.extract", inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract.function", outer) + //@codeaction("a", end, "refactor.extract.function", inner) newFunction() //@loc(end, "4") } @@ -44,7 +44,7 @@ package extract func _() bool { x := 1 - if x == 0 { //@codeaction("if", ifend, "refactor.extract", return) + if x == 0 { //@codeaction("if", ifend, "refactor.extract.function", return) return true } //@loc(ifend, "}") return false @@ -55,7 +55,7 @@ package extract func _() bool { x := 1 - //@codeaction("if", ifend, "refactor.extract", return) + //@codeaction("if", ifend, "refactor.extract.function", return) shouldReturn, returnValue := newFunction(x) if shouldReturn { return returnValue @@ -74,7 +74,7 @@ func newFunction(x int) (bool, bool) { package extract func _() bool { - x := 1 //@codeaction("x", rnnEnd, "refactor.extract", rnn) + x := 1 //@codeaction("x", rnnEnd, "refactor.extract.function", rnn) if x == 0 { return true } @@ -85,7 +85,7 @@ func _() bool { package extract func _() bool { - //@codeaction("x", rnnEnd, "refactor.extract", rnn) + //@codeaction("x", rnnEnd, "refactor.extract.function", rnn) return newFunction() //@loc(rnnEnd, "false") } @@ -105,7 +105,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - z := "bye" //@codeaction("z", rcEnd, "refactor.extract", rc) + z := "bye" //@codeaction("z", rcEnd, "refactor.extract.function", rc) if y == z { return x, y, fmt.Errorf("same") } else if false { @@ -123,7 +123,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - //@codeaction("z", rcEnd, "refactor.extract", rc) + //@codeaction("z", rcEnd, "refactor.extract.function", rc) z, shouldReturn, returnValue, returnValue1, returnValue2 := newFunction(y, x) if shouldReturn { return returnValue, returnValue1, returnValue2 @@ -150,7 +150,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - z := "bye" //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) + z := "bye" //@codeaction("z", rcnnEnd, "refactor.extract.function", rcnn) if y == z { return x, y, fmt.Errorf("same") } else if false { @@ -168,7 +168,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) + //@codeaction("z", rcnnEnd, "refactor.extract.function", rcnn) return newFunction(y, x) //@loc(rcnnEnd, "nil") } @@ -190,7 +190,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - if n == nil { //@codeaction("if", rflEnd, "refactor.extract", rfl) + if n == nil { //@codeaction("if", rflEnd, "refactor.extract.function", rfl) return true } //@loc(rflEnd, "}") return false @@ -204,7 +204,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - //@codeaction("if", rflEnd, "refactor.extract", rfl) + //@codeaction("if", rflEnd, "refactor.extract.function", rfl) shouldReturn, returnValue := newFunction(n) if shouldReturn { return returnValue @@ -227,7 +227,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - if n == nil { //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) + if n == nil { //@codeaction("if", rflnnEnd, "refactor.extract.function", rflnn) return true } return false //@loc(rflnnEnd, "false") @@ -241,7 +241,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) + //@codeaction("if", rflnnEnd, "refactor.extract.function", rflnn) return newFunction(n) //@loc(rflnnEnd, "false") }) } @@ -258,7 +258,7 @@ package extract func _() string { x := 1 - if x == 0 { //@codeaction("if", riEnd, "refactor.extract", ri) + if x == 0 { //@codeaction("if", riEnd, "refactor.extract.function", ri) x = 3 return "a" } //@loc(riEnd, "}") @@ -271,7 +271,7 @@ package extract func _() string { x := 1 - //@codeaction("if", riEnd, "refactor.extract", ri) + //@codeaction("if", riEnd, "refactor.extract.function", ri) shouldReturn, returnValue := newFunction(x) if shouldReturn { return returnValue @@ -293,7 +293,7 @@ package extract func _() string { x := 1 - if x == 0 { //@codeaction("if", rinnEnd, "refactor.extract", rinn) + if x == 0 { //@codeaction("if", rinnEnd, "refactor.extract.function", rinn) x = 3 return "a" } @@ -306,7 +306,7 @@ package extract func _() string { x := 1 - //@codeaction("if", rinnEnd, "refactor.extract", rinn) + //@codeaction("if", rinnEnd, "refactor.extract.function", rinn) return newFunction(x) //@loc(rinnEnd, "\"b\"") } @@ -324,10 +324,10 @@ package extract func _() { a := 1 - a = 5 //@codeaction("a", araend, "refactor.extract", ara) + a = 5 //@codeaction("a", araend, "refactor.extract.function", ara) a = a + 2 //@loc(araend, "2") - b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) + b := a * 2 //@codeaction("b", arbend, "refactor.extract.function", arb) _ = b + 4 //@loc(arbend, "4") } @@ -336,10 +336,10 @@ package extract func _() { a := 1 - //@codeaction("a", araend, "refactor.extract", ara) + //@codeaction("a", araend, "refactor.extract.function", ara) a = newFunction(a) //@loc(araend, "2") - b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) + b := a * 2 //@codeaction("b", arbend, "refactor.extract.function", arb) _ = b + 4 //@loc(arbend, "4") } @@ -354,10 +354,10 @@ package extract func _() { a := 1 - a = 5 //@codeaction("a", araend, "refactor.extract", ara) + a = 5 //@codeaction("a", araend, "refactor.extract.function", ara) a = a + 2 //@loc(araend, "2") - //@codeaction("b", arbend, "refactor.extract", arb) + //@codeaction("b", arbend, "refactor.extract.function", arb) newFunction(a) //@loc(arbend, "4") } @@ -371,7 +371,7 @@ package extract func _() { newFunction := 1 - a := newFunction //@codeaction("a", "newFunction", "refactor.extract", scope) + a := newFunction //@codeaction("a", "newFunction", "refactor.extract.function", scope) _ = a // avoid diagnostic } @@ -384,7 +384,7 @@ package extract func _() { newFunction := 1 - a := newFunction2(newFunction) //@codeaction("a", "newFunction", "refactor.extract", scope) + a := newFunction2(newFunction) //@codeaction("a", "newFunction", "refactor.extract.function", scope) _ = a // avoid diagnostic } @@ -402,7 +402,7 @@ package extract func _() { var a []int - a = append(a, 2) //@codeaction("a", siEnd, "refactor.extract", si) + a = append(a, 2) //@codeaction("a", siEnd, "refactor.extract.function", si) b := 4 //@loc(siEnd, "4") a = append(a, b) } @@ -412,7 +412,7 @@ package extract func _() { var a []int - //@codeaction("a", siEnd, "refactor.extract", si) + //@codeaction("a", siEnd, "refactor.extract.function", si) a, b := newFunction(a) //@loc(siEnd, "4") a = append(a, b) } @@ -429,7 +429,7 @@ package extract func _() { var b []int var a int - a = 2 //@codeaction("a", srEnd, "refactor.extract", sr) + a = 2 //@codeaction("a", srEnd, "refactor.extract.function", sr) b = []int{} b = append(b, a) //@loc(srEnd, ")") b[0] = 1 @@ -441,7 +441,7 @@ package extract func _() { var b []int var a int - //@codeaction("a", srEnd, "refactor.extract", sr) + //@codeaction("a", srEnd, "refactor.extract.function", sr) b = newFunction(a, b) //@loc(srEnd, ")") b[0] = 1 } @@ -458,7 +458,7 @@ package extract func _() { var b []int - a := 2 //@codeaction("a", upEnd, "refactor.extract", up) + a := 2 //@codeaction("a", upEnd, "refactor.extract.function", up) b = []int{} b = append(b, a) //@loc(upEnd, ")") b[0] = 1 @@ -472,7 +472,7 @@ package extract func _() { var b []int - //@codeaction("a", upEnd, "refactor.extract", up) + //@codeaction("a", upEnd, "refactor.extract.function", up) a, b := newFunction(b) //@loc(upEnd, ")") b[0] = 1 if a == 2 { @@ -491,9 +491,9 @@ func newFunction(b []int) (int, []int) { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) - // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) - _ = a + 4 //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract.function", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract.function", comment2) + _ = a + 4 //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract.function", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -504,9 +504,9 @@ package extract func _() { /* comment in the middle of a line */ - //@codeaction("a", commentEnd, "refactor.extract", comment1) - // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) - newFunction() //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) + //@codeaction("a", commentEnd, "refactor.extract.function", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract.function", comment2) + newFunction() //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract.function", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -522,9 +522,9 @@ func newFunction() { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) - // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) - newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract.function", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract.function", comment2) + newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract.function", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -538,9 +538,9 @@ func newFunction(a int) { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) - // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) - newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract.function", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract.function", comment2) + newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract.function", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -557,7 +557,7 @@ import "strconv" func _() { i, err := strconv.Atoi("1") - u, err := strconv.Atoi("2") //@codeaction("u", ")", "refactor.extract", redefine) + u, err := strconv.Atoi("2") //@codeaction("u", ")", "refactor.extract.function", redefine) if i == u || err == nil { return } @@ -570,7 +570,7 @@ import "strconv" func _() { i, err := strconv.Atoi("1") - u, err := newFunction() //@codeaction("u", ")", "refactor.extract", redefine) + u, err := newFunction() //@codeaction("u", ")", "refactor.extract.function", redefine) if i == u || err == nil { return } @@ -581,3 +581,36 @@ func newFunction() (int, error) { return u, err } +-- anonymousfunc.go -- +package extract +import "cmp" +import "slices" + +// issue go#64821 +func _() { + var s []string //@codeaction("var", anonEnd, "refactor.extract.function", anon1) + slices.SortFunc(s, func(a, b string) int { + return cmp.Compare(a, b) + }) + println(s) //@loc(anonEnd, ")") +} + +-- @anon1/anonymousfunc.go -- +package extract +import "cmp" +import "slices" + +// issue go#64821 +func _() { + //@codeaction("var", anonEnd, "refactor.extract.function", anon1) + newFunction() //@loc(anonEnd, ")") +} + +func newFunction() { + var s []string + slices.SortFunc(s, func(a, b string) int { + return cmp.Compare(a, b) + }) + println(s) +} + diff --git a/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt index cadc8e94263..aaca44d6c7a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt +++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt @@ -12,7 +12,7 @@ package extract import "fmt" func main() { - x := []rune{} //@codeaction("x", end, "refactor.extract", ext) + x := []rune{} //@codeaction("x", end, "refactor.extract.function", ext) s := "HELLO" for _, c := range s { x = append(x, c) @@ -26,7 +26,7 @@ package extract import "fmt" func main() { - //@codeaction("x", end, "refactor.extract", ext) + //@codeaction("x", end, "refactor.extract.function", ext) x := newFunction() //@loc(end, "}") fmt.Printf("%x\n", x) } diff --git a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt index 0d22e6ad483..1f14360d2e9 100644 --- a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt +++ b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt @@ -12,7 +12,7 @@ package func_arg func A( a string, b, c int64, - x int /*@codeaction("x", "x", "refactor.rewrite", func_arg)*/, + x int /*@codeaction("x", "x", "refactor.rewrite.joinLines", func_arg)*/, y int, ) (r1 string, r2, r3 int64, r4 int, r5 int) { return a, b, c, x, y @@ -21,7 +21,7 @@ func A( -- @func_arg/func_arg/func_arg.go -- package func_arg -func A(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", func_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { +func A(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite.joinLines", func_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { return a, b, c, x, y } @@ -29,7 +29,7 @@ func A(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", f package func_ret func A(a string, b, c int64, x int, y int) ( - r1 string /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, + r1 string /*@codeaction("r1", "r1", "refactor.rewrite.joinLines", func_ret)*/, r2, r3 int64, r4 int, r5 int, @@ -40,7 +40,7 @@ func A(a string, b, c int64, x int, y int) ( -- @func_ret/func_ret/func_ret.go -- package func_ret -func A(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, r2, r3 int64, r4 int, r5 int) { +func A(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite.joinLines", func_ret)*/, r2, r3 int64, r4 int, r5 int) { return a, b, c, x, y } @@ -50,20 +50,20 @@ package functype_arg type A func( a string, b, c int64, - x int /*@codeaction("x", "x", "refactor.rewrite", functype_arg)*/, + x int /*@codeaction("x", "x", "refactor.rewrite.joinLines", functype_arg)*/, y int, ) (r1 string, r2, r3 int64, r4 int, r5 int) -- @functype_arg/functype_arg/functype_arg.go -- package functype_arg -type A func(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", functype_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) +type A func(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite.joinLines", functype_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) -- functype_ret/functype_ret.go -- package functype_ret type A func(a string, b, c int64, x int, y int) ( - r1 string /*@codeaction("r1", "r1", "refactor.rewrite", functype_ret)*/, + r1 string /*@codeaction("r1", "r1", "refactor.rewrite.joinLines", functype_ret)*/, r2, r3 int64, r4 int, r5 int, @@ -72,7 +72,7 @@ type A func(a string, b, c int64, x int, y int) ( -- @functype_ret/functype_ret/functype_ret.go -- package functype_ret -type A func(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite", functype_ret)*/, r2, r3 int64, r4 int, r5 int) +type A func(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite.joinLines", functype_ret)*/, r2, r3 int64, r4 int, r5 int) -- func_call/func_call.go -- package func_call @@ -81,7 +81,7 @@ import "fmt" func a() { fmt.Println( - 1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, + 1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4), @@ -94,7 +94,7 @@ package func_call import "fmt" func a() { - fmt.Println(1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4)) + fmt.Println(1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4)) } -- indent/indent.go -- @@ -108,7 +108,7 @@ func a() { 2, 3, fmt.Sprintf( - "hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join arguments into one line")*/, + "hello %d" /*@codeaction("hello", "hello", "refactor.rewrite.joinLines", indent)*/, 4, )) } @@ -123,7 +123,7 @@ func a() { 1, 2, 3, - fmt.Sprintf("hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join arguments into one line")*/, 4)) + fmt.Sprintf("hello %d" /*@codeaction("hello", "hello", "refactor.rewrite.joinLines", indent)*/, 4)) } -- structelts/structelts.go -- @@ -137,7 +137,7 @@ type A struct{ func a() { _ = A{ a: 1, - b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/, + b: 2 /*@codeaction("b", "b", "refactor.rewrite.joinLines", structelts)*/, } } @@ -150,7 +150,7 @@ type A struct{ } func a() { - _ = A{a: 1, b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/} + _ = A{a: 1, b: 2 /*@codeaction("b", "b", "refactor.rewrite.joinLines", structelts)*/} } -- sliceelts/sliceelts.go -- @@ -158,7 +158,7 @@ package sliceelts func a() { _ = []int{ - 1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, + 1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", sliceelts)*/, 2, } } @@ -167,7 +167,7 @@ func a() { package sliceelts func a() { - _ = []int{1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, 2} + _ = []int{1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", sliceelts)*/, 2} } -- mapelts/mapelts.go -- @@ -175,7 +175,7 @@ package mapelts func a() { _ = map[string]int{ - "a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, + "a": 1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", mapelts)*/, "b": 2, } } @@ -184,14 +184,14 @@ func a() { package mapelts func a() { - _ = map[string]int{"a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, "b": 2} + _ = map[string]int{"a": 1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", mapelts)*/, "b": 2} } -- starcomment/starcomment.go -- package starcomment func A( - /*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite", starcomment)*/, + /*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite.joinLines", starcomment)*/, /*4*/ y /*5*/ int /*6*/, ) (string, int) { return x, y @@ -200,7 +200,7 @@ func A( -- @starcomment/starcomment/starcomment.go -- package starcomment -func A(/*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite", starcomment)*/, /*4*/ y /*5*/ int /*6*/) (string, int) { +func A(/*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite.joinLines", starcomment)*/, /*4*/ y /*5*/ int /*6*/) (string, int) { return x, y } diff --git a/gopls/internal/test/marker/testdata/codeaction/inline.txt b/gopls/internal/test/marker/testdata/codeaction/inline.txt index 0c5bcb41658..050fe25b8ec 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline.txt @@ -1,4 +1,4 @@ -This is a minimal test of the refactor.inline code action, without resolve support. +This is a minimal test of the refactor.inline.call code action, without resolve support. See inline_resolve.txt for same test with resolve support. -- go.mod -- @@ -9,7 +9,7 @@ go 1.18 package a func _() { - println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline) + println(add(1, 2)) //@codeaction("add", ")", "refactor.inline.call", inline) } func add(x, y int) int { return x + y } @@ -18,7 +18,7 @@ func add(x, y int) int { return x + y } package a func _() { - println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline) + println(1 + 2) //@codeaction("add", ")", "refactor.inline.call", inline) } func add(x, y int) int { return x + y } diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt index 02c27e6505b..fa8476e91f6 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt @@ -1,4 +1,4 @@ -This is a minimal test of the refactor.inline code actions, with resolve support. +This is a minimal test of the refactor.inline.call code actions, with resolve support. See inline.txt for same test without resolve support. -- capabilities.json -- @@ -20,7 +20,7 @@ go 1.18 package a func _() { - println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline) + println(add(1, 2)) //@codeaction("add", ")", "refactor.inline.call", inline) } func add(x, y int) int { return x + y } @@ -29,7 +29,7 @@ func add(x, y int) int { return x + y } package a func _() { - println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline) + println(1 + 2) //@codeaction("add", ")", "refactor.inline.call", inline) } func add(x, y int) int { return x + y } diff --git a/gopls/internal/test/marker/testdata/codeaction/invertif.txt b/gopls/internal/test/marker/testdata/codeaction/invertif.txt index 57e77530844..02f856f6977 100644 --- a/gopls/internal/test/marker/testdata/codeaction/invertif.txt +++ b/gopls/internal/test/marker/testdata/codeaction/invertif.txt @@ -10,7 +10,7 @@ import ( func Boolean() { b := true - if b { //@codeactionedit("if b", "refactor.rewrite", boolean) + if b { //@codeactionedit("if b", "refactor.rewrite.invertIf", boolean) fmt.Println("A") } else { fmt.Println("B") @@ -18,7 +18,7 @@ func Boolean() { } func BooleanFn() { - if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) + if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite.invertIf", boolean_fn) fmt.Println("A") } else { fmt.Println("B") @@ -30,7 +30,7 @@ func DontRemoveParens() { a := false b := true if !(a || - b) { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) + b) { //@codeactionedit("b", "refactor.rewrite.invertIf", dont_remove_parens) fmt.Println("A") } else { fmt.Println("B") @@ -46,7 +46,7 @@ func ElseIf() { // No inversion expected for else-if, that would become unreadable if len(os.Args) > 2 { fmt.Println("A") - } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) + } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite.invertIf", else_if) fmt.Println("B") } else { fmt.Println("C") @@ -54,7 +54,7 @@ func ElseIf() { } func GreaterThan() { - if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite", greater_than) + if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite.invertIf", greater_than) fmt.Println("A") } else { fmt.Println("B") @@ -63,7 +63,7 @@ func GreaterThan() { func NotBoolean() { b := true - if !b { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) + if !b { //@codeactionedit("if !b", "refactor.rewrite.invertIf", not_boolean) fmt.Println("A") } else { fmt.Println("B") @@ -71,7 +71,7 @@ func NotBoolean() { } func RemoveElse() { - if true { //@codeactionedit("if true", "refactor.rewrite", remove_else) + if true { //@codeactionedit("if true", "refactor.rewrite.invertIf", remove_else) fmt.Println("A") } else { fmt.Println("B") @@ -83,7 +83,7 @@ func RemoveElse() { func RemoveParens() { b := true - if !(b) { //@codeactionedit("if", "refactor.rewrite", remove_parens) + if !(b) { //@codeactionedit("if", "refactor.rewrite.invertIf", remove_parens) fmt.Println("A") } else { fmt.Println("B") @@ -91,7 +91,7 @@ func RemoveParens() { } func Semicolon() { - if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite", semicolon) + if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite.invertIf", semicolon) fmt.Println("A") } else { fmt.Println("B") @@ -99,7 +99,7 @@ func Semicolon() { } func SemicolonAnd() { - if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite", semicolon_and) + if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite.invertIf", semicolon_and) fmt.Println("A") } else { fmt.Println("B") @@ -107,7 +107,7 @@ func SemicolonAnd() { } func SemicolonOr() { - if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) + if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite.invertIf", semicolon_or) fmt.Println("A") } else { fmt.Println("B") @@ -116,103 +116,103 @@ func SemicolonOr() { -- @boolean/p.go -- @@ -10,3 +10 @@ -- if b { //@codeactionedit("if b", "refactor.rewrite", boolean) +- if b { //@codeactionedit("if b", "refactor.rewrite.invertIf", boolean) - fmt.Println("A") - } else { + if !b { @@ -14 +12,2 @@ -+ } else { //@codeactionedit("if b", "refactor.rewrite", boolean) ++ } else { //@codeactionedit("if b", "refactor.rewrite.invertIf", boolean) + fmt.Println("A") -- @boolean_fn/p.go -- @@ -18,3 +18 @@ -- if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) +- if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite.invertIf", boolean_fn) - fmt.Println("A") - } else { + if !os.IsPathSeparator('X') { @@ -22 +20,2 @@ -+ } else { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) ++ } else { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite.invertIf", boolean_fn) + fmt.Println("A") -- @dont_remove_parens/p.go -- @@ -29,4 +29,2 @@ - if !(a || -- b) { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) +- b) { //@codeactionedit("b", "refactor.rewrite.invertIf", dont_remove_parens) - fmt.Println("A") - } else { + if (a || + b) { @@ -34 +32,2 @@ -+ } else { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) ++ } else { //@codeactionedit("b", "refactor.rewrite.invertIf", dont_remove_parens) + fmt.Println("A") -- @else_if/p.go -- @@ -46,3 +46 @@ -- } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) +- } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite.invertIf", else_if) - fmt.Println("B") - } else { + } else if os.Args[0] != "X" { @@ -50 +48,2 @@ -+ } else { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) ++ } else { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite.invertIf", else_if) + fmt.Println("B") -- @greater_than/p.go -- @@ -54,3 +54 @@ -- if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite", greater_than) +- if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite.invertIf", greater_than) - fmt.Println("A") - } else { + if len(os.Args) <= 2 { @@ -58 +56,2 @@ -+ } else { //@codeactionedit("i", "refactor.rewrite", greater_than) ++ } else { //@codeactionedit("i", "refactor.rewrite.invertIf", greater_than) + fmt.Println("A") -- @not_boolean/p.go -- @@ -63,3 +63 @@ -- if !b { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) +- if !b { //@codeactionedit("if !b", "refactor.rewrite.invertIf", not_boolean) - fmt.Println("A") - } else { + if b { @@ -67 +65,2 @@ -+ } else { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) ++ } else { //@codeactionedit("if !b", "refactor.rewrite.invertIf", not_boolean) + fmt.Println("A") -- @remove_else/p.go -- @@ -71,3 +71 @@ -- if true { //@codeactionedit("if true", "refactor.rewrite", remove_else) +- if true { //@codeactionedit("if true", "refactor.rewrite.invertIf", remove_else) - fmt.Println("A") - } else { + if false { @@ -78 +76,3 @@ -+ //@codeactionedit("if true", "refactor.rewrite", remove_else) ++ //@codeactionedit("if true", "refactor.rewrite.invertIf", remove_else) + fmt.Println("A") + -- @remove_parens/p.go -- @@ -83,3 +83 @@ -- if !(b) { //@codeactionedit("if", "refactor.rewrite", remove_parens) +- if !(b) { //@codeactionedit("if", "refactor.rewrite.invertIf", remove_parens) - fmt.Println("A") - } else { + if b { @@ -87 +85,2 @@ -+ } else { //@codeactionedit("if", "refactor.rewrite", remove_parens) ++ } else { //@codeactionedit("if", "refactor.rewrite.invertIf", remove_parens) + fmt.Println("A") -- @semicolon/p.go -- @@ -91,3 +91 @@ -- if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite", semicolon) +- if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite.invertIf", semicolon) - fmt.Println("A") - } else { + if _, err := fmt.Println("x"); err == nil { @@ -95 +93,2 @@ -+ } else { //@codeactionedit("if", "refactor.rewrite", semicolon) ++ } else { //@codeactionedit("if", "refactor.rewrite.invertIf", semicolon) + fmt.Println("A") -- @semicolon_and/p.go -- @@ -99,3 +99 @@ -- if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite", semicolon_and) +- if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite.invertIf", semicolon_and) - fmt.Println("A") - } else { + if n, err := fmt.Println("x"); err == nil || n <= 0 { @@ -103 +101,2 @@ -+ } else { //@codeactionedit("f", "refactor.rewrite", semicolon_and) ++ } else { //@codeactionedit("f", "refactor.rewrite.invertIf", semicolon_and) + fmt.Println("A") -- @semicolon_or/p.go -- @@ -107,3 +107 @@ -- if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) +- if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite.invertIf", semicolon_or) - fmt.Println("A") - } else { + if n, err := fmt.Println("x"); err == nil && n >= 5 { @@ -111 +109,2 @@ -+ } else { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) ++ } else { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite.invertIf", semicolon_or) + fmt.Println("A") diff --git a/gopls/internal/test/marker/testdata/codeaction/issue64558.txt b/gopls/internal/test/marker/testdata/codeaction/issue64558.txt index 59aaffba371..7ca661fbf00 100644 --- a/gopls/internal/test/marker/testdata/codeaction/issue64558.txt +++ b/gopls/internal/test/marker/testdata/codeaction/issue64558.txt @@ -8,7 +8,7 @@ go 1.18 package a func _() { - f(1, 2) //@ diag("2", re"too many arguments"), codeactionerr("f", ")", "refactor.inline", re`inlining failed \("args/params mismatch"\), likely because inputs were ill-typed`) + f(1, 2) //@ diag("2", re"too many arguments"), codeactionerr("f", ")", "refactor.inline.call", re`inlining failed \("args/params mismatch"\), likely because inputs were ill-typed`) } func f(int) {} diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt index 25ec6ae1d96..2b78b882df6 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt @@ -9,14 +9,14 @@ go 1.18 -- a/a.go -- package a -func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) return x } -- @a/a/a.go -- package a -func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) return x } @@ -99,7 +99,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite.removeUnusedParam", field) } func _() { @@ -108,7 +108,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite.removeUnusedParam", field) } func _() { @@ -117,7 +117,7 @@ func _() { -- ellipsis/ellipsis.go -- package ellipsis -func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite.removeUnusedParam", ellipsis) } func _() { @@ -138,7 +138,7 @@ func i() []any -- @ellipsis/ellipsis/ellipsis.go -- package ellipsis -func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite.removeUnusedParam", ellipsis) } func _() { @@ -162,7 +162,7 @@ func i() []any -- ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite.removeUnusedParam", ellipsis2) } func _() { @@ -176,7 +176,7 @@ func h() (int, int) -- @ellipsis2/ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite.removeUnusedParam", ellipsis2) } func _() { @@ -191,7 +191,7 @@ func h() (int, int) -- overlapping/overlapping.go -- package overlapping -func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") +func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite.removeUnusedParam", re"overlapping") return 0 } @@ -203,7 +203,7 @@ func _() { -- effects/effects.go -- package effects -func effects(x, y int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite", effects) +func effects(x, y int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite.removeUnusedParam", effects) return x } @@ -217,7 +217,7 @@ func _() { -- @effects/effects/effects.go -- package effects -func effects(x int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite", effects) +func effects(x int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite.removeUnusedParam", effects) return x } @@ -235,13 +235,13 @@ func _() { -- recursive/recursive.go -- package recursive -func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive) +func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", recursive) return Recursive(1) } -- @recursive/recursive/recursive.go -- package recursive -func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive) +func Recursive() int { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", recursive) return Recursive() } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt index 17abb98d5c9..b192d79b584 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt @@ -14,7 +14,7 @@ go 1.18 package a // A doc comment. -func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) // about to return return x // returning // just returned @@ -36,7 +36,7 @@ func one() int { package a // A doc comment. -func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) // about to return return x // returning // just returned diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt index e67e378fde3..ec8f63c34b3 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt @@ -10,7 +10,7 @@ go 1.18 -- a/a.go -- package a -func A(x, unused int) int { //@codeactionerr("unused", "unused", "refactor.rewrite", re"non-call function reference") +func A(x, unused int) int { //@codeactionerr("unused", "unused", "refactor.rewrite.removeUnusedParam", re"non-call function reference") return x } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt index 6fd4b9b6dcf..1f96d6b424c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt @@ -62,7 +62,7 @@ import "mod.test/c" var Chan chan c.C -func B(x, y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) +func B(x, y c.C) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", b) } -- c/c.go -- @@ -76,7 +76,7 @@ package d // Removing the parameter should remove this import. import "mod.test/c" -func D(x c.C) { //@codeaction("x", "x", "refactor.rewrite", d) +func D(x c.C) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", d) } func _() { @@ -148,14 +148,14 @@ import "mod.test/c" var Chan chan c.C -func B(y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) +func B(y c.C) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", b) } -- @d/d/d.go -- package d // Removing the parameter should remove this import. -func D() { //@codeaction("x", "x", "refactor.rewrite", d) +func D() { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", d) } func _() { diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_issue65217.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_issue65217.txt index 93d87d4dbec..f2ecae4ad1c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_issue65217.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_issue65217.txt @@ -27,7 +27,7 @@ func _() { _ = i } -func f(unused S, i int) int { //@codeaction("unused", "unused", "refactor.rewrite", rewrite, "Refactor: remove unused parameter"), diag("unused", re`unused`) +func f(unused S, i int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", rewrite), diag("unused", re`unused`) return i } @@ -53,6 +53,6 @@ func _() { _ = i } -func f(i int) int { //@codeaction("unused", "unused", "refactor.rewrite", rewrite, "Refactor: remove unused parameter"), diag("unused", re`unused`) +func f(i int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", rewrite), diag("unused", re`unused`) return i } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt index a62ff8cd679..174d9061927 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt @@ -18,7 +18,7 @@ package rm type Basic int -func (t Basic) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite", basic) +func (t Basic) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", basic) } func _(b Basic) { @@ -45,7 +45,7 @@ package rm type Basic int -func (t Basic) Foo() { //@codeaction("x", "x", "refactor.rewrite", basic) +func (t Basic) Foo() { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", basic) } func _(b Basic) { @@ -76,7 +76,7 @@ type Missing struct{} var r2 int -func (Missing) M(a, b, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite", missingrecv) +func (Missing) M(a, b, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite.removeUnusedParam", missingrecv) return a + c } @@ -104,7 +104,7 @@ type Missing struct{} var r2 int -func (Missing) M(a, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite", missingrecv) +func (Missing) M(a, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite.removeUnusedParam", missingrecv) return a + c } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt index c67e8a5d039..92f8d299272 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt @@ -20,14 +20,14 @@ go 1.18 -- a/a.go -- package a -func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) return x } -- @a/a/a.go -- package a -func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) return x } @@ -110,7 +110,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite.removeUnusedParam", field) } func _() { @@ -119,7 +119,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite.removeUnusedParam", field) } func _() { @@ -128,7 +128,7 @@ func _() { -- ellipsis/ellipsis.go -- package ellipsis -func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite.removeUnusedParam", ellipsis) } func _() { @@ -149,7 +149,7 @@ func i() []any -- @ellipsis/ellipsis/ellipsis.go -- package ellipsis -func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite.removeUnusedParam", ellipsis) } func _() { @@ -173,7 +173,7 @@ func i() []any -- ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite.removeUnusedParam", ellipsis2) } func _() { @@ -187,7 +187,7 @@ func h() (int, int) -- @ellipsis2/ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite.removeUnusedParam", ellipsis2) } func _() { @@ -202,7 +202,7 @@ func h() (int, int) -- overlapping/overlapping.go -- package overlapping -func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") +func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite.removeUnusedParam", re"overlapping") return 0 } @@ -214,7 +214,7 @@ func _() { -- effects/effects.go -- package effects -func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite", effects), diag("y", re"unused") +func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite.removeUnusedParam", effects), diag("y", re"unused") return x } @@ -228,7 +228,7 @@ func _() { -- @effects/effects/effects.go -- package effects -func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite", effects), diag("y", re"unused") +func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite.removeUnusedParam", effects), diag("y", re"unused") return x } @@ -246,13 +246,13 @@ func _() { -- recursive/recursive.go -- package recursive -func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive) +func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", recursive) return Recursive(1) } -- @recursive/recursive/recursive.go -- package recursive -func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive) +func Recursive() int { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", recursive) return Recursive() } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt index 5a707001d0e..f35662e3dad 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt @@ -21,7 +21,7 @@ package rm type T int -func (t T) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite", basic) +func (t T) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", basic) } -- use/use.go -- @@ -44,7 +44,7 @@ package rm type T int -func (t T) Foo() { //@codeaction("x", "x", "refactor.rewrite", basic) +func (t T) Foo() { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", basic) } -- @basic/use/use.go -- diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt index 60080028f0e..5b4cd37a51a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt @@ -3,7 +3,7 @@ This test checks that we can't remove parameters for packages with errors. -- p.go -- package p -func foo(unused int) { //@codeactionerr("unused", "unused", "refactor.rewrite", re"found 0") +func foo(unused int) { //@codeactionerr("unused", "unused", "refactor.rewrite.removeUnusedParam", re"found 0") } func _() { diff --git a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt index 76b8fb93c76..5600ccb777a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt +++ b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt @@ -9,7 +9,7 @@ go 1.18 -- func_arg/func_arg.go -- package func_arg -func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite", func_arg) +func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite.splitLines", func_arg) return a, b, c, x, y } @@ -21,14 +21,14 @@ func A( b, c int64, x int, y int, -) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite", func_arg) +) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite.splitLines", func_arg) return a, b, c, x, y } -- func_ret/func_ret.go -- package func_ret -func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("r1", "r1", "refactor.rewrite", func_ret) +func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("r1", "r1", "refactor.rewrite.splitLines", func_ret) return a, b, c, x, y } @@ -40,14 +40,14 @@ func A(a string, b, c int64, x int, y int) ( r2, r3 int64, r4 int, r5 int, -) { //@codeaction("r1", "r1", "refactor.rewrite", func_ret) +) { //@codeaction("r1", "r1", "refactor.rewrite.splitLines", func_ret) return a, b, c, x, y } -- functype_arg/functype_arg.go -- package functype_arg -type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite", functype_arg) +type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite.splitLines", functype_arg) -- @functype_arg/functype_arg/functype_arg.go -- package functype_arg @@ -57,12 +57,12 @@ type A func( b, c int64, x int, y int, -) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite", functype_arg) +) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite.splitLines", functype_arg) -- functype_ret/functype_ret.go -- package functype_ret -type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("r1", "r1", "refactor.rewrite", functype_ret) +type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("r1", "r1", "refactor.rewrite.splitLines", functype_ret) -- @functype_ret/functype_ret/functype_ret.go -- package functype_ret @@ -72,7 +72,7 @@ type A func(a string, b, c int64, x int, y int) ( r2, r3 int64, r4 int, r5 int, -) //@codeaction("r1", "r1", "refactor.rewrite", functype_ret) +) //@codeaction("r1", "r1", "refactor.rewrite.splitLines", functype_ret) -- func_call/func_call.go -- package func_call @@ -80,7 +80,7 @@ package func_call import "fmt" func a() { - fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", func_call) + fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite.splitLines", func_call) } -- @func_call/func_call/func_call.go -- @@ -94,7 +94,7 @@ func a() { 2, 3, fmt.Sprintf("hello %d", 4), - ) //@codeaction("1", "1", "refactor.rewrite", func_call) + ) //@codeaction("1", "1", "refactor.rewrite.splitLines", func_call) } -- indent/indent.go -- @@ -103,7 +103,7 @@ package indent import "fmt" func a() { - fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split arguments into separate lines") + fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("hello", "hello", "refactor.rewrite.splitLines", indent) } -- @indent/indent/indent.go -- @@ -115,7 +115,7 @@ func a() { fmt.Println(1, 2, 3, fmt.Sprintf( "hello %d", 4, - )) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split arguments into separate lines") + )) //@codeaction("hello", "hello", "refactor.rewrite.splitLines", indent) } -- indent2/indent2.go -- @@ -125,7 +125,7 @@ import "fmt" func a() { fmt. - Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split arguments into separate lines") + Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite.splitLines", indent2) } -- @indent2/indent2/indent2.go -- @@ -140,7 +140,7 @@ func a() { 2, 3, fmt.Sprintf("hello %d", 4), - ) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split arguments into separate lines") + ) //@codeaction("1", "1", "refactor.rewrite.splitLines", indent2) } -- structelts/structelts.go -- @@ -152,7 +152,7 @@ type A struct{ } func a() { - _ = A{a: 1, b: 2} //@codeaction("b", "b", "refactor.rewrite", structelts) + _ = A{a: 1, b: 2} //@codeaction("b", "b", "refactor.rewrite.splitLines", structelts) } -- @structelts/structelts/structelts.go -- @@ -167,14 +167,14 @@ func a() { _ = A{ a: 1, b: 2, - } //@codeaction("b", "b", "refactor.rewrite", structelts) + } //@codeaction("b", "b", "refactor.rewrite.splitLines", structelts) } -- sliceelts/sliceelts.go -- package sliceelts func a() { - _ = []int{1, 2} //@codeaction("1", "1", "refactor.rewrite", sliceelts) + _ = []int{1, 2} //@codeaction("1", "1", "refactor.rewrite.splitLines", sliceelts) } -- @sliceelts/sliceelts/sliceelts.go -- @@ -184,14 +184,14 @@ func a() { _ = []int{ 1, 2, - } //@codeaction("1", "1", "refactor.rewrite", sliceelts) + } //@codeaction("1", "1", "refactor.rewrite.splitLines", sliceelts) } -- mapelts/mapelts.go -- package mapelts func a() { - _ = map[string]int{"a": 1, "b": 2} //@codeaction("1", "1", "refactor.rewrite", mapelts) + _ = map[string]int{"a": 1, "b": 2} //@codeaction("1", "1", "refactor.rewrite.splitLines", mapelts) } -- @mapelts/mapelts/mapelts.go -- @@ -201,13 +201,13 @@ func a() { _ = map[string]int{ "a": 1, "b": 2, - } //@codeaction("1", "1", "refactor.rewrite", mapelts) + } //@codeaction("1", "1", "refactor.rewrite.splitLines", mapelts) } -- starcomment/starcomment.go -- package starcomment -func A(/*1*/ x /*2*/ string /*3*/, /*4*/ y /*5*/ int /*6*/) (string, int) { //@codeaction("x", "x", "refactor.rewrite", starcomment) +func A(/*1*/ x /*2*/ string /*3*/, /*4*/ y /*5*/ int /*6*/) (string, int) { //@codeaction("x", "x", "refactor.rewrite.splitLines", starcomment) return x, y } @@ -217,7 +217,7 @@ package starcomment func A( /*1*/ x /*2*/ string /*3*/, /*4*/ y /*5*/ int /*6*/, -) (string, int) { //@codeaction("x", "x", "refactor.rewrite", starcomment) +) (string, int) { //@codeaction("x", "x", "refactor.rewrite.splitLines", starcomment) return x, y } diff --git a/gopls/internal/test/marker/testdata/completion/comment.txt b/gopls/internal/test/marker/testdata/completion/comment.txt index 68f2c20cdcf..f66bfdab186 100644 --- a/gopls/internal/test/marker/testdata/completion/comment.txt +++ b/gopls/internal/test/marker/testdata/completion/comment.txt @@ -13,26 +13,26 @@ package comment_completion var p bool -//@complete(re"$") +//@complete(re"//()") func _() { var a int switch a { case 1: - //@complete(re"$") + //@complete(re"//()") _ = a } var b chan int select { case <-b: - //@complete(re"$") + //@complete(re"//()") _ = b } var ( - //@complete(re"$") + //@complete(re"//()") _ = a ) } diff --git a/gopls/internal/test/marker/testdata/completion/foobarbaz.txt b/gopls/internal/test/marker/testdata/completion/foobarbaz.txt index 24ac7171055..1da0a405404 100644 --- a/gopls/internal/test/marker/testdata/completion/foobarbaz.txt +++ b/gopls/internal/test/marker/testdata/completion/foobarbaz.txt @@ -476,7 +476,7 @@ func _() { 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\\}") + builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [2]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\\}") diff --git a/gopls/internal/test/marker/testdata/definition/comment.txt b/gopls/internal/test/marker/testdata/definition/comment.txt index ac253b27310..39c860708b8 100644 --- a/gopls/internal/test/marker/testdata/definition/comment.txt +++ b/gopls/internal/test/marker/testdata/definition/comment.txt @@ -5,10 +5,16 @@ module mod.com go 1.19 +-- path/path.go -- +package path + +func Join() //@loc(Join, "Join") + -- a.go -- package p import "strconv" //@loc(strconv, `"strconv"`) +import pathpkg "mod.com/path" const NumberBase = 10 //@loc(NumberBase, "NumberBase") @@ -19,3 +25,10 @@ func Conv(s string) int { //@loc(Conv, "Conv") i, _ := strconv.ParseInt(s, NumberBase, 64) return int(i) } + +// The declared and imported names of the package both work: +// [path.Join] //@ def("Join", Join) +// [pathpkg.Join] //@ def("Join", Join) +func _() { + pathpkg.Join() +} diff --git a/gopls/internal/test/marker/testdata/definition/embed.txt b/gopls/internal/test/marker/testdata/definition/embed.txt index 4bda1d71ebc..5dc976c8b4d 100644 --- a/gopls/internal/test/marker/testdata/definition/embed.txt +++ b/gopls/internal/test/marker/testdata/definition/embed.txt @@ -47,7 +47,7 @@ type J interface { //@loc(J, "J") -- b/b.go -- package b -import "mod.com/a" //@loc(AImport, re"\".*\"") +import "mod.com/a" //@loc(AImport, re"\"[^\"]*\"") type embed struct { F int //@loc(F, "F") diff --git a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt index f041ee9d9ae..34488bec417 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt @@ -27,7 +27,7 @@ func _() { // printf func _() { - printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, re"example.com.printfWrapper format %s reads arg #1, but call has 0 args") + printfWrapper("%s") //@diag(re`printfWrapper\(.*?\)`, re"example.com.printfWrapper format %s reads arg #1, but call has 0 args") } func printfWrapper(format string, args ...interface{}) { diff --git a/gopls/internal/test/marker/testdata/highlight/issue68918.txt b/gopls/internal/test/marker/testdata/highlight/issue68918.txt new file mode 100644 index 00000000000..b6ffb882df4 --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/issue68918.txt @@ -0,0 +1,15 @@ +Regression test for https://github.com/golang/go/issues/68918: +crash due to missing type information in CompositeLit. + +The corresponding go/types fix in Go 1.24 introduces a +new error message, hence the -ignore_extra_diags flag. + +-- flags -- +-ignore_extra_diags + +-- a.go -- +package a + +var _ = T{{ x }} //@hiloc(x, "x", text), diag("T", re"undefined"), diag("{ ", re"missing type") + +//@highlight(x, x) diff --git a/gopls/internal/test/marker/testdata/rename/bad.txt b/gopls/internal/test/marker/testdata/rename/bad.txt index c596ad13c92..882989cacef 100644 --- a/gopls/internal/test/marker/testdata/rename/bad.txt +++ b/gopls/internal/test/marker/testdata/rename/bad.txt @@ -11,9 +11,19 @@ package bad type myStruct struct { } -func (s *myStruct) sFunc() bool { //@renameerr("sFunc", "rFunc", re"not possible") +func (s *myStruct) sFunc() bool { //@renameerr("sFunc", "rFunc", "not possible because \"bad.go\" in \"golang.org/lsptests/bad\" has errors") return s.Bad //@diag("Bad", re"no field or method") } -- bad_test.go -- package bad + + +-- badsyntax/badsyntax.go -- +package badsyntax + +type S struct {} + +func (s *S) sFunc() bool { //@renameerr("sFunc", "rFunc", "not possible because \"badsyntax.go\" in \"golang.org/lsptests/bad/badsyntax\" has errors") + # //@diag("#", re"expected statement, found") +} diff --git a/gopls/internal/test/marker/testdata/signature/issue69552.txt b/gopls/internal/test/marker/testdata/signature/issue69552.txt new file mode 100644 index 00000000000..22ecda07341 --- /dev/null +++ b/gopls/internal/test/marker/testdata/signature/issue69552.txt @@ -0,0 +1,14 @@ +Regresson test for #69552: panic in activeParam of a builtin, when requesting +signature help outside of the argument list. + +-- go.mod -- +module example.com +go 1.18 + +-- a/a.go -- +package a + +func _() { + _ = len([]int{}) //@signature("en", "len(v Type) int", 0) +} + diff --git a/gopls/internal/test/marker/testdata/signature/signature.txt b/gopls/internal/test/marker/testdata/signature/signature.txt index 7bdd6341818..1da4eb5843e 100644 --- a/gopls/internal/test/marker/testdata/signature/signature.txt +++ b/gopls/internal/test/marker/testdata/signature/signature.txt @@ -16,6 +16,7 @@ import ( "bytes" "encoding/json" "math/big" + "fmt" ) func Foo(a string, b int) (c bool) { @@ -49,6 +50,9 @@ func Qux() { Foo("foo", 123) //@signature(",", "Foo(a string, b int) (c bool)", 0) Foo("foo", 123) //@signature(" 1", "Foo(a string, b int) (c bool)", 1) Foo("foo", 123) //@signature(")", "Foo(a string, b int) (c bool)", 1) + Foo("foo", 123) //@signature("o", "Foo(a string, b int) (c bool)", 0) + _ = Foo //@signature("o", "Foo(a string, b int) (c bool)", 0) + Foo //@signature("o", "Foo(a string, b int) (c bool)", 0) Bar(13.37, 0x13) //@signature("13.37", "Bar(float64, ...byte)", 0) Bar(13.37, 0x37) //@signature("0x37", "Bar(float64, ...byte)", 1) @@ -78,9 +82,28 @@ func Qux() { _ = make([]int, 1, 2) //@signature("2", "make(t Type, size ...int) Type", 1) - Foo(myFunc(123), 456) //@signature("myFunc", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("o(", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("(m", "Foo(a string, b int) (c bool)", 0) + Foo( myFunc(123), 456) //@signature(" m", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature(", ", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("456", "Foo(a string, b int) (c bool)", 1) + Foo(myFunc) //@signature(")", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("(1", "myFunc(foo int) string", 0) Foo(myFunc(123), 456) //@signature("123", "myFunc(foo int) string", 0) + fmt.Println //@signature("ln", "Println(a ...any) (n int, err error)", 0) + fmt.Println(myFunc) //@signature("ln", "Println(a ...any) (n int, err error)", 0) + fmt.Println(myFunc) //@signature("Func", "myFunc(foo int) string", 0) + + var hi string = "hello" + var wl string = " world: %s" + fmt.Println(fmt.Sprintf(wl, myFunc)) //@signature("Func", "myFunc(foo int) string", 0) + fmt.Println(fmt.Sprintf(wl, myFunc)) //@signature("wl", "Sprintf(format string, a ...any) string", 0) + fmt.Println(fmt.Sprintf(wl, myFunc)) //@signature(" m", "Sprintf(format string, a ...any) string", 1) + fmt.Println(hi, fmt.Sprintf(wl, myFunc)) //@signature("Sprint", "Sprintf(format string, a ...any) string", 0) + fmt.Println(hi, fmt.Sprintf(wl, myFunc)) //@signature(" fmt", "Println(a ...any) (n int, err error)", 0) + fmt.Println(hi, fmt.Sprintf(wl, myFunc)) //@signature("hi", "Println(a ...any) (n int, err error)", 0) + panic("oops!") //@signature(")", "panic(v any)", 0) println("hello", "world") //@signature(",", "println(args ...Type)", 0) @@ -100,6 +123,8 @@ package signature func _() { Foo(//@signature("//", "Foo(a string, b int) (c bool)", 0) + Foo.//@signature("//", "Foo(a string, b int) (c bool)", 0) + Foo.//@signature("oo", "Foo(a string, b int) (c bool)", 0) } -- signature/signature3.go -- diff --git a/gopls/internal/test/marker/testdata/suggestedfix/stub.txt b/gopls/internal/test/marker/testdata/suggestedfix/stub.txt index e31494ae461..fc10d8e58ad 100644 --- a/gopls/internal/test/marker/testdata/suggestedfix/stub.txt +++ b/gopls/internal/test/marker/testdata/suggestedfix/stub.txt @@ -347,9 +347,10 @@ type ( func _() { // Local types can't be stubbed as there's nowhere to put the methods. - // The suggestedfix assertion can't express this yet. TODO(adonovan): support it. + // Check that executing the code action causes an error, not file corruption. + // TODO(adonovan): it would be better not to offer the quick fix in this case. type local struct{} - var _ io.ReadCloser = local{} //@diag("local", re"does not implement") + var _ io.ReadCloser = local{} //@suggestedfixerr("local", re"does not implement", "local type \"local\" cannot be stubbed") } -- @typedecl_group/typedecl_group.go -- @@ -18 +18,10 @@ diff --git a/gopls/internal/util/astutil/purge_test.go b/gopls/internal/util/astutil/purge_test.go index c67f9039adc..757dd10a11b 100644 --- a/gopls/internal/util/astutil/purge_test.go +++ b/gopls/internal/util/astutil/purge_test.go @@ -50,7 +50,7 @@ func TestPurgeFuncBodies(t *testing.T) { fset := token.NewFileSet() // Parse then purge (reference implementation). - f1, _ := parser.ParseFile(fset, filename, content, 0) + f1, _ := parser.ParseFile(fset, filename, content, parser.SkipObjectResolution) ast.Inspect(f1, func(n ast.Node) bool { switch n := n.(type) { case *ast.FuncDecl: @@ -66,7 +66,7 @@ func TestPurgeFuncBodies(t *testing.T) { }) // Purge before parse (logic under test). - f2, _ := parser.ParseFile(fset, filename, astutil.PurgeFuncBodies(content), 0) + f2, _ := parser.ParseFile(fset, filename, astutil.PurgeFuncBodies(content), parser.SkipObjectResolution) // Compare sequence of node types. nodes1 := preorder(f1) diff --git a/gopls/internal/util/astutil/util.go b/gopls/internal/util/astutil/util.go index b9cf9a03c1d..ac7515d1daf 100644 --- a/gopls/internal/util/astutil/util.go +++ b/gopls/internal/util/astutil/util.go @@ -7,7 +7,6 @@ package astutil import ( "go/ast" "go/token" - "strings" "golang.org/x/tools/internal/typeparams" ) @@ -70,25 +69,3 @@ L: // unpack receiver type func NodeContains(n ast.Node, pos token.Pos) bool { return n.Pos() <= pos && pos <= n.End() } - -// IsGenerated check if a file is generated code -func IsGenerated(file *ast.File) bool { - // TODO: replace this implementation with calling function ast.IsGenerated when go1.21 is assured - for _, group := range file.Comments { - for _, comment := range group.List { - if comment.Pos() > file.Package { - break // after package declaration - } - // opt: check Contains first to avoid unnecessary array allocation in Split. - const prefix = "// Code generated " - if strings.Contains(comment.Text, prefix) { - for _, line := range strings.Split(comment.Text, "\n") { - if strings.HasPrefix(line, prefix) && strings.HasSuffix(line, " DO NOT EDIT.") { - return true - } - } - } - } - } - return false -} diff --git a/gopls/internal/util/frob/frob.go b/gopls/internal/util/frob/frob.go index cd385a9d692..a5fa584215f 100644 --- a/gopls/internal/util/frob/frob.go +++ b/gopls/internal/util/frob/frob.go @@ -327,14 +327,9 @@ func (fr *frob) decode(in *reader, addr reflect.Value) { kfrob, vfrob := fr.elems[0], fr.elems[1] k := reflect.New(kfrob.t).Elem() v := reflect.New(vfrob.t).Elem() - kzero := reflect.Zero(kfrob.t) - vzero := reflect.Zero(vfrob.t) for i := 0; i < len; i++ { - // TODO(adonovan): use SetZero from go1.20. - // k.SetZero() - // v.SetZero() - k.Set(kzero) - v.Set(vzero) + k.SetZero() + v.SetZero() kfrob.decode(in, k) vfrob.decode(in, v) m.SetMapIndex(k, v) diff --git a/gopls/internal/util/lru/lru_nil_test.go b/gopls/internal/util/lru/lru_nil_test.go index 08ce910989c..443d2a67818 100644 --- a/gopls/internal/util/lru/lru_nil_test.go +++ b/gopls/internal/util/lru/lru_nil_test.go @@ -4,11 +4,6 @@ package lru_test -// TODO(rfindley): uncomment once -lang is at least go1.20. -// Prior to that language version, interfaces did not satisfy comparable. -// Note that we can't simply use //go:build go1.20, because we need at least Go -// 1.21 in the go.mod file for file language versions support! -/* import ( "testing" @@ -22,4 +17,3 @@ func TestSetUntypedNil(t *testing.T) { t.Errorf("cache.Get(nil) = %v, %v, want nil, true", got, ok) } } -*/ diff --git a/gopls/internal/util/maps/maps.go b/gopls/internal/util/moremaps/maps.go similarity index 58% rename from gopls/internal/util/maps/maps.go rename to gopls/internal/util/moremaps/maps.go index daa9c3dafad..c8484d9fecd 100644 --- a/gopls/internal/util/maps/maps.go +++ b/gopls/internal/util/moremaps/maps.go @@ -2,7 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package maps +package moremaps + +import ( + "cmp" + "iter" + "maps" + "slices" +) // Group returns a new non-nil map containing the elements of s grouped by the // keys returned from the key func. @@ -15,8 +22,8 @@ func Group[K comparable, V any](s []V, key func(V) K) map[K][]V { return m } -// Keys returns the keys of the map M. -func Keys[M ~map[K]V, K comparable, V any](m M) []K { +// Keys returns the keys of the map M, like slices.Collect(maps.Keys(m)). +func KeySlice[M ~map[K]V, K comparable, V any](m M) []K { r := make([]K, 0, len(m)) for k := range m { r = append(r, k) @@ -24,8 +31,8 @@ func Keys[M ~map[K]V, K comparable, V any](m M) []K { return r } -// Values returns the values of the map M. -func Values[M ~map[K]V, K comparable, V any](m M) []V { +// Values returns the values of the map M, like slices.Collect(maps.Values(m)). +func ValueSlice[M ~map[K]V, K comparable, V any](m M) []V { r := make([]V, 0, len(m)) for _, v := range m { r = append(r, v) @@ -46,11 +53,14 @@ func SameKeys[K comparable, V1, V2 any](x map[K]V1, y map[K]V2) bool { return true } -// Clone returns a new map with the same entries as m. -func Clone[M ~map[K]V, K comparable, V any](m M) M { - copy := make(map[K]V, len(m)) - for k, v := range m { - copy[k] = v +// Sorted returns an iterator over the entries of m in key order. +func Sorted[M ~map[K]V, K cmp.Ordered, V any](m M) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + keys := slices.Sorted(maps.Keys(m)) + for _, k := range keys { + if !yield(k, m[k]) { + break + } + } } - return copy } diff --git a/gopls/internal/util/moreslices/slices.go b/gopls/internal/util/moreslices/slices.go new file mode 100644 index 00000000000..5905e360bfa --- /dev/null +++ b/gopls/internal/util/moreslices/slices.go @@ -0,0 +1,20 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package moreslices + +// Remove removes all values equal to elem from slice. +// +// The closest equivalent in the standard slices package is: +// +// DeleteFunc(func(x T) bool { return x == elem }) +func Remove[T comparable](slice []T, elem T) []T { + out := slice[:0] + for _, v := range slice { + if v != elem { + out = append(out, v) + } + } + return out +} diff --git a/gopls/internal/util/safetoken/safetoken_test.go b/gopls/internal/util/safetoken/safetoken_test.go index 4cdce7a97b9..ac3b878c6c4 100644 --- a/gopls/internal/util/safetoken/safetoken_test.go +++ b/gopls/internal/util/safetoken/safetoken_test.go @@ -23,11 +23,11 @@ func TestWorkaroundIssue57490(t *testing.T) { // syntax nodes, computed as Rbrace+len("}"), to be beyond EOF. src := `package p; func f() { var x struct` fset := token.NewFileSet() - file, _ := parser.ParseFile(fset, "a.go", src, 0) + file, _ := parser.ParseFile(fset, "a.go", src, parser.SkipObjectResolution) tf := fset.File(file.Pos()) // Add another file to the FileSet. - file2, _ := parser.ParseFile(fset, "b.go", "package q", 0) + file2, _ := parser.ParseFile(fset, "b.go", "package q", parser.SkipObjectResolution) // This is the ambiguity of #57490... if file.End() != file2.Pos() { diff --git a/gopls/internal/util/slices/slices.go b/gopls/internal/util/slices/slices.go deleted file mode 100644 index add52b7f6b1..00000000000 --- a/gopls/internal/util/slices/slices.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slices - -// Clone returns a copy of the slice. -// The elements are copied using assignment, so this is a shallow clone. -// TODO(rfindley): use go1.21 slices.Clone. -func Clone[S ~[]E, E any](s S) S { - // The s[:0:0] preserves nil in case it matters. - return append(s[:0:0], s...) -} - -// Contains reports whether x is present in slice. -// TODO(adonovan): use go1.21 slices.Contains. -func Contains[S ~[]E, E comparable](slice S, x E) bool { - for _, elem := range slice { - if elem == x { - return true - } - } - return false -} - -// IndexFunc returns the first index i satisfying f(s[i]), -// or -1 if none do. -// TODO(adonovan): use go1.21 slices.IndexFunc. -func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { - for i := range s { - if f(s[i]) { - return i - } - } - return -1 -} - -// ContainsFunc reports whether at least one -// element e of s satisfies f(e). -// TODO(adonovan): use go1.21 slices.ContainsFunc. -func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool { - return IndexFunc(s, f) >= 0 -} - -// Concat returns a new slice concatenating the passed in slices. -// TODO(rfindley): use go1.22 slices.Concat. -func Concat[S ~[]E, E any](slices ...S) S { - size := 0 - for _, s := range slices { - size += len(s) - if size < 0 { - panic("len out of range") - } - } - newslice := Grow[S](nil, size) - for _, s := range slices { - newslice = append(newslice, s...) - } - return newslice -} - -// Grow increases the slice's capacity, if necessary, to guarantee space for -// another n elements. After Grow(n), at least n elements can be appended -// to the slice without another allocation. If n is negative or too large to -// allocate the memory, Grow panics. -// TODO(rfindley): use go1.21 slices.Grow. -func Grow[S ~[]E, E any](s S, n int) S { - if n < 0 { - panic("cannot be negative") - } - if n -= cap(s) - len(s); n > 0 { - s = append(s[:cap(s)], make([]E, n)...)[:len(s)] - } - return s -} - -// DeleteFunc removes any elements from s for which del returns true, -// returning the modified slice. -// DeleteFunc zeroes the elements between the new length and the original length. -// TODO(adonovan): use go1.21 slices.DeleteFunc. -func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { - i := IndexFunc(s, del) - if i == -1 { - return s - } - // Don't start copying elements until we find one to delete. - for j := i + 1; j < len(s); j++ { - if v := s[j]; !del(v) { - s[i] = v - i++ - } - } - clear(s[i:]) // zero/nil out the obsolete elements, for GC - return s[:i] -} - -func clear[T any](slice []T) { - for i := range slice { - slice[i] = *new(T) - } -} - -// Remove removes all values equal to elem from slice. -// -// The closest equivalent in the standard slices package is: -// -// DeleteFunc(func(x T) bool { return x == elem }) -func Remove[T comparable](slice []T, elem T) []T { - out := slice[:0] - for _, v := range slice { - if v != elem { - out = append(out, v) - } - } - return out -} - -// Reverse reverses the elements of the slice in place. -// TODO(adonovan): use go1.21 slices.Reverse. -func Reverse[S ~[]E, E any](s S) { - for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { - s[i], s[j] = s[j], s[i] - } -} diff --git a/gopls/internal/util/typesutil/typesutil.go b/gopls/internal/util/typesutil/typesutil.go index 3597b4b4bbc..6e61c7ed874 100644 --- a/gopls/internal/util/typesutil/typesutil.go +++ b/gopls/internal/util/typesutil/typesutil.go @@ -9,19 +9,6 @@ import ( "go/types" ) -// ImportedPkgName returns the PkgName object declared by an ImportSpec. -// TODO(adonovan): use go1.22's Info.PkgNameOf. -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 -} - // FileQualifier returns a [types.Qualifier] function that qualifies // imported symbols appropriately based on the import environment of a // given file. @@ -29,7 +16,7 @@ func FileQualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qual // Construct mapping of import paths to their defined or implicit names. imports := make(map[*types.Package]string) for _, imp := range f.Imports { - if pkgname, ok := ImportedPkgName(info, imp); ok { + if pkgname := info.PkgNameOf(imp); pkgname != nil { imports[pkgname.Imported()] = pkgname.Name() } } diff --git a/internal/aliases/aliases.go b/internal/aliases/aliases.go index f7798e3354e..b9425f5a209 100644 --- a/internal/aliases/aliases.go +++ b/internal/aliases/aliases.go @@ -28,7 +28,7 @@ import ( func NewAlias(enabled bool, pos token.Pos, pkg *types.Package, name string, rhs types.Type, tparams []*types.TypeParam) *types.TypeName { if enabled { tname := types.NewTypeName(pos, pkg, name, nil) - newAlias(tname, rhs, tparams) + SetTypeParams(types.NewAlias(tname, rhs), tparams) return tname } if len(tparams) > 0 { diff --git a/internal/aliases/aliases_go121.go b/internal/aliases/aliases_go121.go deleted file mode 100644 index a775fcc4bed..00000000000 --- a/internal/aliases/aliases_go121.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.22 -// +build !go1.22 - -package aliases - -import ( - "go/types" -) - -// Alias is a placeholder for a go/types.Alias for <=1.21. -// It will never be created by go/types. -type Alias struct{} - -func (*Alias) String() string { panic("unreachable") } -func (*Alias) Underlying() types.Type { panic("unreachable") } -func (*Alias) Obj() *types.TypeName { panic("unreachable") } -func Rhs(alias *Alias) types.Type { panic("unreachable") } -func TypeParams(alias *Alias) *types.TypeParamList { panic("unreachable") } -func SetTypeParams(alias *Alias, tparams []*types.TypeParam) { panic("unreachable") } -func TypeArgs(alias *Alias) *types.TypeList { panic("unreachable") } -func Origin(alias *Alias) *Alias { panic("unreachable") } - -// Unalias returns the type t for go <=1.21. -func Unalias(t types.Type) types.Type { return t } - -func newAlias(name *types.TypeName, rhs types.Type, tparams []*types.TypeParam) *Alias { - panic("unreachable") -} - -// Enabled reports whether [NewAlias] should create [types.Alias] types. -// -// Before go1.22, this function always returns false. -func Enabled() bool { return false } diff --git a/internal/aliases/aliases_go122.go b/internal/aliases/aliases_go122.go index 31c159e42e6..7716a3331db 100644 --- a/internal/aliases/aliases_go122.go +++ b/internal/aliases/aliases_go122.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.22 -// +build go1.22 - package aliases import ( @@ -14,22 +11,19 @@ import ( "go/types" ) -// Alias is an alias of types.Alias. -type Alias = types.Alias - // Rhs returns the type on the right-hand side of the alias declaration. -func Rhs(alias *Alias) types.Type { +func Rhs(alias *types.Alias) types.Type { if alias, ok := any(alias).(interface{ Rhs() types.Type }); ok { return alias.Rhs() // go1.23+ } // go1.22's Alias didn't have the Rhs method, // so Unalias is the best we can do. - return Unalias(alias) + return types.Unalias(alias) } // TypeParams returns the type parameter list of the alias. -func TypeParams(alias *Alias) *types.TypeParamList { +func TypeParams(alias *types.Alias) *types.TypeParamList { if alias, ok := any(alias).(interface{ TypeParams() *types.TypeParamList }); ok { return alias.TypeParams() // go1.23+ } @@ -37,7 +31,7 @@ func TypeParams(alias *Alias) *types.TypeParamList { } // SetTypeParams sets the type parameters of the alias type. -func SetTypeParams(alias *Alias, tparams []*types.TypeParam) { +func SetTypeParams(alias *types.Alias, tparams []*types.TypeParam) { if alias, ok := any(alias).(interface { SetTypeParams(tparams []*types.TypeParam) }); ok { @@ -48,7 +42,7 @@ func SetTypeParams(alias *Alias, tparams []*types.TypeParam) { } // TypeArgs returns the type arguments used to instantiate the Alias type. -func TypeArgs(alias *Alias) *types.TypeList { +func TypeArgs(alias *types.Alias) *types.TypeList { if alias, ok := any(alias).(interface{ TypeArgs() *types.TypeList }); ok { return alias.TypeArgs() // go1.23+ } @@ -57,25 +51,13 @@ func TypeArgs(alias *Alias) *types.TypeList { // Origin returns the generic Alias type of which alias is an instance. // If alias is not an instance of a generic alias, Origin returns alias. -func Origin(alias *Alias) *Alias { +func Origin(alias *types.Alias) *types.Alias { if alias, ok := any(alias).(interface{ Origin() *types.Alias }); ok { return alias.Origin() // go1.23+ } return alias // not an instance of a generic alias (go1.22) } -// Unalias is a wrapper of types.Unalias. -func Unalias(t types.Type) types.Type { return types.Unalias(t) } - -// newAlias is an internal alias around types.NewAlias. -// Direct usage is discouraged as the moment. -// Try to use NewAlias instead. -func newAlias(tname *types.TypeName, rhs types.Type, tparams []*types.TypeParam) *Alias { - a := types.NewAlias(tname, rhs) - SetTypeParams(a, tparams) - return a -} - // Enabled reports whether [NewAlias] should create [types.Alias] types. // // This function is expensive! Call it sparingly. @@ -91,7 +73,7 @@ func Enabled() bool { // many tests. Therefore any attempt to cache the result // is just incorrect. fset := token.NewFileSet() - f, _ := parser.ParseFile(fset, "a.go", "package p; type A = int", 0) + f, _ := parser.ParseFile(fset, "a.go", "package p; type A = int", parser.SkipObjectResolution) pkg, _ := new(types.Config).Check("p", fset, []*ast.File{f}, nil) _, enabled := pkg.Scope().Lookup("A").Type().(*types.Alias) return enabled diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go index d19afcc56c9..551e9e512f1 100644 --- a/internal/aliases/aliases_test.go +++ b/internal/aliases/aliases_test.go @@ -15,13 +15,10 @@ import ( "golang.org/x/tools/internal/testenv" ) -// Assert that Obj exists on Alias. -var _ func(*aliases.Alias) *types.TypeName = (*aliases.Alias).Obj - // TestNewAlias tests that alias.NewAlias creates an alias of a type // whose underlying and Unaliased type is *Named. // When gotypesalias=1 (or unset) and GoVersion >= 1.22, the type will -// be an *aliases.Alias. +// be an *types.Alias. func TestNewAlias(t *testing.T) { const source = ` package p @@ -48,8 +45,8 @@ func TestNewAlias(t *testing.T) { for _, godebug := range []string{ // The default gotypesalias value follows the x/tools/go.mod version - // The go.mod is at 1.19 so the default is gotypesalias=0. - // "", // Use the default GODEBUG value. + // The go.mod is at 1.22 so the default is gotypesalias=0. + "", // Use the default GODEBUG value (off). "gotypesalias=0", "gotypesalias=1", } { @@ -66,14 +63,18 @@ func TestNewAlias(t *testing.T) { if got, want := A.Type().Underlying(), tv.Type; got != want { t.Errorf("Expected A.Type().Underlying()==%q. got %q", want, got) } - if got, want := aliases.Unalias(A.Type()), tv.Type; got != want { + if got, want := types.Unalias(A.Type()), tv.Type; got != want { t.Errorf("Expected Unalias(A)==%q. got %q", want, got) } - if testenv.Go1Point() >= 22 && godebug != "gotypesalias=0" { - if _, ok := A.Type().(*aliases.Alias); !ok { - t.Errorf("Expected A.Type() to be a types.Alias(). got %q", A.Type()) + wantAlias := godebug == "gotypesalias=1" + _, gotAlias := A.Type().(*types.Alias) + if gotAlias != wantAlias { + verb := "to be" + if !wantAlias { + verb = "to not be" } + t.Errorf("Expected A.Type() %s a types.Alias(). got %q", verb, A.Type()) } }) } @@ -86,9 +87,12 @@ func TestNewAlias(t *testing.T) { // // Requires gotypesalias GODEBUG and aliastypeparams GOEXPERIMENT. func TestNewParameterizedAlias(t *testing.T) { - testenv.NeedsGoExperiment(t, "aliastypeparams") + testenv.NeedsGo1Point(t, 23) + if testenv.Go1Point() == 23 { + testenv.NeedsGoExperiment(t, "aliastypeparams") + } - t.Setenv("GODEBUG", "gotypesalias=1") // needed until gotypesalias is removed (1.27). + t.Setenv("GODEBUG", "gotypesalias=1") // needed until gotypesalias is removed (1.27) or enabled by go.mod (1.23). enabled := aliases.Enabled() if !enabled { t.Fatal("Need materialized aliases enabled") @@ -125,11 +129,11 @@ func TestNewParameterizedAlias(t *testing.T) { if got, want := A.Type().Underlying(), ptrT; !types.Identical(got, want) { t.Errorf("A.Type().Underlying (%q) is not identical to %q", got, want) } - if got, want := aliases.Unalias(A.Type()), ptrT; !types.Identical(got, want) { + if got, want := types.Unalias(A.Type()), ptrT; !types.Identical(got, want) { t.Errorf("Unalias(A)==%q is not identical to %q", got, want) } - if _, ok := A.Type().(*aliases.Alias); !ok { + if _, ok := A.Type().(*types.Alias); !ok { t.Errorf("Expected A.Type() to be a types.Alias(). got %q", A.Type()) } @@ -150,7 +154,7 @@ func TestNewParameterizedAlias(t *testing.T) { if got, want := tv.Type.Underlying(), ptrNamed; !types.Identical(got, want) { t.Errorf("A[Named].Type().Underlying (%q) is not identical to %q", got, want) } - if got, want := aliases.Unalias(tv.Type), ptrNamed; !types.Identical(got, want) { + if got, want := types.Unalias(tv.Type), ptrNamed; !types.Identical(got, want) { t.Errorf("Unalias(A[Named])==%q is not identical to %q", got, want) } } diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index e0b13e70a01..24755b41265 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -17,7 +17,6 @@ import ( "strconv" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/internal/aliases" ) func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos { @@ -34,7 +33,7 @@ func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos func ZeroValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { // TODO(adonovan): think about generics, and also generic aliases. - under := aliases.Unalias(typ) + under := types.Unalias(typ) // Don't call Underlying unconditionally: although it removes // Named and Alias, it also removes TypeParam. if n, ok := under.(*types.Named); ok { @@ -416,8 +415,7 @@ func CheckReadable(pass *analysis.Pass, filename string) error { return nil } for _, f := range pass.Files { - // TODO(adonovan): use go1.20 f.FileStart - if pass.Fset.File(f.Pos()).Name() == filename { + if pass.Fset.File(f.FileStart).Name() == filename { return nil } } diff --git a/internal/apidiff/apidiff.go b/internal/apidiff/apidiff.go index f36f25cce8b..a37d5daca38 100644 --- a/internal/apidiff/apidiff.go +++ b/internal/apidiff/apidiff.go @@ -19,8 +19,6 @@ import ( "go/constant" "go/token" "go/types" - - "golang.org/x/tools/internal/aliases" ) // Changes reports on the differences between the APIs of the old and new packages. @@ -208,7 +206,7 @@ func (d *differ) typeChanged(obj types.Object, part string, old, new types.Type) // Since these can change without affecting compatibility, we don't want users to // be distracted by them, so we remove them. func removeNamesFromSignature(t types.Type) types.Type { - t = aliases.Unalias(t) + t = types.Unalias(t) sig, ok := t.(*types.Signature) if !ok { return t @@ -218,7 +216,7 @@ func removeNamesFromSignature(t types.Type) types.Type { var vars []*types.Var for i := 0; i < p.Len(); i++ { v := p.At(i) - vars = append(vars, types.NewVar(v.Pos(), v.Pkg(), "", aliases.Unalias(v.Type()))) + vars = append(vars, types.NewVar(v.Pos(), v.Pkg(), "", types.Unalias(v.Type()))) } return types.NewTuple(vars...) } diff --git a/internal/apidiff/compatibility.go b/internal/apidiff/compatibility.go index 64cad5337be..f8e59d611bd 100644 --- a/internal/apidiff/compatibility.go +++ b/internal/apidiff/compatibility.go @@ -9,13 +9,12 @@ import ( "go/types" "reflect" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) func (d *differ) checkCompatible(otn *types.TypeName, old, new types.Type) { - old = aliases.Unalias(old) - new = aliases.Unalias(new) + old = types.Unalias(old) + new = types.Unalias(new) switch old := old.(type) { case *types.Interface: if new, ok := new.(*types.Interface); ok { @@ -292,7 +291,7 @@ func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addc oldMethodSet := exportedMethods(oldt) newMethodSet := exportedMethods(newt) msname := otn.Name() - if _, ok := aliases.Unalias(oldt).(*types.Pointer); ok { + if _, ok := types.Unalias(oldt).(*types.Pointer); ok { msname = "*" + msname } for name, oldMethod := range oldMethodSet { diff --git a/internal/apidiff/correspondence.go b/internal/apidiff/correspondence.go index dd2f5178173..a626e066430 100644 --- a/internal/apidiff/correspondence.go +++ b/internal/apidiff/correspondence.go @@ -7,8 +7,6 @@ package apidiff import ( "go/types" "sort" - - "golang.org/x/tools/internal/aliases" ) // Two types are correspond if they are identical except for defined types, @@ -33,8 +31,8 @@ func (d *differ) correspond(old, new types.Type) bool { // Compare this to the implementation of go/types.Identical. func (d *differ) corr(old, new types.Type, p *ifacePair) bool { // Structure copied from types.Identical. - old = aliases.Unalias(old) - new = aliases.Unalias(new) + old = types.Unalias(old) + new = types.Unalias(new) switch old := old.(type) { case *types.Basic: return types.Identical(old, new) diff --git a/internal/facts/facts_test.go b/internal/facts/facts_test.go index c1e67926eff..bb7d36a07ad 100644 --- a/internal/facts/facts_test.go +++ b/internal/facts/facts_test.go @@ -18,7 +18,6 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/facts" "golang.org/x/tools/internal/testenv" ) @@ -361,7 +360,7 @@ func find(p *types.Package, expr string) types.Object { if err != nil { return nil } - if n, ok := aliases.Unalias(tv.Type).(*types.Named); ok { + if n, ok := types.Unalias(tv.Type).(*types.Named); ok { return n.Obj() } return nil diff --git a/internal/facts/imports.go b/internal/facts/imports.go index 9f706cd954f..c36f2a5af0c 100644 --- a/internal/facts/imports.go +++ b/internal/facts/imports.go @@ -6,8 +6,6 @@ package facts import ( "go/types" - - "golang.org/x/tools/internal/aliases" ) // importMap computes the import map for a package by traversing the @@ -47,8 +45,8 @@ func importMap(imports []*types.Package) map[string]*types.Package { addType = func(T types.Type) { switch T := T.(type) { - case *aliases.Alias: - addType(aliases.Unalias(T)) + case *types.Alias: + addType(types.Unalias(T)) case *types.Basic: // nop case *types.Named: diff --git a/internal/gcimporter/bexport_test.go b/internal/gcimporter/bexport_test.go index 1a2c8e8dd0a..fb18a5584b3 100644 --- a/internal/gcimporter/bexport_test.go +++ b/internal/gcimporter/bexport_test.go @@ -18,7 +18,6 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/gcimporter" ) @@ -31,8 +30,8 @@ func fileLine(fset *token.FileSet, obj types.Object) string { } func equalType(x, y types.Type) error { - x = aliases.Unalias(x) - y = aliases.Unalias(y) + x = types.Unalias(x) + y = types.Unalias(y) if reflect.TypeOf(x) != reflect.TypeOf(y) { return fmt.Errorf("unequal kinds: %T vs %T", x, y) } diff --git a/internal/gcimporter/bimport.go b/internal/gcimporter/bimport.go index d98b0db2a9a..d79a605ed13 100644 --- a/internal/gcimporter/bimport.go +++ b/internal/gcimporter/bimport.go @@ -87,64 +87,3 @@ func chanDir(d int) types.ChanDir { return 0 } } - -var predeclOnce sync.Once -var predecl []types.Type // initialized lazily - -func predeclared() []types.Type { - predeclOnce.Do(func() { - // initialize lazily to be sure that all - // elements have been initialized before - predecl = []types.Type{ // basic types - types.Typ[types.Bool], - types.Typ[types.Int], - types.Typ[types.Int8], - types.Typ[types.Int16], - types.Typ[types.Int32], - types.Typ[types.Int64], - types.Typ[types.Uint], - types.Typ[types.Uint8], - types.Typ[types.Uint16], - types.Typ[types.Uint32], - types.Typ[types.Uint64], - types.Typ[types.Uintptr], - types.Typ[types.Float32], - types.Typ[types.Float64], - types.Typ[types.Complex64], - types.Typ[types.Complex128], - types.Typ[types.String], - - // basic type aliases - types.Universe.Lookup("byte").Type(), - types.Universe.Lookup("rune").Type(), - - // error - types.Universe.Lookup("error").Type(), - - // untyped types - types.Typ[types.UntypedBool], - types.Typ[types.UntypedInt], - types.Typ[types.UntypedRune], - types.Typ[types.UntypedFloat], - types.Typ[types.UntypedComplex], - types.Typ[types.UntypedString], - types.Typ[types.UntypedNil], - - // package unsafe - types.Typ[types.UnsafePointer], - - // invalid type - types.Typ[types.Invalid], // only appears in packages with errors - - // used internally by gc; never used by this package or in .a files - anyType{}, - } - predecl = append(predecl, additionalPredeclared()...) - }) - return predecl -} - -type anyType struct{} - -func (t anyType) Underlying() types.Type { return t } -func (t anyType) String() string { return "any" } diff --git a/internal/gcimporter/gcimporter.go b/internal/gcimporter/gcimporter.go index 39df91124a4..e6c5d51f8e5 100644 --- a/internal/gcimporter/gcimporter.go +++ b/internal/gcimporter/gcimporter.go @@ -232,14 +232,19 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func // Select appropriate importer. if len(data) > 0 { switch data[0] { - case 'v', 'c', 'd': // binary, till go1.10 + case 'v', 'c', 'd': + // binary: emitted by cmd/compile till go1.10; obsolete. return nil, fmt.Errorf("binary (%c) import format is no longer supported", data[0]) - case 'i': // indexed, till go1.19 + case 'i': + // indexed: emitted by cmd/compile till go1.19; + // now used only for serializing go/types. + // See https://github.com/golang/go/issues/69491. _, pkg, err := IImportData(fset, packages, data[1:], id) return pkg, err - case 'u': // unified, from go1.20 + case 'u': + // unified: emitted by cmd/compile since go1.20. _, pkg, err := UImportData(fset, packages, data[1:size], id) return pkg, err diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 1a56af40323..5519fa08a92 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -5,7 +5,7 @@ // This file is a copy of $GOROOT/src/go/internal/gcimporter/gcimporter_test.go, // adjusted to make it build with code from (std lib) internal/testenv copied. -package gcimporter +package gcimporter_test import ( "bytes" @@ -27,7 +27,7 @@ import ( "testing" "time" - "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/gcimporter" "golang.org/x/tools/internal/goroot" "golang.org/x/tools/internal/testenv" ) @@ -93,7 +93,7 @@ func compilePkg(t *testing.T, dirname, filename, outdirname string, packagefiles func testPath(t *testing.T, path, srcDir string) *types.Package { t0 := time.Now() - pkg, err := Import(make(map[string]*types.Package), path, srcDir, nil) + pkg, err := gcimporter.Import(make(map[string]*types.Package), path, srcDir, nil) if err != nil { t.Errorf("testPath(%s): %s", path, err) return nil @@ -120,12 +120,16 @@ func TestImportTestdata(t *testing.T) { needsCompiler(t, "gc") testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache + testAliases(t, testImportTestdata) +} + +func testImportTestdata(t *testing.T) { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) packageFiles := map[string]string{} for _, pkg := range []string{"go/ast", "go/token"} { - export, _ := FindPkg(pkg, "testdata") + export, _ := gcimporter.FindPkg(pkg, "testdata") if export == "" { t.Fatalf("no export data found for %s", pkg) } @@ -147,10 +151,7 @@ func TestImportTestdata(t *testing.T) { // For now, we just test the presence of a few packages // that we know are there for sure. got := fmt.Sprint(pkg.Imports()) - wants := []string{"go/ast", "go/token"} - if unifiedIR { - wants = []string{"go/ast"} - } + wants := []string{"go/ast", "go/token", "go/ast"} for _, want := range wants { if !strings.Contains(got, want) { t.Errorf(`Package("exports").Imports() = %s, does not contain %s`, got, want) @@ -171,6 +172,23 @@ func TestImportTypeparamTests(t *testing.T) { t.Skipf("gc-built packages not available (compiler = %s)", runtime.Compiler) } + testAliases(t, func(t *testing.T) { + var skip map[string]string + + // Add tests to skip. + if testenv.Go1Point() == 22 && os.Getenv("GODEBUG") == aliasesOn { + // The tests below can be skipped in 1.22 as gotypesalias=1 was experimental. + // These do not need to be addressed. + skip = map[string]string{ + "struct.go": "1.22 differences in formatting a *types.Alias", + "issue50259.go": "1.22 cannot compile due to an understood types.Alias bug", + } + } + testImportTypeparamTests(t, skip) + }) +} + +func testImportTypeparamTests(t *testing.T, skip map[string]string) { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) @@ -182,17 +200,6 @@ func TestImportTypeparamTests(t *testing.T) { t.Fatal(err) } - var skip map[string]string - if !unifiedIR { - // The Go 1.18 frontend still fails several cases. - skip = map[string]string{ - "equal.go": "inconsistent embedded sorting", // TODO(rfindley): investigate this. - "nested.go": "fails to compile", // TODO(rfindley): investigate this. - "issue47631.go": "can not handle local type declarations", - "issue55101.go": "fails to compile", - } - } - for _, entry := range list { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { // For now, only consider standalone go files. @@ -200,8 +207,8 @@ func TestImportTypeparamTests(t *testing.T) { } t.Run(entry.Name(), func(t *testing.T) { - if reason, ok := skip[entry.Name()]; ok { - t.Skip(reason) + if reason := skip[entry.Name()]; reason != "" { + t.Skipf("Skipping due to %s", reason) } filename := filepath.Join(rootDir, entry.Name()) @@ -275,10 +282,6 @@ func checkFile(t *testing.T, filename string, src []byte) *types.Package { } func TestVersionHandling(t *testing.T) { - if debug { - t.Skip("TestVersionHandling panics in debug mode") - } - // This package only handles gc export data. needsCompiler(t, "gc") @@ -310,7 +313,7 @@ func TestVersionHandling(t *testing.T) { } // test that export data can be imported - _, err := Import(make(map[string]*types.Package), pkgpath, dir, nil) + _, err := gcimporter.Import(make(map[string]*types.Package), pkgpath, dir, nil) if err != nil { t.Errorf("import %q failed: %v", pkgpath, err) continue @@ -338,7 +341,7 @@ func TestVersionHandling(t *testing.T) { 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) + _, err = gcimporter.Import(make(map[string]*types.Package), pkgpath, corruptdir, nil) if err == nil { t.Errorf("import corrupted %q succeeded", pkgpath) } else if msg := err.Error(); !strings.Contains(msg, "internal error") { @@ -355,6 +358,10 @@ func TestImportStdLib(t *testing.T) { needsCompiler(t, "gc") testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache + testAliases(t, testImportStdLib) +} + +func testImportStdLib(t *testing.T) { // Get list of packages in stdlib. Filter out test-only packages with {{if .GoFiles}} check. var stderr bytes.Buffer cmd := exec.Command("go", "list", "-f", "{{if .GoFiles}}{{.ImportPath}}{{end}}", "std") @@ -425,7 +432,7 @@ func TestImportedTypes(t *testing.T) { t.Errorf("%s: got %q; want %q", test.name, got, test.want) } - if named, _ := aliases.Unalias(obj.Type()).(*types.Named); named != nil { + if named, _ := types.Unalias(obj.Type()).(*types.Named); named != nil { verifyInterfaceMethodRecvs(t, named, 0) } } @@ -464,7 +471,7 @@ func importObject(t *testing.T, name string) types.Object { importPath := s[0] objName := s[1] - pkg, err := Import(make(map[string]*types.Package), importPath, ".", nil) + pkg, err := gcimporter.Import(make(map[string]*types.Package), importPath, ".", nil) if err != nil { t.Error(err) return nil @@ -508,7 +515,7 @@ func verifyInterfaceMethodRecvs(t *testing.T, named *types.Named, level int) { // check embedded interfaces (if they are named, too) for i := 0; i < iface.NumEmbeddeds(); i++ { // embedding of interfaces cannot have cycles; recursion will terminate - if etype, _ := aliases.Unalias(iface.EmbeddedType(i)).(*types.Named); etype != nil { + if etype, _ := types.Unalias(iface.EmbeddedType(i)).(*types.Named); etype != nil { verifyInterfaceMethodRecvs(t, etype, level+1) } } @@ -528,7 +535,7 @@ func TestIssue5815(t *testing.T) { t.Errorf("no pkg for %s", obj) } if tname, _ := obj.(*types.TypeName); tname != nil { - named := aliases.Unalias(tname.Type()).(*types.Named) + named := types.Unalias(tname.Type()).(*types.Named) for i := 0; i < named.NumMethods(); i++ { m := named.Method(i) if m.Pkg() == nil { @@ -546,7 +553,7 @@ func TestCorrectMethodPackage(t *testing.T) { testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache imports := make(map[string]*types.Package) - _, err := Import(imports, "net/http", ".", nil) + _, err := gcimporter.Import(imports, "net/http", ".", nil) if err != nil { t.Fatal(err) } @@ -564,12 +571,7 @@ func TestIssue13566(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache - - // On windows, we have to set the -D option for the compiler to avoid having a drive - // letter and an illegal ':' in the import path - just skip it (see also issue #3483). - if runtime.GOOS == "windows" { - t.Skip("avoid dealing with relative paths/drive letters on windows") - } + skipWindows(t) tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) @@ -583,7 +585,7 @@ func TestIssue13566(t *testing.T) { t.Fatal(err) } - jsonExport, _ := FindPkg("encoding/json", "testdata") + jsonExport, _ := gcimporter.FindPkg("encoding/json", "testdata") if jsonExport == "" { t.Fatalf("no export data found for encoding/json") } @@ -609,7 +611,7 @@ func TestIssue13898(t *testing.T) { // import go/internal/gcimporter which imports go/types partially imports := make(map[string]*types.Package) - _, err := Import(imports, "go/internal/gcimporter", ".", nil) + _, err := gcimporter.Import(imports, "go/internal/gcimporter", ".", nil) if err != nil { t.Fatal(err) } @@ -628,7 +630,7 @@ func TestIssue13898(t *testing.T) { // look for go/types.Object type obj := lookupObj(t, goTypesPkg.Scope(), "Object") - typ, ok := aliases.Unalias(obj.Type()).(*types.Named) + typ, ok := types.Unalias(obj.Type()).(*types.Named) if !ok { t.Fatalf("go/types.Object type is %v; wanted named type", typ) } @@ -648,12 +650,7 @@ func TestIssue13898(t *testing.T) { func TestIssue15517(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") - - // On windows, we have to set the -D option for the compiler to avoid having a drive - // letter and an illegal ':' in the import path - just skip it (see also issue #3483). - if runtime.GOOS == "windows" { - t.Skip("avoid dealing with relative paths/drive letters on windows") - } + skipWindows(t) tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) @@ -674,7 +671,7 @@ func TestIssue15517(t *testing.T) { // The same issue occurs with vendoring.) imports := make(map[string]*types.Package) for i := 0; i < 3; i++ { - if _, err := Import(imports, "./././testdata/p", tmpdir, nil); err != nil { + if _, err := gcimporter.Import(imports, "./././testdata/p", tmpdir, nil); err != nil { t.Fatal(err) } } @@ -683,12 +680,7 @@ func TestIssue15517(t *testing.T) { func TestIssue15920(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") - - // On windows, we have to set the -D option for the compiler to avoid having a drive - // letter and an illegal ':' in the import path - just skip it (see also issue #3483). - if runtime.GOOS == "windows" { - t.Skip("avoid dealing with relative paths/drive letters on windows") - } + skipWindows(t) compileAndImportPkg(t, "issue15920") } @@ -696,12 +688,7 @@ func TestIssue15920(t *testing.T) { func TestIssue20046(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") - - // On windows, we have to set the -D option for the compiler to avoid having a drive - // letter and an illegal ':' in the import path - just skip it (see also issue #3483). - if runtime.GOOS == "windows" { - t.Skip("avoid dealing with relative paths/drive letters on windows") - } + skipWindows(t) // "./issue20046".V.M must exist pkg := compileAndImportPkg(t, "issue20046") @@ -714,12 +701,7 @@ func TestIssue20046(t *testing.T) { func TestIssue25301(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") - - // On windows, we have to set the -D option for the compiler to avoid having a drive - // letter and an illegal ':' in the import path - just skip it (see also issue #3483). - if runtime.GOOS == "windows" { - t.Skip("avoid dealing with relative paths/drive letters on windows") - } + skipWindows(t) compileAndImportPkg(t, "issue25301") } @@ -727,12 +709,7 @@ func TestIssue25301(t *testing.T) { func TestIssue51836(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") - - // On windows, we have to set the -D option for the compiler to avoid having a drive - // letter and an illegal ':' in the import path - just skip it (see also issue #3483). - if runtime.GOOS == "windows" { - t.Skip("avoid dealing with relative paths/drive letters on windows") - } + skipWindows(t) tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) @@ -782,14 +759,14 @@ type K = StillBad[string] } // Export it. (Shallowness isn't important here.) - data, err := IExportShallow(fset, pkg1, nil) + data, err := gcimporter.IExportShallow(fset, pkg1, nil) if err != nil { t.Fatalf("export: %v", err) // any failure to export is a bug } // Re-import it. imports := make(map[string]*types.Package) - pkg2, err := IImportShallow(fset, GetPackagesFromMap(imports), data, "p", nil) + pkg2, err := gcimporter.IImportShallow(fset, gcimporter.GetPackagesFromMap(imports), data, "p", nil) if err != nil { t.Fatalf("import: %v", err) // any failure of IExport+IImport is a bug. } @@ -820,12 +797,7 @@ type K = StillBad[string] func TestIssue57015(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") - - // On windows, we have to set the -D option for the compiler to avoid having a drive - // letter and an illegal ':' in the import path - just skip it (see also issue #3483). - if runtime.GOOS == "windows" { - t.Skip("avoid dealing with relative paths/drive letters on windows") - } + skipWindows(t) compileAndImportPkg(t, "issue57015") } @@ -879,14 +851,14 @@ func TestExportInvalid(t *testing.T) { // Export it. // (Shallowness isn't important here.) - data, err := IExportShallow(fset, pkg1, nil) + data, err := gcimporter.IExportShallow(fset, pkg1, nil) if err != nil { t.Fatalf("export: %v", err) // any failure to export is a bug } // Re-import it. imports := make(map[string]*types.Package) - pkg2, err := IImportShallow(fset, GetPackagesFromMap(imports), data, "p", nil) + pkg2, err := gcimporter.IImportShallow(fset, gcimporter.GetPackagesFromMap(imports), data, "p", nil) if err != nil { t.Fatalf("import: %v", err) // any failure of IExport+IImport is a bug. } @@ -912,12 +884,7 @@ func TestIssue58296(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache - - // On windows, we have to set the -D option for the compiler to avoid having a drive - // letter and an illegal ':' in the import path - just skip it (see also issue #3483). - if runtime.GOOS == "windows" { - t.Skip("avoid dealing with relative paths/drive letters on windows") - } + skipWindows(t) tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) @@ -940,7 +907,7 @@ func TestIssue58296(t *testing.T) { } // make sure a and b are both imported by c. - pkg, err := Import(imports, "./c", testoutdir, nil) + pkg, err := gcimporter.Import(imports, "./c", testoutdir, nil) if err != nil { t.Fatal(err) } @@ -961,9 +928,9 @@ func TestIssueAliases(t *testing.T) { testenv.NeedsGo1Point(t, 24) needsCompiler(t, "gc") testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache - testenv.NeedsGoExperiment(t, "aliastypeparams") + skipWindows(t) - t.Setenv("GODEBUG", fmt.Sprintf("gotypesalias=%d", 1)) + t.Setenv("GODEBUG", aliasesOn) tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) @@ -983,7 +950,7 @@ func TestIssueAliases(t *testing.T) { ) // import c from gc export data using a and b. - pkg, err := Import(map[string]*types.Package{ + pkg, err := gcimporter.Import(map[string]*types.Package{ apkg: types.NewPackage(apkg, "a"), bpkg: types.NewPackage(bpkg, "b"), }, "./c", testoutdir, nil) @@ -1010,7 +977,7 @@ func TestIssueAliases(t *testing.T) { "X : testdata/b.B[int]", "Y : testdata/c.c[string]", "Z : testdata/c.c[int]", - "c : testdata/c.c", + "c : testdata/c.c[V any]", }, ",") if got := strings.Join(objs, ","); got != want { t.Errorf("got imports %v for package c. wanted %v", objs, want) @@ -1027,7 +994,7 @@ func apkg(testoutdir string) string { } func importPkg(t *testing.T, path, srcDir string) *types.Package { - pkg, err := Import(make(map[string]*types.Package), path, srcDir, nil) + pkg, err := gcimporter.Import(make(map[string]*types.Package), path, srcDir, nil) if err != nil { t.Fatal(err) } @@ -1048,3 +1015,31 @@ func lookupObj(t *testing.T, scope *types.Scope, name string) types.Object { t.Fatalf("%s not found", name) return nil } + +// skipWindows skips the test on windows. +// +// On windows, we have to set the -D option for the compiler to avoid having a drive +// letter and an illegal ':' in the import path - just skip it (see also issue #3483). +func skipWindows(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("avoid dealing with relative paths/drive letters on windows") + } +} + +const ( + aliasesOff = "gotypesalias=0" // default GODEBUG in 1.22 (like x/tools) + aliasesOn = "gotypesalias=1" // default after 1.23 +) + +// testAliases runs f within subtests with the GODEBUG gotypesalias enables and disabled. +func testAliases(t *testing.T, f func(*testing.T)) { + for _, dbg := range []string{ + aliasesOff, + aliasesOn, + } { + t.Run(dbg, func(t *testing.T) { + t.Setenv("GODEBUG", dbg) + f(t) + }) + } +} diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 5f283281a25..1e19fbed8e7 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -242,7 +242,6 @@ import ( "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/internal/aliases" - "golang.org/x/tools/internal/tokeninternal" ) // IExportShallow encodes "shallow" export data for the specified package. @@ -441,7 +440,7 @@ func (p *iexporter) encodeFile(w *intWriter, file *token.File, needed []uint64) // Sort the set of needed offsets. Duplicates are harmless. sort.Slice(needed, func(i, j int) bool { return needed[i] < needed[j] }) - lines := tokeninternal.GetLines(file) // byte offset of each line start + lines := file.Lines() // byte offset of each line start w.uint64(uint64(len(lines))) // Rather than record the entire array of line start offsets, @@ -725,13 +724,13 @@ func (p *iexporter) doDecl(obj types.Object) { case *types.TypeName: t := obj.Type() - if tparam, ok := aliases.Unalias(t).(*types.TypeParam); ok { + if tparam, ok := types.Unalias(t).(*types.TypeParam); ok { w.tag(typeParamTag) w.pos(obj.Pos()) constraint := tparam.Constraint() if p.version >= iexportVersionGo1_18 { implicit := false - if iface, _ := aliases.Unalias(constraint).(*types.Interface); iface != nil { + if iface, _ := types.Unalias(constraint).(*types.Interface); iface != nil { implicit = iface.IsImplicit() } w.bool(implicit) @@ -741,7 +740,7 @@ func (p *iexporter) doDecl(obj types.Object) { } if obj.IsAlias() { - alias, materialized := t.(*aliases.Alias) // may fail when aliases are not enabled + alias, materialized := t.(*types.Alias) // may fail when aliases are not enabled var tparams *types.TypeParamList if materialized { @@ -975,7 +974,7 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { }() } switch t := t.(type) { - case *aliases.Alias: + case *types.Alias: if targs := aliases.TypeArgs(t); targs.Len() > 0 { w.startType(instanceType) w.pos(t.Obj().Pos()) @@ -1091,7 +1090,7 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { for i := 0; i < n; i++ { ft := t.EmbeddedType(i) tPkg := pkg - if named, _ := aliases.Unalias(ft).(*types.Named); named != nil { + if named, _ := types.Unalias(ft).(*types.Named); named != nil { w.pos(named.Obj().Pos()) } else { w.pos(token.NoPos) diff --git a/internal/gcimporter/iexport_common_test.go b/internal/gcimporter/iexport_common_test.go index abc6aa64b92..00dc2ffd5de 100644 --- a/internal/gcimporter/iexport_common_test.go +++ b/internal/gcimporter/iexport_common_test.go @@ -9,8 +9,4 @@ package gcimporter var IExportCommon = iexportCommon -const ( - IExportVersion = iexportVersion - IExportVersionGenerics = iexportVersionGenerics - IExportVersionGo1_18 = iexportVersionGo1_18 -) +const IExportVersion = iexportVersionGenerics diff --git a/internal/gcimporter/iexport_go118_test.go b/internal/gcimporter/iexport_go118_test.go index c748fb36165..005b95b94f3 100644 --- a/internal/gcimporter/iexport_go118_test.go +++ b/internal/gcimporter/iexport_go118_test.go @@ -96,9 +96,13 @@ func testExportSrc(t *testing.T, src []byte) { testPkgData(t, fset, version, pkg, data) } -func TestImportTypeparamTests(t *testing.T) { +func TestIndexedImportTypeparamTests(t *testing.T) { testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache + testAliases(t, testIndexedImportTypeparamTests) +} + +func testIndexedImportTypeparamTests(t *testing.T) { // Check go files in test/typeparam. rootDir := filepath.Join(runtime.GOROOT(), "test", "typeparam") list, err := os.ReadDir(rootDir) @@ -106,10 +110,6 @@ func TestImportTypeparamTests(t *testing.T) { t.Fatal(err) } - if isUnifiedBuilder() { - t.Skip("unified export data format is currently unsupported") - } - for _, entry := range list { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { // For now, only consider standalone go files. diff --git a/internal/gcimporter/iexport_test.go b/internal/gcimporter/iexport_test.go index 7e82a58189f..cb6ccdd7929 100644 --- a/internal/gcimporter/iexport_test.go +++ b/internal/gcimporter/iexport_test.go @@ -4,63 +4,30 @@ // This is a copy of bexport_test.go for iexport.go. -//go:build go1.11 -// +build go1.11 - package gcimporter_test import ( - "bufio" "bytes" "fmt" "go/ast" - "go/build" "go/constant" "go/importer" "go/parser" "go/token" "go/types" - "io" "math/big" "os" "path/filepath" "reflect" - "runtime" - "sort" "strings" "testing" - "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/gcexportdata" - "golang.org/x/tools/go/loader" - "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/gcimporter" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/typeparams/genericfeatures" ) -func readExportFile(filename string) ([]byte, error) { - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - - buf := bufio.NewReader(f) - if _, _, err := gcimporter.FindExportData(buf); err != nil { - return nil, err - } - - if ch, err := buf.ReadByte(); err != nil { - return nil, err - } else if ch != 'i' { - return nil, fmt.Errorf("unexpected byte: %v", ch) - } - - return io.ReadAll(buf) -} - func iexport(fset *token.FileSet, version int, pkg *types.Package) ([]byte, error) { var buf bytes.Buffer const bundle, shallow = false, false @@ -70,19 +37,8 @@ func iexport(fset *token.FileSet, version int, pkg *types.Package) ([]byte, erro return buf.Bytes(), nil } -// isUnifiedBuilder reports whether we are executing on a go builder that uses -// unified export data. -func isUnifiedBuilder() bool { - return os.Getenv("GO_BUILDER_NAME") == "linux-amd64-unified" -} - -const minStdlibPackages = 248 - func TestIExportData_stdlib(t *testing.T) { - if runtime.Compiler == "gccgo" { - t.Skip("gccgo standard library is inaccessible") - } - testenv.NeedsGoBuild(t) + testenv.NeedsGoPackages(t) if isRace { t.Skipf("stdlib tests take too long in race mode and flake on builders") } @@ -90,85 +46,90 @@ func TestIExportData_stdlib(t *testing.T) { t.Skip("skipping RAM hungry test in -short mode") } - // Load, parse and type-check the program. - ctxt := build.Default // copy - ctxt.GOPATH = "" // disable GOPATH - conf := loader.Config{ - Build: &ctxt, - AllowErrors: true, - TypeChecker: types.Config{ - Sizes: types.SizesFor(ctxt.Compiler, ctxt.GOARCH), - Error: func(err error) { t.Log(err) }, - }, - } - for _, path := range buildutil.AllPackages(conf.Build) { - conf.Import(path) + testAliases(t, testIExportData_stdlib) +} + +func testIExportData_stdlib(t *testing.T) { + var errorsDir string // GOROOT/src/errors directory + { + cfg := packages.Config{ + Mode: packages.NeedName | packages.NeedFiles, + } + pkgs, err := packages.Load(&cfg, "errors") + if err != nil { + t.Fatal(err) + } + errorsDir = filepath.Dir(pkgs[0].GoFiles[0]) } - // Create a package containing type and value errors to ensure - // they are properly encoded/decoded. - f, err := conf.ParseFile("haserrors/haserrors.go", `package haserrors + // Load types from syntax for all std packages. + // + // Append a file to package errors containing type and + // value errors to ensure they are properly encoded/decoded. + const bad = `package errors const UnknownValue = "" + 0 type UnknownType undefined -`) +` + cfg := packages.Config{ + Mode: packages.LoadAllSyntax | packages.NeedDeps, + Overlay: map[string][]byte{filepath.Join(errorsDir, "bad.go"): []byte(bad)}, + } + pkgs, err := packages.Load(&cfg, "std") // ~800ms if err != nil { t.Fatal(err) } - conf.CreateFromFiles("haserrors", f) + fset := pkgs[0].Fset - prog, err := conf.Load() - if err != nil { - t.Fatalf("Load failed: %v", err) - } + version := gcimporter.IExportVersion - var sorted []*types.Package - isUnified := isUnifiedBuilder() - for pkg, info := range prog.AllPackages { - // Temporarily skip packages that use generics on the unified builder, to - // fix TryBots. - // - // TODO(#48595): fix this test with GOEXPERIMENT=unified. - inspect := inspector.New(info.Files) - features := genericfeatures.ForPackage(inspect, &info.Info) - if isUnified && features != 0 { - t.Logf("skipping package %q which uses generics", pkg.Path()) - continue + // Export and reimport each package, and check that they match. + var allPkgs []*types.Package + var errorsPkg *types.Package // reimported errors package + packages.Visit(pkgs, nil, func(ppkg *packages.Package) { // ~300ms + pkg := ppkg.Types + path := pkg.Path() + if path == "unsafe" || + strings.HasPrefix(path, "cmd/") || + strings.HasPrefix(path, "vendor/") { + return } - if info.Files != nil { // non-empty directory - sorted = append(sorted, pkg) + allPkgs = append(allPkgs, pkg) + + // Export and reimport the package, and compare. + exportdata, err := iexport(fset, version, pkg) + if err != nil { + t.Error(err) + return + } + pkg2 := testPkgData(t, fset, version, pkg, exportdata) + if path == "errors" { + errorsPkg = pkg2 } - } - sort.Slice(sorted, func(i, j int) bool { - return sorted[i].Path() < sorted[j].Path() }) - version := gcimporter.IExportVersion - numPkgs := len(sorted) - if want := minStdlibPackages; numPkgs < want { - t.Errorf("Loaded only %d packages, want at least %d", numPkgs, want) + // Assert that we saw a plausible sized library. + const minStdlibPackages = 248 + if n := len(allPkgs); n < minStdlibPackages { + t.Errorf("Loaded only %d packages, want at least %d", n, minStdlibPackages) } - // TODO(adonovan): opt: parallelize this slow loop. - for _, pkg := range sorted { - if exportdata, err := iexport(conf.Fset, version, pkg); err != nil { - t.Error(err) - } else { - testPkgData(t, conf.Fset, version, pkg, exportdata) + // Check that reimported errors package has bad decls. + if errorsPkg == nil { + t.Fatalf("'errors' package not found") + } + for _, name := range []string{"UnknownType", "UnknownValue"} { + obj := errorsPkg.Scope().Lookup(name) + if obj == nil { + t.Errorf("errors.%s not found", name) } - - if pkg.Name() == "main" || pkg.Name() == "haserrors" { - // skip; no export data - } else if bp, err := ctxt.Import(pkg.Path(), "", build.FindOnly); err != nil { - t.Log("warning:", err) - } else if exportdata, err := readExportFile(bp.PkgObj); err != nil { - t.Log("warning:", err) - } else { - testPkgData(t, conf.Fset, version, pkg, exportdata) + if typ := obj.Type().Underlying(); typ.String() != "invalid type" { + t.Errorf("errors.%s has underlying type %s, want invalid type", name, typ) } } + // (Sole) test of bundle functionality (250ms). var bundle bytes.Buffer - if err := gcimporter.IExportBundle(&bundle, conf.Fset, sorted); err != nil { + if err := gcimporter.IExportBundle(&bundle, fset, allPkgs); err != nil { t.Fatal(err) } fset2 := token.NewFileSet() @@ -177,13 +138,13 @@ type UnknownType undefined if err != nil { t.Fatal(err) } - - for i, pkg := range sorted { - testPkg(t, conf.Fset, version, pkg, fset2, pkgs2[i]) + for i, pkg := range allPkgs { + testPkg(t, fset, version, pkg, fset2, pkgs2[i]) } } -func testPkgData(t *testing.T, fset *token.FileSet, version int, pkg *types.Package, exportdata []byte) { +// testPkgData imports a package from export data and compares it with pkg. +func testPkgData(t *testing.T, fset *token.FileSet, version int, pkg *types.Package, exportdata []byte) *types.Package { imports := make(map[string]*types.Package) fset2 := token.NewFileSet() _, pkg2, err := gcimporter.IImportData(fset2, imports, exportdata, pkg.Path()) @@ -192,6 +153,7 @@ func testPkgData(t *testing.T, fset *token.FileSet, version int, pkg *types.Pack } testPkg(t, fset, version, pkg, fset2, pkg2) + return pkg2 } func testPkg(t *testing.T, fset *token.FileSet, version int, pkg *types.Package, fset2 *token.FileSet, pkg2 *types.Package) { @@ -266,6 +228,9 @@ func TestIExportData_long(t *testing.T) { } func TestIExportData_typealiases(t *testing.T) { + testAliases(t, testIExportData_typealiases) +} +func testIExportData_typealiases(t *testing.T) { // parse and typecheck fset1 := token.NewFileSet() f, err := parser.ParseFile(fset1, "p.go", src, 0) @@ -344,8 +309,8 @@ func cmpObj(x, y types.Object) error { // situations where the type name is not referenced by the underlying or // any other top-level declarations. Therefore, we must explicitly compare // named types here, before passing their underlying types into equalType. - xn, _ := aliases.Unalias(xt).(*types.Named) - yn, _ := aliases.Unalias(yt).(*types.Named) + xn, _ := types.Unalias(xt).(*types.Named) + yn, _ := types.Unalias(yt).(*types.Named) if (xn == nil) != (yn == nil) { return fmt.Errorf("mismatching types: %T vs %T", xt, yt) } @@ -461,9 +426,12 @@ func (f importerFunc) Import(path string) (*types.Package, error) { return f(pat // on both declarations and uses of type parameterized aliases. func TestIExportDataTypeParameterizedAliases(t *testing.T) { testenv.NeedsGo1Point(t, 23) + skipWindows(t) + if testenv.Go1Point() == 23 { + testenv.NeedsGoExperiment(t, "aliastypeparams") // testenv.Go1Point() >= 24 implies aliastypeparams=1 + } - testenv.NeedsGoExperiment(t, "aliastypeparams") - t.Setenv("GODEBUG", "gotypesalias=1") + t.Setenv("GODEBUG", aliasesOn) // High level steps: // * parse and typecheck @@ -471,7 +439,7 @@ func TestIExportDataTypeParameterizedAliases(t *testing.T) { // * import the data (via either x/tools or GOROOT's gcimporter), and // * check the imported types. - const src = `package a + const src = `package pkg type A[T any] = *T type B[R any, S *R] = []S @@ -483,12 +451,12 @@ type Chained = C[Named] // B[Named, A[Named]] = B[Named, *Named] = []*Named // parse and typecheck fset1 := token.NewFileSet() - f, err := parser.ParseFile(fset1, "a", src, 0) + f, err := parser.ParseFile(fset1, "pkg", src, 0) if err != nil { t.Fatal(err) } var conf types.Config - pkg1, err := conf.Check("a", fset1, []*ast.File{f}, nil) + pkg1, err := conf.Check("pkg", fset1, []*ast.File{f}, nil) if err != nil { t.Fatal(err) } @@ -517,6 +485,8 @@ type Chained = C[Named] // B[Named, A[Named]] = B[Named, *Named] = []*Named // This means that it can be loaded by go/importer or go/types. // This step is not supported, but it does give test coverage for stdlib. "goroot": func(t *testing.T) *types.Package { + testenv.NeedsGo1Point(t, 24) // requires >= 1.24 go/importer. + // Write indexed export data file contents. // // TODO(taking): Slightly unclear to what extent this step should be supported by go/importer. @@ -528,7 +498,7 @@ type Chained = C[Named] // B[Named, A[Named]] = B[Named, *Named] = []*Named // Write export data to temporary file out := t.TempDir() - name := filepath.Join(out, "a.out") + name := filepath.Join(out, "pkg.out") if err := os.WriteFile(name+".a", buf.Bytes(), 0644); err != nil { t.Fatal(err) } @@ -543,58 +513,23 @@ type Chained = C[Named] // B[Named, A[Named]] = B[Named, *Named] = []*Named for name, importer := range testcases { t.Run(name, func(t *testing.T) { pkg := importer(t) + for name, want := range map[string]string{ + "A": "type pkg.A[T any] = *T", + "B": "type pkg.B[R any, S *R] = []S", + "C": "type pkg.C[U any] = pkg.B[U, pkg.A[U]]", + "Named": "type pkg.Named int", + "Chained": "type pkg.Chained = pkg.C[pkg.Named]", + } { + obj := pkg.Scope().Lookup(name) + if obj == nil { + t.Errorf("failed to find %q in package %s", name, pkg) + continue + } - obj := pkg.Scope().Lookup("A") - if obj == nil { - t.Fatalf("failed to find %q in package %s", "A", pkg) - } - - // Check that A is type A[T any] = *T. - // TODO(taking): fix how go/types prints parameterized aliases to simplify tests. - alias, ok := obj.Type().(*aliases.Alias) - if !ok { - t.Fatalf("Obj %s is not an Alias", obj) - } - - targs := aliases.TypeArgs(alias) - if targs.Len() != 0 { - t.Errorf("%s has %d type arguments. expected 0", alias, targs.Len()) - } - - tparams := aliases.TypeParams(alias) - if tparams.Len() != 1 { - t.Fatalf("%s has %d type arguments. expected 1", alias, targs.Len()) - } - tparam := tparams.At(0) - if got, want := tparam.String(), "T"; got != want { - t.Errorf("(%q).TypeParams().At(0)=%q. want %q", alias, got, want) - } - - anyt := types.Universe.Lookup("any").Type() - if c := tparam.Constraint(); !types.Identical(anyt, c) { - t.Errorf("(%q).Constraint()=%q. expected %q", tparam, c, anyt) - } - - ptparam := types.NewPointer(tparam) - if rhs := aliases.Rhs(alias); !types.Identical(ptparam, rhs) { - t.Errorf("(%q).Rhs()=%q. expected %q", alias, rhs, ptparam) - } - - // TODO(taking): add tests for B and C once it is simpler to write tests. - - chained := pkg.Scope().Lookup("Chained") - if chained == nil { - t.Fatalf("failed to find %q in package %s", "Chained", pkg) - } - - named, _ := pkg.Scope().Lookup("Named").(*types.TypeName) - if named == nil { - t.Fatalf("failed to find %q in package %s", "Named", pkg) - } - - want := types.NewSlice(types.NewPointer(named.Type())) - if got := chained.Type(); !types.Identical(got, want) { - t.Errorf("(%q).Type()=%q which should be identical to %q", chained, got, want) + got := strings.ReplaceAll(obj.String(), pkg.Path(), "pkg") + if got != want { + t.Errorf("(%q).String()=%q. wanted %q", name, got, want) + } } }) } diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index ed2d5629596..21908a158b4 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -53,6 +53,7 @@ const ( iexportVersionPosCol = 1 iexportVersionGo1_18 = 2 iexportVersionGenerics = 2 + iexportVersion = iexportVersionGenerics iexportVersionCurrent = 2 ) @@ -540,7 +541,7 @@ func canReuse(def *types.Named, rhs types.Type) bool { if def == nil { return true } - iface, _ := aliases.Unalias(rhs).(*types.Interface) + iface, _ := types.Unalias(rhs).(*types.Interface) if iface == nil { return true } @@ -615,7 +616,7 @@ func (r *importReader) obj(name string) { if targs.Len() > 0 { rparams = make([]*types.TypeParam, targs.Len()) for i := range rparams { - rparams[i] = aliases.Unalias(targs.At(i)).(*types.TypeParam) + rparams[i] = types.Unalias(targs.At(i)).(*types.TypeParam) } } msig := r.signature(recv, rparams, nil) @@ -645,7 +646,7 @@ func (r *importReader) obj(name string) { } constraint := r.typ() if implicit { - iface, _ := aliases.Unalias(constraint).(*types.Interface) + iface, _ := types.Unalias(constraint).(*types.Interface) if iface == nil { errorf("non-interface constraint marked implicit") } @@ -852,7 +853,7 @@ func (r *importReader) typ() types.Type { } func isInterface(t types.Type) bool { - _, ok := aliases.Unalias(t).(*types.Interface) + _, ok := types.Unalias(t).(*types.Interface) return ok } @@ -959,7 +960,7 @@ func (r *importReader) doType(base *types.Named) (res types.Type) { methods[i] = method } - typ := newInterface(methods, embeddeds) + typ := types.NewInterfaceType(methods, embeddeds) r.p.interfaceList = append(r.p.interfaceList, typ) return typ @@ -1051,7 +1052,7 @@ func (r *importReader) tparamList() []*types.TypeParam { for i := range xs { // Note: the standard library importer is tolerant of nil types here, // though would panic in SetTypeParams. - xs[i] = aliases.Unalias(r.typ()).(*types.TypeParam) + xs[i] = types.Unalias(r.typ()).(*types.TypeParam) } return xs } diff --git a/internal/gcimporter/newInterface10.go b/internal/gcimporter/newInterface10.go deleted file mode 100644 index 8b163e3d058..00000000000 --- a/internal/gcimporter/newInterface10.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.11 -// +build !go1.11 - -package gcimporter - -import "go/types" - -func newInterface(methods []*types.Func, embeddeds []types.Type) *types.Interface { - named := make([]*types.Named, len(embeddeds)) - for i, e := range embeddeds { - var ok bool - named[i], ok = e.(*types.Named) - if !ok { - panic("embedding of non-defined interfaces in interfaces is not supported before Go 1.11") - } - } - return types.NewInterface(methods, named) -} diff --git a/internal/gcimporter/newInterface11.go b/internal/gcimporter/newInterface11.go deleted file mode 100644 index 49984f40fd8..00000000000 --- a/internal/gcimporter/newInterface11.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.11 -// +build go1.11 - -package gcimporter - -import "go/types" - -func newInterface(methods []*types.Func, embeddeds []types.Type) *types.Interface { - return types.NewInterfaceType(methods, embeddeds) -} diff --git a/internal/gcimporter/predeclared.go b/internal/gcimporter/predeclared.go new file mode 100644 index 00000000000..907c8557a54 --- /dev/null +++ b/internal/gcimporter/predeclared.go @@ -0,0 +1,91 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gcimporter + +import ( + "go/types" + "sync" +) + +// predecl is a cache for the predeclared types in types.Universe. +// +// Cache a distinct result based on the runtime value of any. +// The pointer value of the any type varies based on GODEBUG settings. +var predeclMu sync.Mutex +var predecl map[types.Type][]types.Type + +func predeclared() []types.Type { + anyt := types.Universe.Lookup("any").Type() + + predeclMu.Lock() + defer predeclMu.Unlock() + + if pre, ok := predecl[anyt]; ok { + return pre + } + + if predecl == nil { + predecl = make(map[types.Type][]types.Type) + } + + decls := []types.Type{ // basic types + types.Typ[types.Bool], + types.Typ[types.Int], + types.Typ[types.Int8], + types.Typ[types.Int16], + types.Typ[types.Int32], + types.Typ[types.Int64], + types.Typ[types.Uint], + types.Typ[types.Uint8], + types.Typ[types.Uint16], + types.Typ[types.Uint32], + types.Typ[types.Uint64], + types.Typ[types.Uintptr], + types.Typ[types.Float32], + types.Typ[types.Float64], + types.Typ[types.Complex64], + types.Typ[types.Complex128], + types.Typ[types.String], + + // basic type aliases + types.Universe.Lookup("byte").Type(), + types.Universe.Lookup("rune").Type(), + + // error + types.Universe.Lookup("error").Type(), + + // untyped types + types.Typ[types.UntypedBool], + types.Typ[types.UntypedInt], + types.Typ[types.UntypedRune], + types.Typ[types.UntypedFloat], + types.Typ[types.UntypedComplex], + types.Typ[types.UntypedString], + types.Typ[types.UntypedNil], + + // package unsafe + types.Typ[types.UnsafePointer], + + // invalid type + types.Typ[types.Invalid], // only appears in packages with errors + + // used internally by gc; never used by this package or in .a files + anyType{}, + + // comparable + types.Universe.Lookup("comparable").Type(), + + // any + anyt, + } + + predecl[anyt] = decls + return decls +} + +type anyType struct{} + +func (t anyType) Underlying() types.Type { return t } +func (t anyType) String() string { return "any" } diff --git a/internal/gcimporter/shallow_test.go b/internal/gcimporter/shallow_test.go index e412a947bdf..f1ae8781e83 100644 --- a/internal/gcimporter/shallow_test.go +++ b/internal/gcimporter/shallow_test.go @@ -27,6 +27,9 @@ func TestShallowStd(t *testing.T) { } testenv.NeedsTool(t, "go") + testAliases(t, testShallowStd) +} +func testShallowStd(t *testing.T) { // Load import graph of the standard library. // (No parsing or type-checking.) cfg := &packages.Config{ diff --git a/internal/gcimporter/stdlib_test.go b/internal/gcimporter/stdlib_test.go index 33ff7958118..85547a49d7b 100644 --- a/internal/gcimporter/stdlib_test.go +++ b/internal/gcimporter/stdlib_test.go @@ -19,10 +19,13 @@ import ( ) // TestStdlib ensures that all packages in std and x/tools can be -// type-checked using export data. Takes around 3s. +// type-checked using export data. func TestStdlib(t *testing.T) { testenv.NeedsGoPackages(t) + testAliases(t, testStdlib) +} +func testStdlib(t *testing.T) { // gcexportdata.Read rapidly consumes FileSet address space, // so disable the test on 32-bit machines. // (We could use a fresh FileSet per type-check, but that diff --git a/internal/gcimporter/support_go118.go b/internal/gcimporter/support_go118.go deleted file mode 100644 index 0cd3b91b65a..00000000000 --- a/internal/gcimporter/support_go118.go +++ /dev/null @@ -1,34 +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. - -package gcimporter - -import "go/types" - -const iexportVersion = iexportVersionGenerics - -// additionalPredeclared returns additional predeclared types in go.1.18. -func additionalPredeclared() []types.Type { - return []types.Type{ - // comparable - types.Universe.Lookup("comparable").Type(), - - // any - types.Universe.Lookup("any").Type(), - } -} - -// See cmd/compile/internal/types.SplitVargenSuffix. -func splitVargenSuffix(name string) (base, suffix string) { - i := len(name) - for i > 0 && name[i-1] >= '0' && name[i-1] <= '9' { - i-- - } - const dot = "·" - if i >= len(dot) && name[i-len(dot):i] == dot { - i -= len(dot) - return name[:i], name[i:] - } - return name, "" -} diff --git a/internal/gcimporter/unified_no.go b/internal/gcimporter/unified_no.go deleted file mode 100644 index 38b624cadab..00000000000 --- a/internal/gcimporter/unified_no.go +++ /dev/null @@ -1,10 +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 !goexperiment.unified -// +build !goexperiment.unified - -package gcimporter - -const unifiedIR = false diff --git a/internal/gcimporter/unified_yes.go b/internal/gcimporter/unified_yes.go deleted file mode 100644 index b5118d0b3a5..00000000000 --- a/internal/gcimporter/unified_yes.go +++ /dev/null @@ -1,10 +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 goexperiment.unified -// +build goexperiment.unified - -package gcimporter - -const unifiedIR = true diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index f0742f5404b..1db408613c9 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -562,7 +562,7 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { // If the underlying type is an interface, we need to // duplicate its methods so we can replace the receiver // parameter's type (#49906). - if iface, ok := aliases.Unalias(underlying).(*types.Interface); ok && iface.NumExplicitMethods() != 0 { + if iface, ok := types.Unalias(underlying).(*types.Interface); ok && iface.NumExplicitMethods() != 0 { methods := make([]*types.Func, iface.NumExplicitMethods()) for i := range methods { fn := iface.ExplicitMethod(i) @@ -738,3 +738,17 @@ func pkgScope(pkg *types.Package) *types.Scope { } return types.Universe } + +// See cmd/compile/internal/types.SplitVargenSuffix. +func splitVargenSuffix(name string) (base, suffix string) { + i := len(name) + for i > 0 && name[i-1] >= '0' && name[i-1] <= '9' { + i-- + } + const dot = "·" + if i >= len(dot) && name[i-len(dot):i] == dot { + i -= len(dot) + return name[:i], name[i:] + } + return name, "" +} diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go index 2e59ff8558c..e333efc87f9 100644 --- a/internal/gocommand/invoke.go +++ b/internal/gocommand/invoke.go @@ -16,7 +16,6 @@ import ( "os" "os/exec" "path/filepath" - "reflect" "regexp" "runtime" "strconv" @@ -250,16 +249,13 @@ func (i *Invocation) run(ctx context.Context, stdout, stderr io.Writer) error { cmd.Stdout = stdout cmd.Stderr = stderr - // cmd.WaitDelay was added only in go1.20 (see #50436). - if waitDelay := reflect.ValueOf(cmd).Elem().FieldByName("WaitDelay"); waitDelay.IsValid() { - // https://go.dev/issue/59541: don't wait forever copying stderr - // after the command has exited. - // After CL 484741 we copy stdout manually, so we we'll stop reading that as - // soon as ctx is done. However, we also don't want to wait around forever - // for stderr. Give a much-longer-than-reasonable delay and then assume that - // something has wedged in the kernel or runtime. - waitDelay.Set(reflect.ValueOf(30 * time.Second)) - } + // https://go.dev/issue/59541: don't wait forever copying stderr + // after the command has exited. + // After CL 484741 we copy stdout manually, so we we'll stop reading that as + // soon as ctx is done. However, we also don't want to wait around forever + // for stderr. Give a much-longer-than-reasonable delay and then assume that + // something has wedged in the kernel or runtime. + cmd.WaitDelay = 30 * time.Second // The cwd gets resolved to the real path. On Darwin, where // /tmp is a symlink, this breaks anything that expects the diff --git a/internal/imports/fix.go b/internal/imports/fix.go index dc7d50a7a40..c15108178ab 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -131,7 +131,7 @@ func parseOtherFiles(ctx context.Context, fset *token.FileSet, srcDir, filename continue } - f, err := parser.ParseFile(fset, filepath.Join(srcDir, fi.Name()), nil, 0) + f, err := parser.ParseFile(fset, filepath.Join(srcDir, fi.Name()), nil, parser.SkipObjectResolution) if err != nil { continue } @@ -1620,6 +1620,7 @@ func loadExportsFromFiles(ctx context.Context, env *ProcessEnv, dir string, incl } fullFile := filepath.Join(dir, fi.Name()) + // Legacy ast.Object resolution is needed here. f, err := parser.ParseFile(fset, fullFile, nil, 0) if err != nil { env.logf("error parsing %v: %v", fullFile, err) diff --git a/internal/imports/imports.go b/internal/imports/imports.go index f83465520a4..ff6b59a58a0 100644 --- a/internal/imports/imports.go +++ b/internal/imports/imports.go @@ -86,7 +86,7 @@ func ApplyFixes(fixes []*ImportFix, filename string, src []byte, opt *Options, e // Don't use parse() -- we don't care about fragments or statement lists // here, and we need to work with unparseable files. fileSet := token.NewFileSet() - parserMode := parser.Mode(0) + parserMode := parser.SkipObjectResolution if opt.Comments { parserMode |= parser.ParseComments } @@ -165,7 +165,7 @@ func formatFile(fset *token.FileSet, file *ast.File, src []byte, adjust func(ori // parse parses src, which was read from filename, // as a Go source file or statement list. func parse(fset *token.FileSet, filename string, src []byte, opt *Options) (*ast.File, func(orig, src []byte) []byte, error) { - parserMode := parser.Mode(0) + var parserMode parser.Mode // legacy ast.Object resolution is required here if opt.Comments { parserMode |= parser.ParseComments } diff --git a/internal/imports/mkindex.go b/internal/imports/mkindex.go index 2ecc9e45e9f..ff006b0cd2e 100644 --- a/internal/imports/mkindex.go +++ b/internal/imports/mkindex.go @@ -158,6 +158,7 @@ func loadExports(dir string) map[string]bool { return nil } for _, file := range buildPkg.GoFiles { + // Legacy ast.Object resolution is needed here. f, err := parser.ParseFile(fset, filepath.Join(dir, file), nil, 0) if err != nil { log.Printf("could not parse %q: %v", file, err) diff --git a/internal/imports/mod_go122_test.go b/internal/imports/mod_go122_test.go deleted file mode 100644 index af8feef713d..00000000000 --- a/internal/imports/mod_go122_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019 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.22 -// +build go1.22 - -package imports - -import ( - "context" - "testing" -) - -// Tests that go.work files and vendor directory are respected. -func TestModWorkspaceVendoring(t *testing.T) { - mt := setup(t, nil, ` --- go.work -- -go 1.22 - -use ( - ./a - ./b -) --- a/go.mod -- -module example.com/a - -go 1.22 - -require rsc.io/sampler v1.3.1 --- a/a.go -- -package a - -import _ "rsc.io/sampler" --- b/go.mod -- -module example.com/b - -go 1.22 --- b/b.go -- -package b -`, "") - defer mt.cleanup() - - // generate vendor directory - if _, err := mt.env.invokeGo(context.Background(), "work", "vendor"); err != nil { - t.Fatal(err) - } - - // update module resolver - mt.env.ClearModuleInfo() - mt.env.UpdateResolver(mt.env.resolver.ClearForNewScan()) - - mt.assertModuleFoundInDir("example.com/a", "a", `main/a$`) - mt.assertScanFinds("example.com/a", "a") - mt.assertModuleFoundInDir("example.com/b", "b", `main/b$`) - mt.assertScanFinds("example.com/b", "b") - mt.assertModuleFoundInDir("rsc.io/sampler", "sampler", `/vendor/`) -} diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go index e65104cbf2e..890dc1b2e25 100644 --- a/internal/imports/mod_test.go +++ b/internal/imports/mod_test.go @@ -267,10 +267,7 @@ import _ "rsc.io/sampler" t.Fatal(err) } - wantDir := `pkg.*mod.*/sampler@.*$` - if testenv.Go1Point() >= 14 { - wantDir = `/vendor/` - } + wantDir := `/vendor/` // Clear out the resolver's module info, since we've changed the environment. // (the presence of a /vendor directory affects `go list -m`). @@ -1323,3 +1320,48 @@ func BenchmarkModuleResolver_InitialScan(b *testing.B) { scanToSlice(resolver, exclude) } } + +// Tests that go.work files and vendor directory are respected. +func TestModWorkspaceVendoring(t *testing.T) { + mt := setup(t, nil, ` +-- go.work -- +go 1.22 + +use ( + ./a + ./b +) +-- a/go.mod -- +module example.com/a + +go 1.22 + +require rsc.io/sampler v1.3.1 +-- a/a.go -- +package a + +import _ "rsc.io/sampler" +-- b/go.mod -- +module example.com/b + +go 1.22 +-- b/b.go -- +package b +`, "") + defer mt.cleanup() + + // generate vendor directory + if _, err := mt.env.invokeGo(context.Background(), "work", "vendor"); err != nil { + t.Fatal(err) + } + + // update module resolver + mt.env.ClearModuleInfo() + mt.env.UpdateResolver(mt.env.resolver.ClearForNewScan()) + + mt.assertModuleFoundInDir("example.com/a", "a", `main/a$`) + mt.assertScanFinds("example.com/a", "a") + mt.assertModuleFoundInDir("example.com/b", "b", `main/b$`) + mt.assertScanFinds("example.com/b", "b") + mt.assertModuleFoundInDir("rsc.io/sampler", "sampler", `/vendor/`) +} diff --git a/internal/modindex/dir_test.go b/internal/modindex/dir_test.go new file mode 100644 index 00000000000..862d111ea42 --- /dev/null +++ b/internal/modindex/dir_test.go @@ -0,0 +1,127 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modindex + +import ( + "os" + "path/filepath" + "testing" +) + +type id struct { + importPath string + best int // which of the dirs is the one that should have been chosen + dirs []string +} + +var idtests = []id{ + { // get one right + importPath: "cloud.google.com/go/longrunning", + best: 2, + dirs: []string{ + "cloud.google.com/go/longrunning@v0.3.0", + "cloud.google.com/go/longrunning@v0.4.1", + "cloud.google.com/go@v0.104.0/longrunning", + "cloud.google.com/go@v0.94.0/longrunning", + }, + }, + { // make sure we can run more than one test + importPath: "cloud.google.com/go/compute/metadata", + best: 2, + dirs: []string{ + "cloud.google.com/go/compute/metadata@v0.2.1", + "cloud.google.com/go/compute/metadata@v0.2.3", + "cloud.google.com/go/compute@v1.7.0/metadata", + "cloud.google.com/go@v0.94.0/compute/metadata", + }, + }, + { //m test bizarre characters in directory name + importPath: "bad,guy.com/go", + best: 0, + dirs: []string{"bad,guy.com/go@v0.1.0"}, + }, +} + +func testModCache(t *testing.T) string { + t.Helper() + dir := t.TempDir() + IndexDir = func() (string, error) { return dir, nil } + return dir +} + +func TestDirsSinglePath(t *testing.T) { + for _, itest := range idtests { + t.Run(itest.importPath, func(t *testing.T) { + // create a new fake GOMODCACHE + dir := testModCache(t) + for _, d := range itest.dirs { + if err := os.MkdirAll(filepath.Join(dir, d), 0755); err != nil { + t.Fatal(err) + } + // gopathwalk wants to see .go files + err := os.WriteFile(filepath.Join(dir, d, "main.go"), []byte("package main\nfunc main() {}"), 0600) + if err != nil { + t.Fatal(err) + } + } + // build and check the index + if err := IndexModCache(dir, false); err != nil { + t.Fatal(err) + } + ix, err := ReadIndex(dir) + if err != nil { + t.Fatal(err) + } + if len(ix.Entries) != 1 { + t.Fatalf("got %d entries, wanted 1", len(ix.Entries)) + } + if ix.Entries[0].ImportPath != itest.importPath { + t.Fatalf("got %s import path, wanted %s", ix.Entries[0].ImportPath, itest.importPath) + } + if ix.Entries[0].Dir != Relpath(itest.dirs[itest.best]) { + t.Fatalf("got dir %s, wanted %s", ix.Entries[0].Dir, itest.dirs[itest.best]) + } + }) + } +} + +/* more data for tests + +directories.go:169: WEIRD cloud.google.com/go/iam/admin/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0/admin/apiv1 +cloud.google.com/go/iam@v0.13.0/admin/apiv1 +cloud.google.com/go/iam@v0.3.0/admin/apiv1 +cloud.google.com/go/iam@v0.7.0/admin/apiv1 +cloud.google.com/go/iam@v1.0.1/admin/apiv1 +cloud.google.com/go@v0.94.0/iam/admin/apiv1] +directories.go:169: WEIRD cloud.google.com/go/iam +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0 cloud.google.com/go/iam@v0.13.0 +cloud.google.com/go/iam@v0.3.0 cloud.google.com/go/iam@v0.7.0 +cloud.google.com/go/iam@v1.0.1 cloud.google.com/go@v0.94.0/iam] +directories.go:169: WEIRD cloud.google.com/go/compute/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/compute:4]: +[cloud.google.com/go/compute@v1.12.1/apiv1 +cloud.google.com/go/compute@v1.18.0/apiv1 +cloud.google.com/go/compute@v1.19.0/apiv1 +cloud.google.com/go/compute@v1.7.0/apiv1 +cloud.google.com/go@v0.94.0/compute/apiv1] +directories.go:169: WEIRD cloud.google.com/go/longrunning/autogen +map[cloud.google.com/go:2 cloud.google.com/go/longrunning:2]: +[cloud.google.com/go/longrunning@v0.3.0/autogen +cloud.google.com/go/longrunning@v0.4.1/autogen +cloud.google.com/go@v0.104.0/longrunning/autogen +cloud.google.com/go@v0.94.0/longrunning/autogen] +directories.go:169: WEIRD cloud.google.com/go/iam/credentials/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0/credentials/apiv1 +cloud.google.com/go/iam@v0.13.0/credentials/apiv1 +cloud.google.com/go/iam@v0.3.0/credentials/apiv1 +cloud.google.com/go/iam@v0.7.0/credentials/apiv1 +cloud.google.com/go/iam@v1.0.1/credentials/apiv1 +cloud.google.com/go@v0.94.0/iam/credentials/apiv1] + +*/ diff --git a/internal/modindex/directories.go b/internal/modindex/directories.go new file mode 100644 index 00000000000..b8aab3b736e --- /dev/null +++ b/internal/modindex/directories.go @@ -0,0 +1,137 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modindex + +import ( + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "sync" + "time" + + "golang.org/x/mod/semver" + "golang.org/x/tools/internal/gopathwalk" +) + +type directory struct { + path Relpath + importPath string + version string // semantic version +} + +// filterDirs groups the directories by import path, +// sorting the ones with the same import path by semantic version, +// most recent first. +func byImportPath(dirs []Relpath) (map[string][]*directory, error) { + ans := make(map[string][]*directory) // key is import path + for _, d := range dirs { + ip, sv, err := DirToImportPathVersion(d) + if err != nil { + return nil, err + } + ans[ip] = append(ans[ip], &directory{ + path: d, + importPath: ip, + version: sv, + }) + } + for k, v := range ans { + semanticSort(v) + ans[k] = v + } + return ans, nil +} + +// sort the directories by semantic version, lates first +func semanticSort(v []*directory) { + slices.SortFunc(v, func(l, r *directory) int { + if n := semver.Compare(l.version, r.version); n != 0 { + return -n // latest first + } + return strings.Compare(string(l.path), string(r.path)) + }) +} + +// modCacheRegexp splits a relpathpath into module, module version, and package. +var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`) + +// DirToImportPathVersion computes import path and semantic version +func DirToImportPathVersion(dir Relpath) (string, string, error) { + m := modCacheRegexp.FindStringSubmatch(string(dir)) + // m[1] is the module path + // m[2] is the version major.minor.patch(-
 that contains the name
+// of the current index. We believe writing that short file is atomic.
+// ReadIndex reads that file to get the file name of the index.
+// WriteIndex writes an index with a unique name and then
+// writes that name into a new version of index-name-.
+// ( stands for the CurrentVersion of the index format.)
+package modindex
+
+import (
+	"log"
+	"path/filepath"
+	"slices"
+	"strings"
+	"time"
+
+	"golang.org/x/mod/semver"
+)
+
+// Modindex writes an index current as of when it is called.
+// If clear is true the index is constructed from all of GOMODCACHE
+// otherwise the index is constructed from the last previous index
+// and the updates to the cache.
+func IndexModCache(cachedir string, clear bool) error {
+	cachedir, err := filepath.Abs(cachedir)
+	if err != nil {
+		return err
+	}
+	cd := Abspath(cachedir)
+	future := time.Now().Add(24 * time.Hour) // safely in the future
+	err = modindexTimed(future, cd, clear)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// modindexTimed writes an index current as of onlyBefore.
+// If clear is true the index is constructed from all of GOMODCACHE
+// otherwise the index is constructed from the last previous index
+// and all the updates to the cache before onlyBefore.
+// (this is useful for testing; perhaps it should not be exported)
+func modindexTimed(onlyBefore time.Time, cachedir Abspath, clear bool) error {
+	var curIndex *Index
+	if !clear {
+		var err error
+		curIndex, err = ReadIndex(string(cachedir))
+		if clear && err != nil {
+			return err
+		}
+		// TODO(pjw): check that most of those directorie still exist
+	}
+	cfg := &work{
+		onlyBefore: onlyBefore,
+		oldIndex:   curIndex,
+		cacheDir:   cachedir,
+	}
+	if curIndex != nil {
+		cfg.onlyAfter = curIndex.Changed
+	}
+	if err := cfg.buildIndex(); err != nil {
+		return err
+	}
+	if err := cfg.writeIndex(); err != nil {
+		return err
+	}
+	return nil
+}
+
+type work struct {
+	onlyBefore time.Time // do not use directories later than this
+	onlyAfter  time.Time // only interested in directories after this
+	// directories from before onlyAfter come from oldIndex
+	oldIndex *Index
+	newIndex *Index
+	cacheDir Abspath
+}
+
+func (w *work) buildIndex() error {
+	// The effective date of the new index should be at least
+	// slightly earlier than when the directories are scanned
+	// so set it now.
+	w.newIndex = &Index{Changed: time.Now(), Cachedir: w.cacheDir}
+	dirs := findDirs(string(w.cacheDir), w.onlyAfter, w.onlyBefore)
+	newdirs, err := byImportPath(dirs)
+	if err != nil {
+		return err
+	}
+	log.Printf("%d dirs, %d ips", len(dirs), len(newdirs))
+	// for each import path it might occur only in newdirs,
+	// only in w.oldIndex, or in both.
+	// If it occurs in both, use the semantically later one
+	if w.oldIndex != nil {
+		killed := 0
+		for _, e := range w.oldIndex.Entries {
+			found, ok := newdirs[e.ImportPath]
+			if !ok {
+				continue
+			}
+			if semver.Compare(found[0].version, e.Version) > 0 {
+				// the new one is better, disable the old one
+				e.ImportPath = ""
+				killed++
+			} else {
+				// use the old one, forget the new one
+				delete(newdirs, e.ImportPath)
+			}
+		}
+		log.Printf("%d killed, %d ips", killed, len(newdirs))
+	}
+	// Build the skeleton of the new index using newdirs,
+	// and include the surviving parts of the old index
+	if w.oldIndex != nil {
+		for _, e := range w.oldIndex.Entries {
+			if e.ImportPath != "" {
+				w.newIndex.Entries = append(w.newIndex.Entries, e)
+			}
+		}
+	}
+	for k, v := range newdirs {
+		d := v[0]
+		entry := Entry{
+			Dir:        d.path,
+			ImportPath: k,
+			Version:    d.version,
+		}
+		w.newIndex.Entries = append(w.newIndex.Entries, entry)
+	}
+	// find symbols for the incomplete entries
+	log.Print("not finding any symbols yet")
+	// sort the entries in the new index
+	slices.SortFunc(w.newIndex.Entries, func(l, r Entry) int {
+		if n := strings.Compare(l.PkgName, r.PkgName); n != 0 {
+			return n
+		}
+		return strings.Compare(l.ImportPath, r.ImportPath)
+	})
+	return nil
+}
+
+func (w *work) writeIndex() error {
+	return writeIndex(w.cacheDir, w.newIndex)
+}
diff --git a/internal/modindex/types.go b/internal/modindex/types.go
new file mode 100644
index 00000000000..ece44886309
--- /dev/null
+++ b/internal/modindex/types.go
@@ -0,0 +1,25 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package modindex
+
+import (
+	"strings"
+)
+
+// some special types to avoid confusions
+
+// distinguish various types of directory names. It's easy to get confused.
+type Abspath string // absolute paths
+type Relpath string // paths with GOMODCACHE prefix removed
+
+func toRelpath(cachedir Abspath, s string) Relpath {
+	if strings.HasPrefix(s, string(cachedir)) {
+		if s == string(cachedir) {
+			return Relpath("")
+		}
+		return Relpath(s[len(cachedir)+1:])
+	}
+	return Relpath(s)
+}
diff --git a/internal/proxydir/proxydir.go b/internal/proxydir/proxydir.go
index ffec81c264c..dc6b6ae94e8 100644
--- a/internal/proxydir/proxydir.go
+++ b/internal/proxydir/proxydir.go
@@ -14,8 +14,6 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
-
-	"golang.org/x/tools/internal/testenv"
 )
 
 // WriteModuleVersion creates a directory in the proxy dir for a module.
@@ -82,18 +80,10 @@ func checkClose(name string, closer io.Closer, err *error) {
 
 // ToURL returns the file uri for a proxy directory.
 func ToURL(dir string) string {
-	if testenv.Go1Point() >= 13 {
-		// file URLs on Windows must start with file:///. See golang.org/issue/6027.
-		path := filepath.ToSlash(dir)
-		if !strings.HasPrefix(path, "/") {
-			path = "/" + path
-		}
-		return "file://" + path
-	} else {
-		// Prior to go1.13, the Go command on Windows only accepted GOPROXY file URLs
-		// of the form file://C:/path/to/proxy. This was incorrect: when parsed, "C:"
-		// is interpreted as the host. See golang.org/issue/6027. This has been
-		// fixed in go1.13, but we emit the old format for old releases.
-		return "file://" + filepath.ToSlash(dir)
+	// file URLs on Windows must start with file:///. See golang.org/issue/6027.
+	path := filepath.ToSlash(dir)
+	if !strings.HasPrefix(path, "/") {
+		path = "/" + path
 	}
+	return "file://" + path
 }
diff --git a/internal/refactor/inline/analyzer/analyzer.go b/internal/refactor/inline/analyzer/analyzer.go
index 8f9d19e8368..f0519469b5e 100644
--- a/internal/refactor/inline/analyzer/analyzer.go
+++ b/internal/refactor/inline/analyzer/analyzer.go
@@ -2,8 +2,6 @@
 // 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 (
diff --git a/internal/refactor/inline/analyzer/analyzer_test.go b/internal/refactor/inline/analyzer/analyzer_test.go
index 05daac901f7..5ad85cfb821 100644
--- a/internal/refactor/inline/analyzer/analyzer_test.go
+++ b/internal/refactor/inline/analyzer/analyzer_test.go
@@ -2,8 +2,6 @@
 // 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/callee.go b/internal/refactor/inline/callee.go
index 09deda33b5e..c72699cb772 100644
--- a/internal/refactor/inline/callee.go
+++ b/internal/refactor/inline/callee.go
@@ -16,7 +16,6 @@ import (
 	"go/types"
 	"strings"
 
-	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/types/typeutil"
 	"golang.org/x/tools/internal/typeparams"
 )
@@ -242,7 +241,7 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa
 		// not just a return statement
 	} else if ret, ok := decl.Body.List[0].(*ast.ReturnStmt); ok && len(ret.Results) == 1 {
 		validForCallStmt = func() bool {
-			switch expr := astutil.Unparen(ret.Results[0]).(type) {
+			switch expr := ast.Unparen(ret.Results[0]).(type) {
 			case *ast.CallExpr: // f(x)
 				callee := typeutil.Callee(info, expr)
 				if callee == nil {
diff --git a/internal/refactor/inline/falcon.go b/internal/refactor/inline/falcon.go
index de054342be3..9154c5093fb 100644
--- a/internal/refactor/inline/falcon.go
+++ b/internal/refactor/inline/falcon.go
@@ -17,7 +17,6 @@ import (
 	"strings"
 
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/aliases"
 	"golang.org/x/tools/internal/typeparams"
 )
 
@@ -423,7 +422,7 @@ func (st *falconState) expr(e ast.Expr) (res any) { // = types.TypeAndValue | as
 		if e.Type != nil {
 			_ = st.expr(e.Type)
 		}
-		t := aliases.Unalias(typeparams.Deref(tv.Type))
+		t := types.Unalias(typeparams.Deref(tv.Type))
 		var uniques []ast.Expr
 		for _, elt := range e.Elts {
 			if kv, ok := elt.(*ast.KeyValueExpr); ok {
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
index da2d26e8d4d..0fda1c579f9 100644
--- a/internal/refactor/inline/inline.go
+++ b/internal/refactor/inline/inline.go
@@ -15,6 +15,7 @@ import (
 	"go/types"
 	pathpkg "path"
 	"reflect"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -151,7 +152,7 @@ func (st *state) inline() (*Result, error) {
 	elideBraces := res.elideBraces
 	if !elideBraces {
 		if newBlock, ok := res.new.(*ast.BlockStmt); ok {
-			i := nodeIndex(caller.path, res.old)
+			i := slices.Index(caller.path, res.old)
 			parent := caller.path[i+1]
 			var body []ast.Stmt
 			switch parent := parent.(type) {
@@ -527,7 +528,7 @@ func (st *state) inlineCall() (*inlineCallResult, error) {
 			// or time.Second.String()) will remain after
 			// inlining, as arguments.
 			if pkgName, ok := existing.(*types.PkgName); ok {
-				if sel, ok := astutil.Unparen(caller.Call.Fun).(*ast.SelectorExpr); ok {
+				if sel, ok := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr); ok {
 					if sole := soleUse(caller.Info, pkgName); sole == sel.X {
 						for _, spec := range caller.File.Imports {
 							pkgName2, ok := importedPkgName(caller.Info, spec)
@@ -1263,7 +1264,7 @@ func (st *state) arguments(caller *Caller, calleeDecl *ast.FuncDecl, assign1 fun
 
 	callArgs := caller.Call.Args
 	if calleeDecl.Recv != nil {
-		sel := astutil.Unparen(caller.Call.Fun).(*ast.SelectorExpr)
+		sel := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr)
 		seln := caller.Info.Selections[sel]
 		var recvArg ast.Expr
 		switch seln.Kind() {
@@ -2227,7 +2228,7 @@ func pure(info *types.Info, assign1 func(*types.Var) bool, e ast.Expr) bool {
 // 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 id, ok := ast.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":
@@ -2465,7 +2466,7 @@ func callStmt(callPath []ast.Node, unrestricted bool) *ast.ExprStmt {
 	parent, _ := callContext(callPath)
 	stmt, ok := parent.(*ast.ExprStmt)
 	if ok && unrestricted {
-		switch callPath[nodeIndex(callPath, stmt)+1].(type) {
+		switch callPath[slices.Index(callPath, ast.Node(stmt))+1].(type) {
 		case *ast.LabeledStmt,
 			*ast.BlockStmt,
 			*ast.CaseClause,
@@ -2775,7 +2776,7 @@ func consistentOffsets(caller *Caller) bool {
 // 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)
+	i := slices.Index(callPath, old)
 	if i == -1 {
 		panic("not found")
 	}
@@ -2837,16 +2838,6 @@ func needsParens(callPath []ast.Node, old, new ast.Node) bool {
 	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.)
diff --git a/internal/testfiles/testfiles.go b/internal/testfiles/testfiles.go
index ec5fc7f6920..78733976b3b 100644
--- a/internal/testfiles/testfiles.go
+++ b/internal/testfiles/testfiles.go
@@ -14,6 +14,8 @@ import (
 	"strings"
 	"testing"
 
+	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/testenv"
 	"golang.org/x/tools/txtar"
 )
 
@@ -104,3 +106,42 @@ func ExtractTxtarFileToTmp(t testing.TB, file string) string {
 	}
 	return CopyToTmp(t, fs)
 }
+
+// LoadPackages loads typed syntax for all packages that match the
+// patterns, interpreted relative to the archive root.
+//
+// The packages must be error-free.
+func LoadPackages(t testing.TB, ar *txtar.Archive, patterns ...string) []*packages.Package {
+	testenv.NeedsGoPackages(t)
+
+	fs, err := txtar.FS(ar)
+	if err != nil {
+		t.Fatal(err)
+	}
+	dir := CopyToTmp(t, fs)
+
+	cfg := &packages.Config{
+		Mode: packages.NeedSyntax |
+			packages.NeedTypesInfo |
+			packages.NeedDeps |
+			packages.NeedName |
+			packages.NeedFiles |
+			packages.NeedImports |
+			packages.NeedCompiledGoFiles |
+			packages.NeedTypes,
+		Dir: dir,
+		Env: append(os.Environ(),
+			"GO111MODULES=on",
+			"GOPATH=",
+			"GOWORK=off",
+			"GOPROXY=off"),
+	}
+	pkgs, err := packages.Load(cfg, patterns...)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if num := packages.PrintErrors(pkgs); num > 0 {
+		t.Fatalf("packages contained %d errors", num)
+	}
+	return pkgs
+}
diff --git a/internal/tokeninternal/tokeninternal.go b/internal/tokeninternal/tokeninternal.go
index ff9437a36cd..0a73e2ebda3 100644
--- a/internal/tokeninternal/tokeninternal.go
+++ b/internal/tokeninternal/tokeninternal.go
@@ -11,41 +11,10 @@ import (
 	"go/token"
 	"sort"
 	"sync"
+	"sync/atomic"
 	"unsafe"
 )
 
-// GetLines returns the table of line-start offsets from a token.File.
-func GetLines(file *token.File) []int {
-	// token.File has a Lines method on Go 1.21 and later.
-	if file, ok := (interface{})(file).(interface{ Lines() []int }); ok {
-		return file.Lines()
-	}
-
-	// This declaration must match that of token.File.
-	// This creates a risk of dependency skew.
-	// For now we check that the size of the two
-	// declarations is the same, on the (fragile) assumption
-	// that future changes would add fields.
-	type tokenFile119 struct {
-		_     string
-		_     int
-		_     int
-		mu    sync.Mutex // we're not complete monsters
-		lines []int
-		_     []struct{}
-	}
-
-	if unsafe.Sizeof(*file) != unsafe.Sizeof(tokenFile119{}) {
-		panic("unexpected token.File size")
-	}
-	var ptr *tokenFile119
-	type uP = unsafe.Pointer
-	*(*uP)(uP(&ptr)) = uP(file)
-	ptr.mu.Lock()
-	defer ptr.mu.Unlock()
-	return ptr.lines
-}
-
 // AddExistingFiles adds the specified files to the FileSet if they
 // are not already present. It panics if any pair of files in the
 // resulting FileSet would overlap.
@@ -56,7 +25,7 @@ func AddExistingFiles(fset *token.FileSet, files []*token.File) {
 		mutex sync.RWMutex
 		base  int
 		files []*token.File
-		_     *token.File // changed to atomic.Pointer[token.File] in go1.19
+		_     atomic.Pointer[token.File]
 	}
 
 	// If the size of token.FileSet changes, this will fail to compile.
@@ -116,8 +85,7 @@ func FileSetFor(files ...*token.File) *token.FileSet {
 	fset := token.NewFileSet()
 	for _, f := range files {
 		f2 := fset.AddFile(f.Name(), f.Base(), f.Size())
-		lines := GetLines(f)
-		f2.SetLines(lines)
+		f2.SetLines(f.Lines())
 	}
 	return fset
 }
diff --git a/internal/tool/tool.go b/internal/tool/tool.go
index eadb0fb5ab9..46f5b87fa35 100644
--- a/internal/tool/tool.go
+++ b/internal/tool/tool.go
@@ -45,6 +45,7 @@ type Profile struct {
 	Memory string `flag:"profile.mem" help:"write memory profile to this file"`
 	Alloc  string `flag:"profile.alloc" help:"write alloc profile to this file"`
 	Trace  string `flag:"profile.trace" help:"write trace log to this file"`
+	Block  string `flag:"profile.block" help:"write block profile to this file"`
 }
 
 // Application is the interface that must be satisfied by an object passed to Main.
@@ -172,7 +173,9 @@ func Run(ctx context.Context, s *flag.FlagSet, app Application, args []string) (
 			if err := pprof.WriteHeapProfile(f); err != nil {
 				log.Printf("Writing memory profile: %v", err)
 			}
-			f.Close()
+			if err := f.Close(); err != nil {
+				log.Printf("Closing memory profile: %v", err)
+			}
 		}()
 	}
 
@@ -185,7 +188,25 @@ func Run(ctx context.Context, s *flag.FlagSet, app Application, args []string) (
 			if err := pprof.Lookup("allocs").WriteTo(f, 0); err != nil {
 				log.Printf("Writing alloc profile: %v", err)
 			}
-			f.Close()
+			if err := f.Close(); err != nil {
+				log.Printf("Closing alloc profile: %v", err)
+			}
+		}()
+	}
+
+	if p != nil && p.Block != "" {
+		f, err := os.Create(p.Block)
+		if err != nil {
+			return err
+		}
+		runtime.SetBlockProfileRate(1) // record all blocking events
+		defer func() {
+			if err := pprof.Lookup("block").WriteTo(f, 0); err != nil {
+				log.Printf("Writing block profile: %v", err)
+			}
+			if err := f.Close(); err != nil {
+				log.Printf("Closing block profile: %v", err)
+			}
 		}()
 	}
 
diff --git a/internal/typeparams/common.go b/internal/typeparams/common.go
index 89bd256dc67..0b84acc5c7f 100644
--- a/internal/typeparams/common.go
+++ b/internal/typeparams/common.go
@@ -16,8 +16,6 @@ import (
 	"go/ast"
 	"go/token"
 	"go/types"
-
-	"golang.org/x/tools/internal/aliases"
 )
 
 // UnpackIndexExpr extracts data from AST nodes that represent index
@@ -65,7 +63,7 @@ func PackIndexExpr(x ast.Expr, lbrack token.Pos, indices []ast.Expr, rbrack toke
 
 // IsTypeParam reports whether t is a type parameter (or an alias of one).
 func IsTypeParam(t types.Type) bool {
-	_, ok := aliases.Unalias(t).(*types.TypeParam)
+	_, ok := types.Unalias(t).(*types.TypeParam)
 	return ok
 }
 
@@ -93,8 +91,8 @@ func IsTypeParam(t types.Type) bool {
 // In this case, GenericAssignableTo reports that instantiations of Container
 // are assignable to the corresponding instantiation of Interface.
 func GenericAssignableTo(ctxt *types.Context, V, T types.Type) bool {
-	V = aliases.Unalias(V)
-	T = aliases.Unalias(T)
+	V = types.Unalias(V)
+	T = types.Unalias(T)
 
 	// If V and T are not both named, or do not have matching non-empty type
 	// parameter lists, fall back on types.AssignableTo.
diff --git a/internal/typeparams/free.go b/internal/typeparams/free.go
index a1d138226c9..358108268b4 100644
--- a/internal/typeparams/free.go
+++ b/internal/typeparams/free.go
@@ -6,8 +6,6 @@ package typeparams
 
 import (
 	"go/types"
-
-	"golang.org/x/tools/internal/aliases"
 )
 
 // Free is a memoization of the set of free type parameters within a
@@ -37,8 +35,8 @@ func (w *Free) Has(typ types.Type) (res bool) {
 	case nil, *types.Basic: // TODO(gri) should nil be handled here?
 		break
 
-	case *aliases.Alias:
-		return w.Has(aliases.Unalias(t))
+	case *types.Alias:
+		return w.Has(types.Unalias(t))
 
 	case *types.Array:
 		return w.Has(t.Elem())
diff --git a/internal/typeparams/genericfeatures/features.go b/internal/typeparams/genericfeatures/features.go
index e7d0e0e6112..236d4bb9b56 100644
--- a/internal/typeparams/genericfeatures/features.go
+++ b/internal/typeparams/genericfeatures/features.go
@@ -12,7 +12,6 @@ import (
 	"strings"
 
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/aliases"
 )
 
 // Features is a set of flags reporting which features of generic Go code a
@@ -93,7 +92,7 @@ func ForPackage(inspect *inspector.Inspector, info *types.Info) Features {
 	})
 
 	for _, inst := range info.Instances {
-		switch aliases.Unalias(inst.Type).(type) {
+		switch types.Unalias(inst.Type).(type) {
 		case *types.Named:
 			direct |= TypeInstantiation
 		case *types.Signature:
diff --git a/internal/typesinternal/element.go b/internal/typesinternal/element.go
new file mode 100644
index 00000000000..4957f021641
--- /dev/null
+++ b/internal/typesinternal/element.go
@@ -0,0 +1,133 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package typesinternal
+
+import (
+	"fmt"
+	"go/types"
+
+	"golang.org/x/tools/go/types/typeutil"
+)
+
+// ForEachElement calls f for type T and each type reachable from its
+// type through reflection. It does this by recursively stripping off
+// type constructors; in addition, for each named type N, the type *N
+// is added to the result as it may have additional methods.
+//
+// The caller must provide an initially empty set used to de-duplicate
+// identical types, potentially across multiple calls to ForEachElement.
+// (Its final value holds all the elements seen, matching the arguments
+// passed to f.)
+//
+// TODO(adonovan): share/harmonize with go/callgraph/rta.
+func ForEachElement(rtypes *typeutil.Map, msets *typeutil.MethodSetCache, T types.Type, f func(types.Type)) {
+	var visit func(T types.Type, skip bool)
+	visit = func(T types.Type, skip bool) {
+		if !skip {
+			if seen, _ := rtypes.Set(T, true).(bool); seen {
+				return // de-dup
+			}
+
+			f(T) // notify caller of new element type
+		}
+
+		// Recursion over signatures of each method.
+		tmset := msets.MethodSet(T)
+		for i := 0; i < tmset.Len(); i++ {
+			sig := tmset.At(i).Type().(*types.Signature)
+			// It is tempting to call visit(sig, false)
+			// but, as noted in golang.org/cl/65450043,
+			// the Signature.Recv field is ignored by
+			// types.Identical and typeutil.Map, which
+			// is confusing at best.
+			//
+			// More importantly, the true signature rtype
+			// reachable from a method using reflection
+			// has no receiver but an extra ordinary parameter.
+			// For the Read method of io.Reader we want:
+			//   func(Reader, []byte) (int, error)
+			// but here sig is:
+			//   func([]byte) (int, error)
+			// with .Recv = Reader (though it is hard to
+			// notice because it doesn't affect Signature.String
+			// or types.Identical).
+			//
+			// TODO(adonovan): construct and visit the correct
+			// non-method signature with an extra parameter
+			// (though since unnamed func types have no methods
+			// there is essentially no actual demand for this).
+			//
+			// TODO(adonovan): document whether or not it is
+			// safe to skip non-exported methods (as RTA does).
+			visit(sig.Params(), true)  // skip the Tuple
+			visit(sig.Results(), true) // skip the Tuple
+		}
+
+		switch T := T.(type) {
+		case *types.Alias:
+			visit(types.Unalias(T), skip) // emulates the pre-Alias behavior
+
+		case *types.Basic:
+			// nop
+
+		case *types.Interface:
+			// nop---handled by recursion over method set.
+
+		case *types.Pointer:
+			visit(T.Elem(), false)
+
+		case *types.Slice:
+			visit(T.Elem(), false)
+
+		case *types.Chan:
+			visit(T.Elem(), false)
+
+		case *types.Map:
+			visit(T.Key(), false)
+			visit(T.Elem(), false)
+
+		case *types.Signature:
+			if T.Recv() != nil {
+				panic(fmt.Sprintf("Signature %s has Recv %s", T, T.Recv()))
+			}
+			visit(T.Params(), true)  // skip the Tuple
+			visit(T.Results(), true) // skip the Tuple
+
+		case *types.Named:
+			// A pointer-to-named type can be derived from a named
+			// type via reflection.  It may have methods too.
+			visit(types.NewPointer(T), false)
+
+			// Consider 'type T struct{S}' where S has methods.
+			// Reflection provides no way to get from T to struct{S},
+			// only to S, so the method set of struct{S} is unwanted,
+			// so set 'skip' flag during recursion.
+			visit(T.Underlying(), true) // skip the unnamed type
+
+		case *types.Array:
+			visit(T.Elem(), false)
+
+		case *types.Struct:
+			for i, n := 0, T.NumFields(); i < n; i++ {
+				// TODO(adonovan): document whether or not
+				// it is safe to skip non-exported fields.
+				visit(T.Field(i).Type(), false)
+			}
+
+		case *types.Tuple:
+			for i, n := 0, T.Len(); i < n; i++ {
+				visit(T.At(i).Type(), false)
+			}
+
+		case *types.TypeParam, *types.Union:
+			// forEachReachable must not be called on parameterized types.
+			panic(T)
+
+		default:
+			panic(T)
+		}
+	}
+	visit(T, false)
+}
diff --git a/internal/typesinternal/element_test.go b/internal/typesinternal/element_test.go
new file mode 100644
index 00000000000..b4475633270
--- /dev/null
+++ b/internal/typesinternal/element_test.go
@@ -0,0 +1,153 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package typesinternal_test
+
+import (
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"go/types"
+	"strings"
+	"testing"
+
+	"golang.org/x/tools/go/types/typeutil"
+	"golang.org/x/tools/internal/typesinternal"
+)
+
+const elementSrc = `
+package p
+
+type A = int
+
+type B = *map[chan int][]func() [2]bool
+
+type C = T
+
+type T struct{ x int }
+func (T) method() uint
+func (*T) ptrmethod() complex128
+
+type D = A
+
+type E = struct{ x int }
+
+type F = func(int8, int16) (int32, int64)
+
+type G = struct { U }
+
+type U struct{}
+func (U) method() uint32
+
+`
+
+func TestForEachElement(t *testing.T) {
+	fset := token.NewFileSet()
+	f, err := parser.ParseFile(fset, "a.go", elementSrc, 0)
+	if err != nil {
+		t.Fatal(err) // parse error
+	}
+	var config types.Config
+	pkg, err := config.Check(f.Name.Name, fset, []*ast.File{f}, nil)
+	if err != nil {
+		t.Fatal(err) // type error
+	}
+
+	tests := []struct {
+		name string   // name of a type alias whose RHS type's elements to compute
+		want []string // strings of types that are/are not elements (! => not)
+	}{
+		// simple type
+		{"A", []string{"int"}},
+
+		// compound type
+		{"B", []string{
+			"*map[chan int][]func() [2]bool",
+			"map[chan int][]func() [2]bool",
+			"chan int",
+			"int",
+			"[]func() [2]bool",
+			"func() [2]bool",
+			"[2]bool",
+			"bool",
+		}},
+
+		// defined struct type with methods, incl. pointer methods.
+		// Observe that it descends into the field type, but
+		// the result does not include the struct type itself.
+		// (This follows the Go toolchain behavior , and finesses the need
+		// to create wrapper methods for that struct type.)
+		{"C", []string{"T", "*T", "int", "uint", "complex128", "!struct{x int}"}},
+
+		// alias type
+		{"D", []string{"int"}},
+
+		// struct type not beneath a defined type
+		{"E", []string{"struct{x int}", "int"}},
+
+		// signature types: the params/results tuples
+		// are traversed but not included.
+		{"F", []string{"func(int8, int16) (int32, int64)",
+			"int8", "int16", "int32", "int64"}},
+
+		// struct with embedded field that has methods
+		{"G", []string{"*U", "struct{U}", "uint32", "U"}},
+	}
+	var msets typeutil.MethodSetCache
+	for _, test := range tests {
+		tname, ok := pkg.Scope().Lookup(test.name).(*types.TypeName)
+		if !ok {
+			t.Errorf("no such type %q", test.name)
+			continue
+		}
+		T := types.Unalias(tname.Type())
+
+		toStr := func(T types.Type) string {
+			return types.TypeString(T, func(*types.Package) string { return "" })
+		}
+
+		got := make(map[string]bool)
+		set := new(typeutil.Map)  // for de-duping
+		set2 := new(typeutil.Map) // for consistency check
+		typesinternal.ForEachElement(set, &msets, T, func(elem types.Type) {
+			got[toStr(elem)] = true
+			set2.Set(elem, true)
+		})
+
+		// Assert that set==set2, meaning f(x) was
+		// called for each x in the de-duping map.
+		if set.Len() != set2.Len() {
+			t.Errorf("ForEachElement called f %d times yet de-dup set has %d elements",
+				set2.Len(), set.Len())
+		} else {
+			set.Iterate(func(key types.Type, _ any) {
+				if set2.At(key) == nil {
+					t.Errorf("ForEachElement did not call f(%v)", key)
+				}
+			})
+		}
+
+		// Assert than all expected (and no unexpected) elements were found.
+		fail := false
+		for _, typstr := range test.want {
+			found := got[typstr]
+			typstr, unwanted := strings.CutPrefix(typstr, "!")
+			if found && unwanted {
+				fail = true
+				t.Errorf("ForEachElement(%s): unwanted element %q", T, typstr)
+			} else if !found && !unwanted {
+				fail = true
+				t.Errorf("ForEachElement(%s): element %q not found", T, typstr)
+			}
+		}
+		if fail {
+			for k := range got {
+				t.Logf("got element: %s", k)
+			}
+			// TODO(adonovan): use this when go1.23 is assured:
+			// t.Logf("got elements:\n%s",
+			// 	strings.Join(slices.Sorted(maps.Keys(got)), "\n"))
+		}
+	}
+}
diff --git a/internal/typesinternal/recv.go b/internal/typesinternal/recv.go
index fea7c8b75e8..ba6f4f4ebd5 100644
--- a/internal/typesinternal/recv.go
+++ b/internal/typesinternal/recv.go
@@ -6,8 +6,6 @@ package typesinternal
 
 import (
 	"go/types"
-
-	"golang.org/x/tools/internal/aliases"
 )
 
 // ReceiverNamed returns the named type (if any) associated with the
@@ -15,11 +13,11 @@ import (
 // It also reports whether a Pointer was present.
 func ReceiverNamed(recv *types.Var) (isPtr bool, named *types.Named) {
 	t := recv.Type()
-	if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok {
+	if ptr, ok := types.Unalias(t).(*types.Pointer); ok {
 		isPtr = true
 		t = ptr.Elem()
 	}
-	named, _ = aliases.Unalias(t).(*types.Named)
+	named, _ = types.Unalias(t).(*types.Named)
 	return
 }
 
@@ -36,7 +34,7 @@ func ReceiverNamed(recv *types.Var) (isPtr bool, named *types.Named) {
 // indirection from the type, regardless of named types (analogous to
 // a LOAD instruction).
 func Unpointer(t types.Type) types.Type {
-	if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok {
+	if ptr, ok := types.Unalias(t).(*types.Pointer); ok {
 		return ptr.Elem()
 	}
 	return t
diff --git a/internal/versions/toolchain.go b/internal/versions/toolchain.go
deleted file mode 100644
index 377bf7a53b4..00000000000
--- a/internal/versions/toolchain.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2024 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package versions
-
-// toolchain is maximum version (<1.22) that the go toolchain used
-// to build the current tool is known to support.
-//
-// When a tool is built with >=1.22, the value of toolchain is unused.
-//
-// x/tools does not support building with go <1.18. So we take this
-// as the minimum possible maximum.
-var toolchain string = Go1_18
diff --git a/internal/versions/toolchain_go119.go b/internal/versions/toolchain_go119.go
deleted file mode 100644
index f65beed9d83..00000000000
--- a/internal/versions/toolchain_go119.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2024 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build go1.19
-// +build go1.19
-
-package versions
-
-func init() {
-	if Compare(toolchain, Go1_19) < 0 {
-		toolchain = Go1_19
-	}
-}
diff --git a/internal/versions/toolchain_go120.go b/internal/versions/toolchain_go120.go
deleted file mode 100644
index 1a9efa126cd..00000000000
--- a/internal/versions/toolchain_go120.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2024 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build go1.20
-// +build go1.20
-
-package versions
-
-func init() {
-	if Compare(toolchain, Go1_20) < 0 {
-		toolchain = Go1_20
-	}
-}
diff --git a/internal/versions/toolchain_go121.go b/internal/versions/toolchain_go121.go
deleted file mode 100644
index b7ef216dfec..00000000000
--- a/internal/versions/toolchain_go121.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2024 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build go1.21
-// +build go1.21
-
-package versions
-
-func init() {
-	if Compare(toolchain, Go1_21) < 0 {
-		toolchain = Go1_21
-	}
-}
diff --git a/internal/versions/types.go b/internal/versions/types.go
index 562eef21fa2..f0bb0d15f03 100644
--- a/internal/versions/types.go
+++ b/internal/versions/types.go
@@ -5,15 +5,34 @@
 package versions
 
 import (
+	"go/ast"
 	"go/types"
 )
 
-// GoVersion returns the Go version of the type package.
-// It returns zero if no version can be determined.
-func GoVersion(pkg *types.Package) string {
-	// TODO(taking): x/tools can call GoVersion() [from 1.21] after 1.25.
-	if pkg, ok := any(pkg).(interface{ GoVersion() string }); ok {
-		return pkg.GoVersion()
+// FileVersion returns a file's Go version.
+// The reported version is an unknown Future version if a
+// version cannot be determined.
+func FileVersion(info *types.Info, file *ast.File) string {
+	// In tools built with Go >= 1.22, the Go version of a file
+	// follow a cascades of sources:
+	// 1) types.Info.FileVersion, which follows the cascade:
+	//   1.a) file version (ast.File.GoVersion),
+	//   1.b) the package version (types.Config.GoVersion), or
+	// 2) is some unknown Future version.
+	//
+	// File versions require a valid package version to be provided to types
+	// in Config.GoVersion. Config.GoVersion is either from the package's module
+	// or the toolchain (go run). This value should be provided by go/packages
+	// or unitchecker.Config.GoVersion.
+	if v := info.FileVersions[file]; IsValid(v) {
+		return v
 	}
-	return ""
+	// Note: we could instead return runtime.Version() [if valid].
+	// This would act as a max version on what a tool can support.
+	return Future
+}
+
+// InitFileVersions initializes info to record Go versions for Go files.
+func InitFileVersions(info *types.Info) {
+	info.FileVersions = make(map[*ast.File]string)
 }
diff --git a/internal/versions/types_go121.go b/internal/versions/types_go121.go
deleted file mode 100644
index b4345d3349e..00000000000
--- a/internal/versions/types_go121.go
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2023 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build !go1.22
-// +build !go1.22
-
-package versions
-
-import (
-	"go/ast"
-	"go/types"
-)
-
-// FileVersion returns a language version (<=1.21) derived from runtime.Version()
-// or an unknown future version.
-func FileVersion(info *types.Info, file *ast.File) string {
-	// In x/tools built with Go <= 1.21, we do not have Info.FileVersions
-	// available. We use a go version derived from the toolchain used to
-	// compile the tool by default.
-	// This will be <= go1.21. We take this as the maximum version that
-	// this tool can support.
-	//
-	// There are no features currently in x/tools that need to tell fine grained
-	// differences for versions <1.22.
-	return toolchain
-}
-
-// InitFileVersions is a noop when compiled with this Go version.
-func InitFileVersions(*types.Info) {}
diff --git a/internal/versions/types_go122.go b/internal/versions/types_go122.go
deleted file mode 100644
index aac5db62c98..00000000000
--- a/internal/versions/types_go122.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2023 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build go1.22
-// +build go1.22
-
-package versions
-
-import (
-	"go/ast"
-	"go/types"
-)
-
-// FileVersion returns a file's Go version.
-// The reported version is an unknown Future version if a
-// version cannot be determined.
-func FileVersion(info *types.Info, file *ast.File) string {
-	// In tools built with Go >= 1.22, the Go version of a file
-	// follow a cascades of sources:
-	// 1) types.Info.FileVersion, which follows the cascade:
-	//   1.a) file version (ast.File.GoVersion),
-	//   1.b) the package version (types.Config.GoVersion), or
-	// 2) is some unknown Future version.
-	//
-	// File versions require a valid package version to be provided to types
-	// in Config.GoVersion. Config.GoVersion is either from the package's module
-	// or the toolchain (go run). This value should be provided by go/packages
-	// or unitchecker.Config.GoVersion.
-	if v := info.FileVersions[file]; IsValid(v) {
-		return v
-	}
-	// Note: we could instead return runtime.Version() [if valid].
-	// This would act as a max version on what a tool can support.
-	return Future
-}
-
-// InitFileVersions initializes info to record Go versions for Go files.
-func InitFileVersions(info *types.Info) {
-	info.FileVersions = make(map[*ast.File]string)
-}
diff --git a/internal/versions/types_test.go b/internal/versions/types_test.go
index df3705cc7a9..377369ffed7 100644
--- a/internal/versions/types_test.go
+++ b/internal/versions/types_test.go
@@ -13,13 +13,10 @@ import (
 	"go/types"
 	"testing"
 
-	"golang.org/x/tools/internal/testenv"
 	"golang.org/x/tools/internal/versions"
 )
 
 func Test(t *testing.T) {
-	testenv.NeedsGo1Point(t, 22)
-
 	var contents = map[string]string{
 		"gobuild.go": `
 	//go:build go1.23
@@ -49,7 +46,7 @@ func Test(t *testing.T) {
 				files[i] = parse(t, fset, test.fname, contents[test.fname])
 			}
 			pkg, info := typeCheck(t, fset, files, item.goversion)
-			if got, want := versions.GoVersion(pkg), item.pversion; versions.Compare(got, want) != 0 {
+			if got, want := pkg.GoVersion(), item.pversion; versions.Compare(got, want) != 0 {
 				t.Errorf("GoVersion()=%q. expected %q", got, want)
 			}
 			if got := versions.FileVersion(info, nil); got != "" {
diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go
index dbc1c555d22..0886f8c80be 100644
--- a/internal/versions/versions_test.go
+++ b/internal/versions/versions_test.go
@@ -11,7 +11,6 @@ import (
 	"go/types"
 	"testing"
 
-	"golang.org/x/tools/internal/testenv"
 	"golang.org/x/tools/internal/versions"
 )
 
@@ -192,9 +191,7 @@ func TestBefore(t *testing.T) {
 	}
 }
 
-func TestFileVersions122(t *testing.T) {
-	testenv.NeedsGo1Point(t, 22)
-
+func TestFileVersions(t *testing.T) {
 	const source = `
 	package P
 	`
@@ -230,27 +227,3 @@ func TestFileVersions122(t *testing.T) {
 		}
 	}
 }
-
-func TestFileVersions121(t *testing.T) {
-	testenv.SkipAfterGo1Point(t, 21)
-
-	// If <1.22, info and file are ignored.
-	v := versions.FileVersion(nil, nil)
-	oneof := map[string]bool{
-		versions.Go1_18: true,
-		versions.Go1_19: true,
-		versions.Go1_20: true,
-		versions.Go1_21: true,
-	}
-	if !oneof[v] {
-		t.Errorf("FileVersion(...)=%q expected to be a known go version <1.22", v)
-	}
-
-	if versions.AtLeast(v, versions.Go1_22) {
-		t.Errorf("versions.AtLeast(%q, %q) expected to be false", v, versions.Go1_22)
-	}
-
-	if !versions.Before(v, versions.Go1_22) {
-		t.Errorf("versions.Before(%q, %q) expected to hold", v, versions.Go1_22)
-	}
-}
diff --git a/refactor/eg/eg_test.go b/refactor/eg/eg_test.go
index 36fd3add8a7..eb54f0b3f95 100644
--- a/refactor/eg/eg_test.go
+++ b/refactor/eg/eg_test.go
@@ -12,21 +12,22 @@ package eg_test
 import (
 	"bytes"
 	"flag"
-	"go/build"
+	"fmt"
 	"go/constant"
-	"go/parser"
-	"go/token"
+	"go/format"
 	"go/types"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"runtime"
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/go/loader"
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/testenv"
+	"golang.org/x/tools/internal/testfiles"
 	"golang.org/x/tools/refactor/eg"
+	"golang.org/x/tools/txtar"
 )
 
 // TODO(adonovan): more tests:
@@ -41,80 +42,64 @@ var (
 )
 
 func Test(t *testing.T) {
-	testenv.NeedsTool(t, "go")
+	testenv.NeedsGoPackages(t)
 
 	switch runtime.GOOS {
 	case "windows":
 		t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
 	}
 
-	ctx := build.Default   // copy
-	ctx.CgoEnabled = false // don't use cgo
-	conf := loader.Config{
-		Fset:       token.NewFileSet(),
-		ParserMode: parser.ParseComments,
-		Build:      &ctx,
-	}
-
-	// Each entry is a single-file package.
-	// (Multi-file packages aren't interesting for this test.)
-	// Order matters: each non-template package is processed using
-	// the preceding template package.
+	// Each txtar defines a package example.com/template and zero
+	// or more input packages example.com/in/... on which to apply
+	// it. The outputs are compared with the corresponding files
+	// in example.com/out/...
 	for _, filename := range []string{
-		"testdata/A.template",
-		"testdata/A1.go",
-		"testdata/A2.go",
-
-		"testdata/B.template",
-		"testdata/B1.go",
-
-		"testdata/C.template",
-		"testdata/C1.go",
-
-		"testdata/D.template",
-		"testdata/D1.go",
-
-		"testdata/E.template",
-		"testdata/E1.go",
-
-		"testdata/F.template",
-		"testdata/F1.go",
-
-		"testdata/G.template",
-		"testdata/G1.go",
-
-		"testdata/H.template",
-		"testdata/H1.go",
-
-		"testdata/I.template",
-		"testdata/I1.go",
-
-		"testdata/J.template",
-		"testdata/J1.go",
-
-		"testdata/bad_type.template",
-		"testdata/no_before.template",
-		"testdata/no_after_return.template",
-		"testdata/type_mismatch.template",
-		"testdata/expr_type_mismatch.template",
+		"testdata/a.txtar",
+		"testdata/b.txtar",
+		"testdata/c.txtar",
+		"testdata/d.txtar",
+		"testdata/e.txtar",
+		"testdata/f.txtar",
+		"testdata/g.txtar",
+		"testdata/h.txtar",
+		"testdata/i.txtar",
+		"testdata/j.txtar",
+		"testdata/bad_type.txtar",
+		"testdata/no_before.txtar",
+		"testdata/no_after_return.txtar",
+		"testdata/type_mismatch.txtar",
+		"testdata/expr_type_mismatch.txtar",
 	} {
-		pkgname := strings.TrimSuffix(filepath.Base(filename), ".go")
-		conf.CreateFromFilenames(pkgname, filename)
-	}
-	iprog, err := conf.Load()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	var xform *eg.Transformer
-	for _, info := range iprog.Created {
-		file := info.Files[0]
-		filename := iprog.Fset.File(file.Pos()).Name() // foo.go
+		t.Run(filename, func(t *testing.T) {
+			// Extract and load packages from test archive.
+			dir := testfiles.ExtractTxtarFileToTmp(t, filename)
+			cfg := packages.Config{
+				Mode: packages.LoadAllSyntax,
+				Dir:  dir,
+			}
+			pkgs, err := packages.Load(&cfg, "example.com/template", "example.com/in/...")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if packages.PrintErrors(pkgs) > 0 {
+				t.Fatal("Load: there were errors")
+			}
 
-		if strings.HasSuffix(filename, "template") {
-			// a new template
-			shouldFail, _ := info.Pkg.Scope().Lookup("shouldFail").(*types.Const)
-			xform, err = eg.NewTransformer(iprog.Fset, info.Pkg, file, &info.Info, *verboseFlag)
+			// Find and compile the template.
+			var template *packages.Package
+			var inputs []*packages.Package
+			for _, pkg := range pkgs {
+				if pkg.Types.Name() == "template" {
+					template = pkg
+				} else {
+					inputs = append(inputs, pkg)
+				}
+			}
+			if template == nil {
+				t.Fatal("no template package")
+			}
+			shouldFail, _ := template.Types.Scope().Lookup("shouldFail").(*types.Const)
+			xform, err := eg.NewTransformer(template.Fset, template.Types, template.Syntax[0], template.TypesInfo, *verboseFlag)
 			if err != nil {
 				if shouldFail == nil {
 					t.Errorf("NewTransformer(%s): %s", filename, err)
@@ -125,55 +110,67 @@ func Test(t *testing.T) {
 				t.Errorf("NewTransformer(%s) succeeded unexpectedly; want error %q",
 					filename, shouldFail.Val())
 			}
-			continue
-		}
-
-		if xform == nil {
-			t.Errorf("%s: no previous template", filename)
-			continue
-		}
-
-		// apply previous template to this package
-		n := xform.Transform(&info.Info, info.Pkg, file)
-		if n == 0 {
-			t.Errorf("%s: no matches", filename)
-			continue
-		}
-
-		gotf, err := os.CreateTemp("", filepath.Base(filename)+"t")
-		if err != nil {
-			t.Fatal(err)
-		}
-		got := gotf.Name()          // foo.got
-		golden := filename + "lden" // foo.golden
-
-		// Write actual output to foo.got.
-		if err := eg.WriteAST(iprog.Fset, got, file); err != nil {
-			t.Error(err)
-		}
-		defer os.Remove(got)
-
-		// Compare foo.got with foo.golden.
-		var cmd *exec.Cmd
-		switch runtime.GOOS {
-		case "plan9":
-			cmd = exec.Command("/bin/diff", "-c", golden, got)
-		default:
-			cmd = exec.Command("/usr/bin/diff", "-u", golden, got)
-		}
-		buf := new(bytes.Buffer)
-		cmd.Stdout = buf
-		cmd.Stderr = os.Stderr
-		if err := cmd.Run(); err != nil {
-			t.Errorf("eg tests for %s failed: %s.\n%s\n", filename, err, buf)
 
+			// Apply template to each input package.
+			updated := make(map[string][]byte)
+			for _, pkg := range inputs {
+				for _, file := range pkg.Syntax {
+					filename, err := filepath.Rel(dir, pkg.Fset.File(file.FileStart).Name())
+					if err != nil {
+						t.Fatalf("can't relativize filename: %v", err)
+					}
+
+					// Apply the transform and reformat.
+					n := xform.Transform(pkg.TypesInfo, pkg.Types, file)
+					if n == 0 {
+						t.Fatalf("%s: no replacements", filename)
+					}
+					var got []byte
+					{
+						var out bytes.Buffer
+						format.Node(&out, pkg.Fset, file) // ignore error
+						got = out.Bytes()
+					}
+
+					// Compare formatted output with out/
+					// Errors here are not fatal, so we can proceed to -update.
+					outfile := strings.Replace(filename, "in", "out", 1)
+					updated[outfile] = got
+					want, err := os.ReadFile(filepath.Join(dir, outfile))
+					if err != nil {
+						t.Errorf("can't read output file: %v", err)
+					} else if diff := cmp.Diff(want, got); diff != "" {
+						t.Errorf("Unexpected output:\n%s\n\ngot %s:\n%s\n\nwant %s:\n%s",
+							diff,
+							filename, got, outfile, want)
+					}
+				}
+			}
+
+			// -update: replace the .txtar.
 			if *updateFlag {
-				t.Logf("Updating %s...", golden)
-				if err := exec.Command("/bin/cp", got, golden).Run(); err != nil {
-					t.Errorf("Update failed: %s", err)
+				ar, err := txtar.ParseFile(filename)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				var new bytes.Buffer
+				new.Write(ar.Comment)
+				for _, file := range ar.Files {
+					data, ok := updated[file.Name]
+					if !ok {
+						data = file.Data
+					}
+					fmt.Fprintf(&new, "-- %s --\n%s", file.Name, data)
+				}
+				t.Logf("Updating %s...", filename)
+				os.Remove(filename + ".bak")         // ignore error
+				os.Rename(filename, filename+".bak") // ignore error
+				if err := os.WriteFile(filename, new.Bytes(), 0666); err != nil {
+					t.Fatal(err)
 				}
 			}
-		}
+		})
 	}
 }
 
diff --git a/refactor/eg/testdata/A.template b/refactor/eg/testdata/A.template
deleted file mode 100644
index 6a23f12f61e..00000000000
--- a/refactor/eg/testdata/A.template
+++ /dev/null
@@ -1,11 +0,0 @@
-package template
-
-// Basic test of type-aware expression refactoring.
-
-import (
-	"errors"
-	"fmt"
-)
-
-func before(s string) error { return fmt.Errorf("%s", s) }
-func after(s string) error  { return errors.New(s) }
diff --git a/refactor/eg/testdata/A1.go b/refactor/eg/testdata/A1.go
deleted file mode 100644
index c64fd800b31..00000000000
--- a/refactor/eg/testdata/A1.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package A1
-
-import (
-	. "fmt"
-	myfmt "fmt"
-	"os"
-	"strings"
-)
-
-func example(n int) {
-	x := "foo" + strings.Repeat("\t", n)
-	// Match, despite named import.
-	myfmt.Errorf("%s", x)
-
-	// Match, despite dot import.
-	Errorf("%s", x)
-
-	// Match: multiple matches in same function are possible.
-	myfmt.Errorf("%s", x)
-
-	// No match: wildcarded operand has the wrong type.
-	myfmt.Errorf("%s", 3)
-
-	// No match: function operand doesn't match.
-	myfmt.Printf("%s", x)
-
-	// No match again, dot import.
-	Printf("%s", x)
-
-	// Match.
-	myfmt.Fprint(os.Stderr, myfmt.Errorf("%s", x+"foo"))
-
-	// No match: though this literally matches the template,
-	// fmt doesn't resolve to a package here.
-	var fmt struct{ Errorf func(string, string) }
-	fmt.Errorf("%s", x)
-
-	// Recursive matching:
-
-	// Match: both matches are well-typed, so both succeed.
-	myfmt.Errorf("%s", myfmt.Errorf("%s", x+"foo").Error())
-
-	// Outer match succeeds, inner doesn't: 3 has wrong type.
-	myfmt.Errorf("%s", myfmt.Errorf("%s", 3).Error())
-
-	// Inner match succeeds, outer doesn't: the inner replacement
-	// has the wrong type (error not string).
-	myfmt.Errorf("%s", myfmt.Errorf("%s", x+"foo"))
-}
diff --git a/refactor/eg/testdata/A1.golden b/refactor/eg/testdata/A1.golden
deleted file mode 100644
index a8aeb068999..00000000000
--- a/refactor/eg/testdata/A1.golden
+++ /dev/null
@@ -1,50 +0,0 @@
-package A1
-
-import (
-	"errors"
-	. "fmt"
-	myfmt "fmt"
-	"os"
-	"strings"
-)
-
-func example(n int) {
-	x := "foo" + strings.Repeat("\t", n)
-	// Match, despite named import.
-	errors.New(x)
-
-	// Match, despite dot import.
-	errors.New(x)
-
-	// Match: multiple matches in same function are possible.
-	errors.New(x)
-
-	// No match: wildcarded operand has the wrong type.
-	myfmt.Errorf("%s", 3)
-
-	// No match: function operand doesn't match.
-	myfmt.Printf("%s", x)
-
-	// No match again, dot import.
-	Printf("%s", x)
-
-	// Match.
-	myfmt.Fprint(os.Stderr, errors.New(x+"foo"))
-
-	// No match: though this literally matches the template,
-	// fmt doesn't resolve to a package here.
-	var fmt struct{ Errorf func(string, string) }
-	fmt.Errorf("%s", x)
-
-	// Recursive matching:
-
-	// Match: both matches are well-typed, so both succeed.
-	errors.New(errors.New(x + "foo").Error())
-
-	// Outer match succeeds, inner doesn't: 3 has wrong type.
-	errors.New(myfmt.Errorf("%s", 3).Error())
-
-	// Inner match succeeds, outer doesn't: the inner replacement
-	// has the wrong type (error not string).
-	myfmt.Errorf("%s", errors.New(x+"foo"))
-}
diff --git a/refactor/eg/testdata/A2.go b/refactor/eg/testdata/A2.go
deleted file mode 100644
index 2fab7904001..00000000000
--- a/refactor/eg/testdata/A2.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package A2
-
-// This refactoring causes addition of "errors" import.
-// TODO(adonovan): fix: it should also remove "fmt".
-
-import myfmt "fmt"
-
-func example(n int) {
-	myfmt.Errorf("%s", "")
-}
diff --git a/refactor/eg/testdata/A2.golden b/refactor/eg/testdata/A2.golden
deleted file mode 100644
index 0e4ca447bc4..00000000000
--- a/refactor/eg/testdata/A2.golden
+++ /dev/null
@@ -1,13 +0,0 @@
-package A2
-
-// This refactoring causes addition of "errors" import.
-// TODO(adonovan): fix: it should also remove "fmt".
-
-import (
-	"errors"
-	myfmt "fmt"
-)
-
-func example(n int) {
-	errors.New("")
-}
diff --git a/refactor/eg/testdata/B.template b/refactor/eg/testdata/B.template
deleted file mode 100644
index c16627bd55e..00000000000
--- a/refactor/eg/testdata/B.template
+++ /dev/null
@@ -1,9 +0,0 @@
-package template
-
-// Basic test of expression refactoring.
-// (Types are not important in this case; it could be done with gofmt -r.)
-
-import "time"
-
-func before(t time.Time) time.Duration { return time.Now().Sub(t) }
-func after(t time.Time) time.Duration  { return time.Since(t) }
diff --git a/refactor/eg/testdata/B1.go b/refactor/eg/testdata/B1.go
deleted file mode 100644
index 1e09c905d27..00000000000
--- a/refactor/eg/testdata/B1.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package B1
-
-import "time"
-
-var startup = time.Now()
-
-func example() time.Duration {
-	before := time.Now()
-	time.Sleep(1)
-	return time.Now().Sub(before)
-}
-
-func msSinceStartup() int64 {
-	return int64(time.Now().Sub(startup) / time.Millisecond)
-}
diff --git a/refactor/eg/testdata/B1.golden b/refactor/eg/testdata/B1.golden
deleted file mode 100644
index b2ed30b72fc..00000000000
--- a/refactor/eg/testdata/B1.golden
+++ /dev/null
@@ -1,15 +0,0 @@
-package B1
-
-import "time"
-
-var startup = time.Now()
-
-func example() time.Duration {
-	before := time.Now()
-	time.Sleep(1)
-	return time.Since(before)
-}
-
-func msSinceStartup() int64 {
-	return int64(time.Since(startup) / time.Millisecond)
-}
diff --git a/refactor/eg/testdata/C.template b/refactor/eg/testdata/C.template
deleted file mode 100644
index f6f94d4aa9f..00000000000
--- a/refactor/eg/testdata/C.template
+++ /dev/null
@@ -1,10 +0,0 @@
-package template
-
-// Test of repeated use of wildcard in pattern.
-
-// NB: multiple patterns would be required to handle variants such as
-// s[:len(s)], s[x:len(s)], etc, since a wildcard can't match nothing at all.
-// TODO(adonovan): support multiple templates in a single pass.
-
-func before(s string) string { return s[:len(s)] }
-func after(s string) string  { return s }
diff --git a/refactor/eg/testdata/C1.go b/refactor/eg/testdata/C1.go
deleted file mode 100644
index fb565a3587f..00000000000
--- a/refactor/eg/testdata/C1.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package C1
-
-import "strings"
-
-func example() {
-	x := "foo"
-	println(x[:len(x)])
-
-	// Match, but the transformation is not sound w.r.t. possible side effects.
-	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 3))])
-
-	// No match, since second use of wildcard doesn't match first.
-	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 2))])
-
-	// Recursive match demonstrating bottom-up rewrite:
-	// only after the inner replacement occurs does the outer syntax match.
-	println((x[:len(x)])[:len(x[:len(x)])])
-	// -> (x[:len(x)])
-	// -> x
-}
diff --git a/refactor/eg/testdata/C1.golden b/refactor/eg/testdata/C1.golden
deleted file mode 100644
index d3b0b711881..00000000000
--- a/refactor/eg/testdata/C1.golden
+++ /dev/null
@@ -1,20 +0,0 @@
-package C1
-
-import "strings"
-
-func example() {
-	x := "foo"
-	println(x)
-
-	// Match, but the transformation is not sound w.r.t. possible side effects.
-	println(strings.Repeat("*", 3))
-
-	// No match, since second use of wildcard doesn't match first.
-	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 2))])
-
-	// Recursive match demonstrating bottom-up rewrite:
-	// only after the inner replacement occurs does the outer syntax match.
-	println(x)
-	// -> (x[:len(x)])
-	// -> x
-}
diff --git a/refactor/eg/testdata/D.template b/refactor/eg/testdata/D.template
deleted file mode 100644
index 6d3b6feb71d..00000000000
--- a/refactor/eg/testdata/D.template
+++ /dev/null
@@ -1,8 +0,0 @@
-package template
-
-import "fmt"
-
-// Test of semantic (not syntactic) matching of basic literals.
-
-func before() (int, error) { return fmt.Println(123, "a") }
-func after() (int, error)  { return fmt.Println(456, "!") }
diff --git a/refactor/eg/testdata/D1.go b/refactor/eg/testdata/D1.go
deleted file mode 100644
index 03a434c8738..00000000000
--- a/refactor/eg/testdata/D1.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package D1
-
-import "fmt"
-
-func example() {
-	fmt.Println(123, "a")         // match
-	fmt.Println(0x7b, `a`)        // match
-	fmt.Println(0173, "\x61")     // match
-	fmt.Println(100+20+3, "a"+"") // no match: constant expressions, but not basic literals
-}
diff --git a/refactor/eg/testdata/D1.golden b/refactor/eg/testdata/D1.golden
deleted file mode 100644
index 88d4a9e5151..00000000000
--- a/refactor/eg/testdata/D1.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-package D1
-
-import "fmt"
-
-func example() {
-	fmt.Println(456, "!")         // match
-	fmt.Println(456, "!")         // match
-	fmt.Println(456, "!")         // match
-	fmt.Println(100+20+3, "a"+"") // no match: constant expressions, but not basic literals
-}
diff --git a/refactor/eg/testdata/E.template b/refactor/eg/testdata/E.template
deleted file mode 100644
index 4bbbd1139b9..00000000000
--- a/refactor/eg/testdata/E.template
+++ /dev/null
@@ -1,12 +0,0 @@
-package template
-
-import (
-	"fmt"
-	"log"
-	"os"
-)
-
-// Replace call to void function by call to non-void function.
-
-func before(x interface{}) { log.Fatal(x) }
-func after(x interface{})  { fmt.Fprintf(os.Stderr, "warning: %v", x) }
diff --git a/refactor/eg/testdata/E1.go b/refactor/eg/testdata/E1.go
deleted file mode 100644
index 54054c81258..00000000000
--- a/refactor/eg/testdata/E1.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package E1
-
-import "log"
-
-func example() {
-	log.Fatal("oops") // match
-}
diff --git a/refactor/eg/testdata/E1.golden b/refactor/eg/testdata/E1.golden
deleted file mode 100644
index ec10b41e5c9..00000000000
--- a/refactor/eg/testdata/E1.golden
+++ /dev/null
@@ -1,11 +0,0 @@
-package E1
-
-import (
-	"fmt"
-	"log"
-	"os"
-)
-
-func example() {
-	fmt.Fprintf(os.Stderr, "warning: %v", "oops") // match
-}
diff --git a/refactor/eg/testdata/F.template b/refactor/eg/testdata/F.template
deleted file mode 100644
index df73beb28d7..00000000000
--- a/refactor/eg/testdata/F.template
+++ /dev/null
@@ -1,8 +0,0 @@
-package templates
-
-// Test
-
-import "sync"
-
-func before(s sync.RWMutex) { s.Lock() }
-func after(s sync.RWMutex)  { s.RLock() }
diff --git a/refactor/eg/testdata/F1.go b/refactor/eg/testdata/F1.go
deleted file mode 100644
index da9c9de1b2d..00000000000
--- a/refactor/eg/testdata/F1.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package F1
-
-import "sync"
-
-func example(n int) {
-	var x struct {
-		mutex sync.RWMutex
-	}
-
-	var y struct {
-		sync.RWMutex
-	}
-
-	type l struct {
-		sync.RWMutex
-	}
-
-	var z struct {
-		l
-	}
-
-	var a struct {
-		*l
-	}
-
-	var b struct{ Lock func() }
-
-	// Match
-	x.mutex.Lock()
-
-	// Match
-	y.Lock()
-
-	// Match indirect
-	z.Lock()
-
-	// Should be no match however currently matches due to:
-	// https://golang.org/issue/8584
-	// Will start failing when this is fixed then just change golden to
-	// No match pointer indirect
-	// a.Lock()
-	a.Lock()
-
-	// No match
-	b.Lock()
-}
diff --git a/refactor/eg/testdata/F1.golden b/refactor/eg/testdata/F1.golden
deleted file mode 100644
index ea5d0cde3a8..00000000000
--- a/refactor/eg/testdata/F1.golden
+++ /dev/null
@@ -1,46 +0,0 @@
-package F1
-
-import "sync"
-
-func example(n int) {
-	var x struct {
-		mutex sync.RWMutex
-	}
-
-	var y struct {
-		sync.RWMutex
-	}
-
-	type l struct {
-		sync.RWMutex
-	}
-
-	var z struct {
-		l
-	}
-
-	var a struct {
-		*l
-	}
-
-	var b struct{ Lock func() }
-
-	// Match
-	x.mutex.RLock()
-
-	// Match
-	y.RLock()
-
-	// Match indirect
-	z.RLock()
-
-	// Should be no match however currently matches due to:
-	// https://golang.org/issue/8584
-	// Will start failing when this is fixed then just change golden to
-	// No match pointer indirect
-	// a.Lock()
-	a.RLock()
-
-	// No match
-	b.Lock()
-}
diff --git a/refactor/eg/testdata/G.template b/refactor/eg/testdata/G.template
deleted file mode 100644
index ab368ce4637..00000000000
--- a/refactor/eg/testdata/G.template
+++ /dev/null
@@ -1,9 +0,0 @@
-package templates
-
-import (
-	"go/ast" // defines many unencapsulated structs
-	"go/token"
-)
-
-func before(from, to token.Pos) ast.BadExpr { return ast.BadExpr{From: from, To: to} }
-func after(from, to token.Pos) ast.BadExpr  { return ast.BadExpr{from, to} }
diff --git a/refactor/eg/testdata/G1.go b/refactor/eg/testdata/G1.go
deleted file mode 100644
index 0fb9ab95b84..00000000000
--- a/refactor/eg/testdata/G1.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package G1
-
-import "go/ast"
-
-func example() {
-	_ = ast.BadExpr{From: 123, To: 456} // match
-	_ = ast.BadExpr{123, 456}           // no match
-	_ = ast.BadExpr{From: 123}          // no match
-	_ = ast.BadExpr{To: 456}            // no match
-}
diff --git a/refactor/eg/testdata/G1.golden b/refactor/eg/testdata/G1.golden
deleted file mode 100644
index ba3704c4210..00000000000
--- a/refactor/eg/testdata/G1.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-package G1
-
-import "go/ast"
-
-func example() {
-	_ = ast.BadExpr{123, 456}  // match
-	_ = ast.BadExpr{123, 456}  // no match
-	_ = ast.BadExpr{From: 123} // no match
-	_ = ast.BadExpr{To: 456}   // no match
-}
diff --git a/refactor/eg/testdata/H.template b/refactor/eg/testdata/H.template
deleted file mode 100644
index fa6f802c8af..00000000000
--- a/refactor/eg/testdata/H.template
+++ /dev/null
@@ -1,9 +0,0 @@
-package templates
-
-import (
-	"go/ast" // defines many unencapsulated structs
-	"go/token"
-)
-
-func before(from, to token.Pos) ast.BadExpr { return ast.BadExpr{from, to} }
-func after(from, to token.Pos) ast.BadExpr  { return ast.BadExpr{From: from, To: to} }
diff --git a/refactor/eg/testdata/H1.go b/refactor/eg/testdata/H1.go
deleted file mode 100644
index e151ac87764..00000000000
--- a/refactor/eg/testdata/H1.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package H1
-
-import "go/ast"
-
-func example() {
-	_ = ast.BadExpr{From: 123, To: 456} // no match
-	_ = ast.BadExpr{123, 456}           // match
-	_ = ast.BadExpr{From: 123}          // no match
-	_ = ast.BadExpr{To: 456}            // no match
-}
diff --git a/refactor/eg/testdata/H1.golden b/refactor/eg/testdata/H1.golden
deleted file mode 100644
index da2658a6648..00000000000
--- a/refactor/eg/testdata/H1.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-package H1
-
-import "go/ast"
-
-func example() {
-	_ = ast.BadExpr{From: 123, To: 456} // no match
-	_ = ast.BadExpr{From: 123, To: 456} // match
-	_ = ast.BadExpr{From: 123}          // no match
-	_ = ast.BadExpr{To: 456}            // no match
-}
diff --git a/refactor/eg/testdata/I.template b/refactor/eg/testdata/I.template
deleted file mode 100644
index b8e8f939b10..00000000000
--- a/refactor/eg/testdata/I.template
+++ /dev/null
@@ -1,12 +0,0 @@
-package templates
-
-import (
-	"errors"
-	"fmt"
-)
-
-func before(s string) error { return fmt.Errorf("%s", s) }
-func after(s string) error {
-	n := fmt.Sprintf("error - %s", s)
-	return errors.New(n)
-}
diff --git a/refactor/eg/testdata/I1.go b/refactor/eg/testdata/I1.go
deleted file mode 100644
index ef3fe8befac..00000000000
--- a/refactor/eg/testdata/I1.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package I1
-
-import "fmt"
-
-func example() {
-	_ = fmt.Errorf("%s", "foo")
-}
diff --git a/refactor/eg/testdata/I1.golden b/refactor/eg/testdata/I1.golden
deleted file mode 100644
index d0246aeb85d..00000000000
--- a/refactor/eg/testdata/I1.golden
+++ /dev/null
@@ -1,12 +0,0 @@
-package I1
-
-import (
-	"errors"
-	"fmt"
-)
-
-func example() {
-
-	n := fmt.Sprintf("error - %s", "foo")
-	_ = errors.New(n)
-}
diff --git a/refactor/eg/testdata/J.template b/refactor/eg/testdata/J.template
deleted file mode 100644
index b3b1f1872ac..00000000000
--- a/refactor/eg/testdata/J.template
+++ /dev/null
@@ -1,9 +0,0 @@
-package templates
-
-import ()
-
-func before(x int) int { return x + x + x }
-func after(x int) int {
-	temp := x + x
-	return temp + x
-}
diff --git a/refactor/eg/testdata/J1.go b/refactor/eg/testdata/J1.go
deleted file mode 100644
index 532ca13e66c..00000000000
--- a/refactor/eg/testdata/J1.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package I1
-
-import "fmt"
-
-func example() {
-	temp := 5
-	fmt.Print(temp + temp + temp)
-}
diff --git a/refactor/eg/testdata/J1.golden b/refactor/eg/testdata/J1.golden
deleted file mode 100644
index 911ef874175..00000000000
--- a/refactor/eg/testdata/J1.golden
+++ /dev/null
@@ -1,9 +0,0 @@
-package I1
-
-import "fmt"
-
-func example() {
-	temp := 5
-	temp := temp + temp
-	fmt.Print(temp + temp)
-}
diff --git a/refactor/eg/testdata/a.txtar b/refactor/eg/testdata/a.txtar
new file mode 100644
index 00000000000..873197391e5
--- /dev/null
+++ b/refactor/eg/testdata/a.txtar
@@ -0,0 +1,147 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+// Basic test of type-aware expression refactoring.
+
+import (
+	"errors"
+	"fmt"
+)
+
+func before(s string) error { return fmt.Errorf("%s", s) }
+func after(s string) error  { return errors.New(s) }
+
+-- in/a1/a1.go --
+package a1
+
+import (
+	. "fmt"
+	myfmt "fmt"
+	"os"
+	"strings"
+)
+
+func example(n int) {
+	x := "foo" + strings.Repeat("\t", n)
+	// Match, despite named import.
+	myfmt.Errorf("%s", x)
+
+	// Match, despite dot import.
+	Errorf("%s", x)
+
+	// Match: multiple matches in same function are possible.
+	myfmt.Errorf("%s", x)
+
+	// No match: wildcarded operand has the wrong type.
+	myfmt.Errorf("%s", 3)
+
+	// No match: function operand doesn't match.
+	myfmt.Printf("%s", x)
+
+	// No match again, dot import.
+	Printf("%s", x)
+
+	// Match.
+	myfmt.Fprint(os.Stderr, myfmt.Errorf("%s", x+"foo"))
+
+	// No match: though this literally matches the template,
+	// fmt doesn't resolve to a package here.
+	var fmt struct{ Errorf func(string, string) }
+	fmt.Errorf("%s", x)
+
+	// Recursive matching:
+
+	// Match: both matches are well-typed, so both succeed.
+	myfmt.Errorf("%s", myfmt.Errorf("%s", x+"foo").Error())
+
+	// Outer match succeeds, inner doesn't: 3 has wrong type.
+	myfmt.Errorf("%s", myfmt.Errorf("%s", 3).Error())
+
+	// Inner match succeeds, outer doesn't: the inner replacement
+	// has the wrong type (error not string).
+	myfmt.Errorf("%s", myfmt.Errorf("%s", x+"foo"))
+}
+
+-- out/a1/a1.go --
+package a1
+
+import (
+	"errors"
+	. "fmt"
+	myfmt "fmt"
+	"os"
+	"strings"
+)
+
+func example(n int) {
+	x := "foo" + strings.Repeat("\t", n)
+	// Match, despite named import.
+	errors.New(x)
+
+	// Match, despite dot import.
+	errors.New(x)
+
+	// Match: multiple matches in same function are possible.
+	errors.New(x)
+
+	// No match: wildcarded operand has the wrong type.
+	myfmt.Errorf("%s", 3)
+
+	// No match: function operand doesn't match.
+	myfmt.Printf("%s", x)
+
+	// No match again, dot import.
+	Printf("%s", x)
+
+	// Match.
+	myfmt.Fprint(os.Stderr, errors.New(x+"foo"))
+
+	// No match: though this literally matches the template,
+	// fmt doesn't resolve to a package here.
+	var fmt struct{ Errorf func(string, string) }
+	fmt.Errorf("%s", x)
+
+	// Recursive matching:
+
+	// Match: both matches are well-typed, so both succeed.
+	errors.New(errors.New(x + "foo").Error())
+
+	// Outer match succeeds, inner doesn't: 3 has wrong type.
+	errors.New(myfmt.Errorf("%s", 3).Error())
+
+	// Inner match succeeds, outer doesn't: the inner replacement
+	// has the wrong type (error not string).
+	myfmt.Errorf("%s", errors.New(x+"foo"))
+}
+-- a2/a2.go --
+package a2
+
+// This refactoring causes addition of "errors" import.
+// TODO(adonovan): fix: it should also remove "fmt".
+
+import myfmt "fmt"
+
+func example(n int) {
+	myfmt.Errorf("%s", "")
+}
+
+-- out/a2/a2.go --
+package a2
+
+// This refactoring causes addition of "errors" import.
+// TODO(adonovan): fix: it should also remove "fmt".
+
+import (
+	"errors"
+	myfmt "fmt"
+)
+
+func example(n int) {
+	errors.New("")
+}
diff --git a/refactor/eg/testdata/b.txtar b/refactor/eg/testdata/b.txtar
new file mode 100644
index 00000000000..d55fa1ad7ea
--- /dev/null
+++ b/refactor/eg/testdata/b.txtar
@@ -0,0 +1,49 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+// Basic test of expression refactoring.
+// (Types are not important in this case; it could be done with gofmt -r.)
+
+import "time"
+
+func before(t time.Time) time.Duration { return time.Now().Sub(t) }
+func after(t time.Time) time.Duration  { return time.Since(t) }
+
+-- in/b1/b1.go --
+package b1
+
+import "time"
+
+var startup = time.Now()
+
+func example() time.Duration {
+	before := time.Now()
+	time.Sleep(1)
+	return time.Now().Sub(before)
+}
+
+func msSinceStartup() int64 {
+	return int64(time.Now().Sub(startup) / time.Millisecond)
+}
+
+-- out/b1/b1.go --
+package b1
+
+import "time"
+
+var startup = time.Now()
+
+func example() time.Duration {
+	before := time.Now()
+	time.Sleep(1)
+	return time.Since(before)
+}
+
+func msSinceStartup() int64 {
+	return int64(time.Since(startup) / time.Millisecond)
+}
diff --git a/refactor/eg/testdata/bad_type.template b/refactor/eg/testdata/bad_type.txtar
similarity index 75%
rename from refactor/eg/testdata/bad_type.template
rename to refactor/eg/testdata/bad_type.txtar
index 6d53d7e5709..3c4ff5638ba 100644
--- a/refactor/eg/testdata/bad_type.template
+++ b/refactor/eg/testdata/bad_type.txtar
@@ -1,3 +1,9 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 // Test in which replacement has a different type.
diff --git a/refactor/eg/testdata/c.txtar b/refactor/eg/testdata/c.txtar
new file mode 100644
index 00000000000..67c29fed1c1
--- /dev/null
+++ b/refactor/eg/testdata/c.txtar
@@ -0,0 +1,60 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+// Test of repeated use of wildcard in pattern.
+
+// NB: multiple patterns would be required to handle variants such as
+// s[:len(s)], s[x:len(s)], etc, since a wildcard can't match nothing at all.
+// TODO(adonovan): support multiple templates in a single pass.
+
+func before(s string) string { return s[:len(s)] }
+func after(s string) string  { return s }
+
+-- in/c1/c1.go --
+package C1
+
+import "strings"
+
+func example() {
+	x := "foo"
+	println(x[:len(x)])
+
+	// Match, but the transformation is not sound w.r.t. possible side effects.
+	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 3))])
+
+	// No match, since second use of wildcard doesn't match first.
+	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 2))])
+
+	// Recursive match demonstrating bottom-up rewrite:
+	// only after the inner replacement occurs does the outer syntax match.
+	println((x[:len(x)])[:len(x[:len(x)])])
+	// -> (x[:len(x)])
+	// -> x
+}
+
+-- out/c1/c1.go --
+package C1
+
+import "strings"
+
+func example() {
+	x := "foo"
+	println(x)
+
+	// Match, but the transformation is not sound w.r.t. possible side effects.
+	println(strings.Repeat("*", 3))
+
+	// No match, since second use of wildcard doesn't match first.
+	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 2))])
+
+	// Recursive match demonstrating bottom-up rewrite:
+	// only after the inner replacement occurs does the outer syntax match.
+	println(x)
+	// -> (x[:len(x)])
+	// -> x
+}
diff --git a/refactor/eg/testdata/d.txtar b/refactor/eg/testdata/d.txtar
new file mode 100644
index 00000000000..5b4e65d2e3c
--- /dev/null
+++ b/refactor/eg/testdata/d.txtar
@@ -0,0 +1,38 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import "fmt"
+
+// Test of semantic (not syntactic) matching of basic literals.
+
+func before() (int, error) { return fmt.Println(123, "a") }
+func after() (int, error)  { return fmt.Println(456, "!") }
+
+-- in/d1/d1.go --
+package d1
+
+import "fmt"
+
+func example() {
+	fmt.Println(123, "a")         // match
+	fmt.Println(0x7b, `a`)        // match
+	fmt.Println(0173, "\x61")     // match
+	fmt.Println(100+20+3, "a"+"") // no match: constant expressions, but not basic literals
+}
+
+-- out/d1/d1.go --
+package d1
+
+import "fmt"
+
+func example() {
+	fmt.Println(456, "!")         // match
+	fmt.Println(456, "!")         // match
+	fmt.Println(456, "!")         // match
+	fmt.Println(100+20+3, "a"+"") // no match: constant expressions, but not basic literals
+}
diff --git a/refactor/eg/testdata/e.txtar b/refactor/eg/testdata/e.txtar
new file mode 100644
index 00000000000..e82652f221a
--- /dev/null
+++ b/refactor/eg/testdata/e.txtar
@@ -0,0 +1,40 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import (
+	"fmt"
+	"log"
+	"os"
+)
+
+// Replace call to void function by call to non-void function.
+
+func before(x interface{}) { log.Fatal(x) }
+func after(x interface{})  { fmt.Fprintf(os.Stderr, "warning: %v", x) }
+
+-- in/e1/e1.go --
+package e1
+
+import "log"
+
+func example() {
+	log.Fatal("oops") // match
+}
+
+-- out/e1/e1.go --
+package e1
+
+import (
+	"fmt"
+	"log"
+	"os"
+)
+
+func example() {
+	fmt.Fprintf(os.Stderr, "warning: %v", "oops") // match
+}
diff --git a/refactor/eg/testdata/expr_type_mismatch.template b/refactor/eg/testdata/expr_type_mismatch.txtar
similarity index 88%
rename from refactor/eg/testdata/expr_type_mismatch.template
rename to refactor/eg/testdata/expr_type_mismatch.txtar
index 00e00b1c419..dca702687a0 100644
--- a/refactor/eg/testdata/expr_type_mismatch.template
+++ b/refactor/eg/testdata/expr_type_mismatch.txtar
@@ -1,3 +1,10 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 import (
diff --git a/refactor/eg/testdata/f.txtar b/refactor/eg/testdata/f.txtar
new file mode 100644
index 00000000000..139405e57c0
--- /dev/null
+++ b/refactor/eg/testdata/f.txtar
@@ -0,0 +1,110 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+// Test
+
+import "sync"
+
+func before(s sync.RWMutex) { s.Lock() }
+func after(s sync.RWMutex)  { s.RLock() }
+
+-- in/f1/f1.go --
+package F1
+
+import "sync"
+
+func example(n int) {
+	var x struct {
+		mutex sync.RWMutex
+	}
+
+	var y struct {
+		sync.RWMutex
+	}
+
+	type l struct {
+		sync.RWMutex
+	}
+
+	var z struct {
+		l
+	}
+
+	var a struct {
+		*l
+	}
+
+	var b struct{ Lock func() }
+
+	// Match
+	x.mutex.Lock()
+
+	// Match
+	y.Lock()
+
+	// Match indirect
+	z.Lock()
+
+	// Should be no match however currently matches due to:
+	// https://golang.org/issue/8584
+	// Will start failing when this is fixed then just change golden to
+	// No match pointer indirect
+	// a.Lock()
+	a.Lock()
+
+	// No match
+	b.Lock()
+}
+
+-- out/f1/f1.go --
+package F1
+
+import "sync"
+
+func example(n int) {
+	var x struct {
+		mutex sync.RWMutex
+	}
+
+	var y struct {
+		sync.RWMutex
+	}
+
+	type l struct {
+		sync.RWMutex
+	}
+
+	var z struct {
+		l
+	}
+
+	var a struct {
+		*l
+	}
+
+	var b struct{ Lock func() }
+
+	// Match
+	x.mutex.RLock()
+
+	// Match
+	y.RLock()
+
+	// Match indirect
+	z.RLock()
+
+	// Should be no match however currently matches due to:
+	// https://golang.org/issue/8584
+	// Will start failing when this is fixed then just change golden to
+	// No match pointer indirect
+	// a.Lock()
+	a.RLock()
+
+	// No match
+	b.Lock()
+}
diff --git a/refactor/eg/testdata/g.txtar b/refactor/eg/testdata/g.txtar
new file mode 100644
index 00000000000..95843bf940a
--- /dev/null
+++ b/refactor/eg/testdata/g.txtar
@@ -0,0 +1,39 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import (
+	"go/ast" // defines many unencapsulated structs
+	"go/token"
+)
+
+func before(from, to token.Pos) ast.BadExpr { return ast.BadExpr{From: from, To: to} }
+func after(from, to token.Pos) ast.BadExpr  { return ast.BadExpr{from, to} }
+
+-- in/g1/g1.go --
+package g1
+
+import "go/ast"
+
+func example() {
+	_ = ast.BadExpr{From: 123, To: 456} // match
+	_ = ast.BadExpr{123, 456}           // no match
+	_ = ast.BadExpr{From: 123}          // no match
+	_ = ast.BadExpr{To: 456}            // no match
+}
+
+-- out/g1/g1.go --
+package g1
+
+import "go/ast"
+
+func example() {
+	_ = ast.BadExpr{123, 456}  // match
+	_ = ast.BadExpr{123, 456}  // no match
+	_ = ast.BadExpr{From: 123} // no match
+	_ = ast.BadExpr{To: 456}   // no match
+}
diff --git a/refactor/eg/testdata/h.txtar b/refactor/eg/testdata/h.txtar
new file mode 100644
index 00000000000..94085350183
--- /dev/null
+++ b/refactor/eg/testdata/h.txtar
@@ -0,0 +1,39 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import (
+	"go/ast" // defines many unencapsulated structs
+	"go/token"
+)
+
+func before(from, to token.Pos) ast.BadExpr { return ast.BadExpr{from, to} }
+func after(from, to token.Pos) ast.BadExpr  { return ast.BadExpr{From: from, To: to} }
+
+-- in/h1/h1.go --
+package h1
+
+import "go/ast"
+
+func example() {
+	_ = ast.BadExpr{From: 123, To: 456} // no match
+	_ = ast.BadExpr{123, 456}           // match
+	_ = ast.BadExpr{From: 123}          // no match
+	_ = ast.BadExpr{To: 456}            // no match
+}
+
+-- out/h1/h1.go --
+package h1
+
+import "go/ast"
+
+func example() {
+	_ = ast.BadExpr{From: 123, To: 456} // no match
+	_ = ast.BadExpr{From: 123, To: 456} // match
+	_ = ast.BadExpr{From: 123}          // no match
+	_ = ast.BadExpr{To: 456}            // no match
+}
diff --git a/refactor/eg/testdata/i.txtar b/refactor/eg/testdata/i.txtar
new file mode 100644
index 00000000000..11486c2112f
--- /dev/null
+++ b/refactor/eg/testdata/i.txtar
@@ -0,0 +1,41 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import (
+	"errors"
+	"fmt"
+)
+
+func before(s string) error { return fmt.Errorf("%s", s) }
+func after(s string) error {
+	n := fmt.Sprintf("error - %s", s)
+	return errors.New(n)
+}
+
+-- in/i1/i1.go --
+package i1
+
+import "fmt"
+
+func example() {
+	_ = fmt.Errorf("%s", "foo")
+}
+
+-- out/i1/i1.go --
+package i1
+
+import (
+	"errors"
+	"fmt"
+)
+
+func example() {
+
+	n := fmt.Sprintf("error - %s", "foo")
+	_ = errors.New(n)
+}
diff --git a/refactor/eg/testdata/j.txtar b/refactor/eg/testdata/j.txtar
new file mode 100644
index 00000000000..9bb0a71418b
--- /dev/null
+++ b/refactor/eg/testdata/j.txtar
@@ -0,0 +1,36 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import ()
+
+func before(x int) int { return x + x + x }
+func after(x int) int {
+	temp := x + x
+	return temp + x
+}
+
+-- in/j1/j1.go --
+package j1
+
+import "fmt"
+
+func example() {
+	temp := 5
+	fmt.Print(temp + temp + temp)
+}
+
+-- out/j1/j1.go --
+package j1
+
+import "fmt"
+
+func example() {
+	temp := 5
+	temp := temp + temp
+	fmt.Print(temp + temp)
+}
diff --git a/refactor/eg/testdata/no_after_return.template b/refactor/eg/testdata/no_after_return.txtar
similarity index 56%
rename from refactor/eg/testdata/no_after_return.template
rename to refactor/eg/testdata/no_after_return.txtar
index dd2cbf61e15..7965ddd8538 100644
--- a/refactor/eg/testdata/no_after_return.template
+++ b/refactor/eg/testdata/no_after_return.txtar
@@ -1,3 +1,10 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 func before() int { return 0 }
diff --git a/refactor/eg/testdata/no_before.template b/refactor/eg/testdata/no_before.txtar
similarity index 56%
rename from refactor/eg/testdata/no_before.template
rename to refactor/eg/testdata/no_before.txtar
index 9205e6677a4..640f7269a04 100644
--- a/refactor/eg/testdata/no_before.template
+++ b/refactor/eg/testdata/no_before.txtar
@@ -1,3 +1,10 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 const shouldFail = "no 'before' func found in template"
diff --git a/refactor/eg/testdata/type_mismatch.template b/refactor/eg/testdata/type_mismatch.txtar
similarity index 64%
rename from refactor/eg/testdata/type_mismatch.template
rename to refactor/eg/testdata/type_mismatch.txtar
index 787c9a7a8c7..94157b6dc06 100644
--- a/refactor/eg/testdata/type_mismatch.template
+++ b/refactor/eg/testdata/type_mismatch.txtar
@@ -1,3 +1,10 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 const shouldFail = "different signatures"
diff --git a/refactor/rename/rename.go b/refactor/rename/rename.go
index a5a59e97488..ae646475692 100644
--- a/refactor/rename/rename.go
+++ b/refactor/rename/rename.go
@@ -2,10 +2,13 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package rename contains the implementation of the 'gorename' command
-// whose main function is in golang.org/x/tools/cmd/gorename.
-// See the Usage constant for the command documentation.
-package rename // import "golang.org/x/tools/refactor/rename"
+// Package rename contains the obsolete implementation of the deleted
+// golang.org/x/tools/cmd/gorename. This logic has not worked properly
+// since the advent of Go modules, and should be deleted too.
+//
+// Use gopls instead, either via the Rename LSP method or the "gopls
+// rename" subcommand.
+package rename
 
 import (
 	"bytes"
diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go
index bab0e3cfd3f..3d693aa04ab 100644
--- a/refactor/satisfy/find.go
+++ b/refactor/satisfy/find.go
@@ -43,7 +43,6 @@ import (
 	"go/token"
 	"go/types"
 
-	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/types/typeutil"
 	"golang.org/x/tools/internal/typeparams"
 )
@@ -708,7 +707,7 @@ func (f *Finder) stmt(s ast.Stmt) {
 
 // -- Plundered from golang.org/x/tools/go/ssa -----------------
 
-func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) }
+func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) }
 
 func isInterface(T types.Type) bool { return types.IsInterface(T) }
 
diff --git a/txtar/fs.go b/txtar/fs.go
index e37397e7b71..fc8df12c18f 100644
--- a/txtar/fs.go
+++ b/txtar/fs.go
@@ -10,6 +10,7 @@ import (
 	"io"
 	"io/fs"
 	"path"
+	"slices"
 	"time"
 )
 
@@ -152,10 +153,7 @@ func (fsys *filesystem) ReadFile(name string) ([]byte, error) {
 		return nil, err
 	}
 	if file, ok := file.(*openFile); ok {
-		// TODO: use slices.Clone once x/tools has 1.21 available.
-		cp := make([]byte, file.size)
-		copy(cp, file.data)
-		return cp, err
+		return slices.Clone(file.data), nil
 	}
 	return nil, &fs.PathError{Op: "read", Path: name, Err: fs.ErrInvalid}
 }