diff --git a/cmd/bisect/main.go b/cmd/bisect/main.go index 6a3745c0582..a152fbd37c7 100644 --- a/cmd/bisect/main.go +++ b/cmd/bisect/main.go @@ -262,7 +262,7 @@ type Bisect struct { // each pattern starts with a !. Disable bool - // SkipDigits is the number of hex digits to use in skip messages. + // SkipHexDigits is the number of hex digits to use in skip messages. // If the set of available changes is the same in each run, as it should be, // then this doesn't matter: we'll only exclude suffixes that uniquely identify // a given change. But for some programs, especially bisecting runtime diff --git a/cmd/splitdwarf/splitdwarf.go b/cmd/splitdwarf/splitdwarf.go index c20a9a7745a..e2a7790106f 100644 --- a/cmd/splitdwarf/splitdwarf.go +++ b/cmd/splitdwarf/splitdwarf.go @@ -358,6 +358,7 @@ func CreateMmapFile(outDwarf string, size int64) (*os.File, []byte) { return dwarfFile, buffer } +// (dead code; retained for debugging) func describe(exem *macho.FileTOC) { note("Type = %s, Flags=0x%x", exem.Type, uint32(exem.Flags)) for i, l := range exem.Loads { diff --git a/cmd/ssadump/main.go b/cmd/ssadump/main.go index 275e0a92aef..f04c1c04633 100644 --- a/cmd/ssadump/main.go +++ b/cmd/ssadump/main.go @@ -188,11 +188,6 @@ func doMain() error { // e.g. --flag=one --flag=two would produce []string{"one", "two"}. type stringListValue []string -func newStringListValue(val []string, p *[]string) *stringListValue { - *p = val - return (*stringListValue)(p) -} - func (ss *stringListValue) Get() interface{} { return []string(*ss) } func (ss *stringListValue) String() string { return fmt.Sprintf("%q", *ss) } diff --git a/cmd/stringer/endtoend_test.go b/cmd/stringer/endtoend_test.go index 5a56636be46..721c1f68df5 100644 --- a/cmd/stringer/endtoend_test.go +++ b/cmd/stringer/endtoend_test.go @@ -93,20 +93,6 @@ func typeName(fname string) string { return fmt.Sprintf("%c%s", base[0]+'A'-'a', base[1:len(base)-len(".go")]) } -func moreTests(t *testing.T, dirname, prefix string) []string { - x, err := os.ReadDir(dirname) - if err != nil { - // error, but try the rest of the tests - t.Errorf("can't read type param tess from %s: %v", dirname, err) - return nil - } - names := make([]string, len(x)) - for i, f := range x { - names[i] = prefix + "/" + f.Name() - } - return names -} - // TestTags verifies that the -tags flag works as advertised. func TestTags(t *testing.T) { stringer := stringerPath(t) @@ -121,11 +107,10 @@ func TestTags(t *testing.T) { t.Fatal(err) } } - // Run stringer in the directory that contains the package files. - // We cannot run stringer in the current directory for the following reasons: - // - Versions of Go earlier than Go 1.11, do not support absolute directories as a pattern. - // - When the current directory is inside a go module, the path will not be considered - // a valid path to a package. + // Run stringer in the directory that contains the module that contains the package files. + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n"), 0o600); err != nil { + t.Fatal(err) + } err := runInDir(t, dir, stringer, "-type", "Const", ".") if err != nil { t.Fatal(err) @@ -167,7 +152,10 @@ func TestConstValueChange(t *testing.T) { t.Fatal(err) } stringSource := filepath.Join(dir, "day_string.go") - // Run stringer in the directory that contains the package files. + // Run stringer in the directory that contains the module that contains the package files. + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n"), 0o600); err != nil { + t.Fatal(err) + } err = runInDir(t, dir, stringer, "-type", "Day", "-output", stringSource) if err != nil { t.Fatal(err) @@ -388,7 +376,6 @@ func runInDir(t testing.TB, dir, name string, arg ...string) error { t.Helper() cmd := testenv.Command(t, name, arg...) cmd.Dir = dir - cmd.Env = append(os.Environ(), "GO111MODULE=auto") out, err := cmd.CombinedOutput() if len(out) > 0 { t.Logf("%s", out) diff --git a/container/intsets/sparse.go b/container/intsets/sparse.go index d5fe156ed36..c56aacc28bb 100644 --- a/container/intsets/sparse.go +++ b/container/intsets/sparse.go @@ -287,14 +287,6 @@ func (s *Sparse) next(b *block) *block { return b.next } -// prev returns the previous block in the list, or end if b is the first block. -func (s *Sparse) prev(b *block) *block { - if b.prev == &s.root { - return &none - } - return b.prev -} - // IsEmpty reports whether the set s is empty. func (s *Sparse) IsEmpty() bool { return s.root.next == nil || s.root.offset == MaxInt @@ -1077,6 +1069,7 @@ func (s *Sparse) AppendTo(slice []int) []int { // -- Testing/debugging ------------------------------------------------ // check returns an error if the representation invariants of s are violated. +// (unused; retained for debugging) func (s *Sparse) check() error { s.init() if s.root.empty() { diff --git a/copyright/copyright.go b/copyright/copyright.go index 16bd9d2f329..54bc8f512a4 100644 --- a/copyright/copyright.go +++ b/copyright/copyright.go @@ -16,6 +16,7 @@ import ( "strings" ) +// (used only by tests) func checkCopyright(dir string) ([]string, error) { var files []string err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { diff --git a/go.mod b/go.mod index d7b6f18ddc1..0f49047782e 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.22.0 - golang.org/x/net v0.32.0 + golang.org/x/net v0.34.0 golang.org/x/sync v0.10.0 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 ) -require golang.org/x/sys v0.28.0 // indirect +require golang.org/x/sys v0.29.0 // indirect diff --git a/go.sum b/go.sum index 9b25a309b97..c788c5fbdc3 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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.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/analysis.go b/go/analysis/analysis.go index d384aa89b8e..3a73084a53c 100644 --- a/go/analysis/analysis.go +++ b/go/analysis/analysis.go @@ -156,10 +156,17 @@ type Pass struct { // AllPackageFacts returns a new slice containing all package // facts of the analysis's FactTypes in unspecified order. + // See comments for AllObjectFacts. AllPackageFacts func() []PackageFact // AllObjectFacts returns a new slice containing all object // facts of the analysis's FactTypes in unspecified order. + // + // The result includes all facts exported by packages + // whose symbols are referenced by the current package + // (by qualified identifiers or field/method selections). + // And it includes all facts exported from the current + // package by the current analysis pass. AllObjectFacts func() []ObjectFact /* Further fields may be added in future. */ diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 6aa04ed1502..3cc2beca737 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -352,7 +352,7 @@ func Run(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Res testenv.NeedsGoPackages(t) } - pkgs, err := loadPackages(a, dir, patterns...) + pkgs, err := loadPackages(dir, patterns...) if err != nil { t.Errorf("loading %s: %v", patterns, err) return nil @@ -433,7 +433,7 @@ type Result struct { // dependencies) from dir, which is the root of a GOPATH-style project tree. // loadPackages returns an error if any package had an error, or the pattern // matched no packages. -func loadPackages(a *analysis.Analyzer, dir string, patterns ...string) ([]*packages.Package, error) { +func loadPackages(dir string, patterns ...string) ([]*packages.Package, error) { env := []string{"GOPATH=" + dir, "GO111MODULE=off", "GOWORK=off"} // GOPATH mode // Undocumented module mode. Will be replaced by something better. diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go index 1282e70d41f..c2445575cff 100644 --- a/go/analysis/internal/analysisflags/flags.go +++ b/go/analysis/internal/analysisflags/flags.go @@ -250,21 +250,12 @@ const ( setFalse ) -func triStateFlag(name string, value triState, usage string) *triState { - flag.Var(&value, name, usage) - return &value -} - // triState implements flag.Value, flag.Getter, and flag.boolFlag. // They work like boolean flags: we can say vet -printf as well as vet -printf=true func (ts *triState) Get() interface{} { return *ts == setTrue } -func (ts triState) isTrue() bool { - return ts == setTrue -} - func (ts *triState) Set(value string) error { b, err := strconv.ParseBool(value) if err != nil { diff --git a/go/analysis/passes/structtag/structtag.go b/go/analysis/passes/structtag/structtag.go index a0beb46bd14..4115ef76943 100644 --- a/go/analysis/passes/structtag/structtag.go +++ b/go/analysis/passes/structtag/structtag.go @@ -89,7 +89,7 @@ var checkTagSpaces = map[string]bool{"json": true, "xml": true, "asn1": true} // checkCanonicalFieldTag checks a single struct field tag. func checkCanonicalFieldTag(pass *analysis.Pass, field *types.Var, tag string, seen *namesSeen) { switch pass.Pkg.Path() { - case "encoding/json", "encoding/xml": + case "encoding/json", "encoding/json/v2", "encoding/xml": // These packages know how to use their own APIs. // Sometimes they are testing what happens to incorrect programs. return diff --git a/go/ast/inspector/inspector.go b/go/ast/inspector/inspector.go index 958cf38deb0..cfda8934332 100644 --- a/go/ast/inspector/inspector.go +++ b/go/ast/inspector/inspector.go @@ -36,6 +36,7 @@ package inspector import ( "go/ast" + _ "unsafe" ) // An Inspector provides methods for inspecting @@ -44,6 +45,9 @@ type Inspector struct { events []event } +//go:linkname events +func events(in *Inspector) []event { return in.events } + // New returns an Inspector for the specified syntax trees. func New(files []*ast.File) *Inspector { return &Inspector{traverse(files)} @@ -52,9 +56,10 @@ func New(files []*ast.File) *Inspector { // An event represents a push or a pop // of an ast.Node during a traversal. type event struct { - node ast.Node - typ uint64 // typeOf(node) on push event, or union of typ strictly between push and pop events on pop events - index int // index of corresponding push or pop event + node ast.Node + typ uint64 // typeOf(node) on push event, or union of typ strictly between push and pop events on pop events + index int32 // index of corresponding push or pop event + parent int32 // index of parent's push node (defined for push nodes only) } // TODO: Experiment with storing only the second word of event.node (unsafe.Pointer). @@ -83,7 +88,7 @@ func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) { // }) mask := maskOf(types) - for i := 0; i < len(in.events); { + for i := int32(0); i < int32(len(in.events)); { ev := in.events[i] if ev.index > i { // push @@ -113,7 +118,7 @@ func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) { // matches an element of the types slice. func (in *Inspector) Nodes(types []ast.Node, f func(n ast.Node, push bool) (proceed bool)) { mask := maskOf(types) - for i := 0; i < len(in.events); { + for i := int32(0); i < int32(len(in.events)); { ev := in.events[i] if ev.index > i { // push @@ -147,7 +152,7 @@ func (in *Inspector) Nodes(types []ast.Node, f func(n ast.Node, push bool) (proc func (in *Inspector) WithStack(types []ast.Node, f func(n ast.Node, push bool, stack []ast.Node) (proceed bool)) { mask := maskOf(types) var stack []ast.Node - for i := 0; i < len(in.events); { + for i := int32(0); i < int32(len(in.events)); { ev := in.events[i] if ev.index > i { // push @@ -196,18 +201,24 @@ func traverse(files []*ast.File) []event { events := make([]event, 0, capacity) var stack []event - stack = append(stack, event{}) // include an extra event so file nodes have a parent + stack = append(stack, event{index: -1}) // include an extra event so file nodes have a parent for _, f := range files { ast.Inspect(f, func(n ast.Node) bool { if n != nil { // push ev := event{ - node: n, - typ: 0, // temporarily used to accumulate type bits of subtree - index: len(events), // push event temporarily holds own index + node: n, + typ: 0, // temporarily used to accumulate type bits of subtree + index: int32(len(events)), // push event temporarily holds own index + parent: stack[len(stack)-1].index, } stack = append(stack, ev) events = append(events, ev) + + // 2B nodes ought to be enough for anyone! + if int32(len(events)) < 0 { + panic("event index exceeded int32") + } } else { // pop top := len(stack) - 1 @@ -216,9 +227,9 @@ func traverse(files []*ast.File) []event { push := ev.index parent := top - 1 - events[push].typ = typ // set type of push - stack[parent].typ |= typ | ev.typ // parent's typ contains push and pop's typs. - events[push].index = len(events) // make push refer to pop + events[push].typ = typ // set type of push + stack[parent].typ |= typ | ev.typ // parent's typ contains push and pop's typs. + events[push].index = int32(len(events)) // make push refer to pop stack = stack[:top] events = append(events, ev) diff --git a/go/ast/inspector/iter.go b/go/ast/inspector/iter.go index b7e959114cb..c576dc70ac7 100644 --- a/go/ast/inspector/iter.go +++ b/go/ast/inspector/iter.go @@ -26,7 +26,7 @@ func (in *Inspector) PreorderSeq(types ...ast.Node) iter.Seq[ast.Node] { return func(yield func(ast.Node) bool) { mask := maskOf(types) - for i := 0; i < len(in.events); { + for i := int32(0); i < int32(len(in.events)); { ev := in.events[i] if ev.index > i { // push @@ -63,7 +63,7 @@ func All[N interface { mask := typeOf((N)(nil)) return func(yield func(N) bool) { - for i := 0; i < len(in.events); { + for i := int32(0); i < int32(len(in.events)); { ev := in.events[i] if ev.index > i { // push diff --git a/go/ast/inspector/typeof.go b/go/ast/inspector/typeof.go index 2a872f89d47..40b1bfd7e62 100644 --- a/go/ast/inspector/typeof.go +++ b/go/ast/inspector/typeof.go @@ -12,6 +12,8 @@ package inspector import ( "go/ast" "math" + + _ "unsafe" ) const ( @@ -215,6 +217,7 @@ func typeOf(n ast.Node) uint64 { return 0 } +//go:linkname maskOf func maskOf(nodes []ast.Node) uint64 { if nodes == nil { return math.MaxUint64 // match all node types diff --git a/go/callgraph/callgraph_test.go b/go/callgraph/callgraph_test.go index f90c52f8541..7c7cb0d2c3f 100644 --- a/go/callgraph/callgraph_test.go +++ b/go/callgraph/callgraph_test.go @@ -71,7 +71,6 @@ func main() { var ( once sync.Once - prog *ssa.Program main *ssa.Function ) @@ -82,7 +81,7 @@ func example(t testing.TB) (*ssa.Program, *ssa.Function) { prog.Build() main = ssapkgs[0].Members["main"].(*ssa.Function) }) - return prog, main + return main.Prog, main } var stats bool = false // print stats? diff --git a/go/expect/expect.go b/go/expect/expect.go index 6cdfcf0bef0..be0e1dd23e6 100644 --- a/go/expect/expect.go +++ b/go/expect/expect.go @@ -120,10 +120,3 @@ func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern } return f.Pos(startOffset + matchStart), f.Pos(startOffset + matchEnd), nil } - -func lineEnd(f *token.File, line int) token.Pos { - if line >= f.LineCount() { - return token.Pos(f.Base() + f.Size()) - } - return f.LineStart(line + 1) -} diff --git a/go/packages/golist.go b/go/packages/golist.go index 870271ed51f..0458b4f9c43 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -322,6 +322,7 @@ type jsonPackage struct { ImportPath string Dir string Name string + Target string Export string GoFiles []string CompiledGoFiles []string @@ -506,6 +507,7 @@ func (state *golistState) createDriverResponse(words ...string) (*DriverResponse Name: p.Name, ID: p.ImportPath, Dir: p.Dir, + Target: p.Target, GoFiles: absJoin(p.Dir, p.GoFiles, p.CgoFiles), CompiledGoFiles: absJoin(p.Dir, p.CompiledGoFiles), OtherFiles: absJoin(p.Dir, otherFiles(p)...), @@ -811,6 +813,9 @@ func jsonFlag(cfg *Config, goVersion int) string { if cfg.Mode&NeedEmbedPatterns != 0 { addFields("EmbedPatterns") } + if cfg.Mode&NeedTarget != 0 { + addFields("Target") + } return "-json=" + strings.Join(fields, ",") } diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go index aab3362dbfd..3841ac3410b 100644 --- a/go/packages/gopackages/main.go +++ b/go/packages/gopackages/main.go @@ -248,11 +248,6 @@ func (app *application) print(lpkg *packages.Package) { // e.g. --flag=one --flag=two would produce []string{"one", "two"}. type stringListValue []string -func newStringListValue(val []string, p *[]string) *stringListValue { - *p = val - return (*stringListValue)(p) -} - func (ss *stringListValue) Get() interface{} { return []string(*ss) } func (ss *stringListValue) String() string { return fmt.Sprintf("%q", *ss) } diff --git a/go/packages/loadmode_string.go b/go/packages/loadmode_string.go index 969da4c263c..69eec9f44dd 100644 --- a/go/packages/loadmode_string.go +++ b/go/packages/loadmode_string.go @@ -27,6 +27,7 @@ var modes = [...]struct { {NeedModule, "NeedModule"}, {NeedEmbedFiles, "NeedEmbedFiles"}, {NeedEmbedPatterns, "NeedEmbedPatterns"}, + {NeedTarget, "NeedTarget"}, } func (mode LoadMode) String() string { diff --git a/go/packages/packages.go b/go/packages/packages.go index 9dedf9777dc..0147d9080aa 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -118,6 +118,9 @@ const ( // NeedEmbedPatterns adds EmbedPatterns. NeedEmbedPatterns + // NeedTarget adds Target. + NeedTarget + // Be sure to update loadmode_string.go when adding new items! ) @@ -479,6 +482,10 @@ type Package struct { // information for the package as provided by the build system. ExportFile string + // Target is the absolute install path of the .a file, for libraries, + // and of the executable file, for binaries. + Target string + // Imports maps import paths appearing in the package's Go source files // to corresponding loaded Packages. Imports map[string]*Package diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 11c4f77dce4..fc420321c31 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2322,8 +2322,8 @@ func TestLoadModeStrings(t *testing.T) { "(NeedName|NeedExportFile)", }, { - packages.NeedForTest | packages.NeedEmbedFiles | packages.NeedEmbedPatterns, - "(NeedForTest|NeedEmbedFiles|NeedEmbedPatterns)", + packages.NeedForTest | packages.NeedTarget | packages.NeedEmbedFiles | packages.NeedEmbedPatterns, + "(NeedForTest|NeedEmbedFiles|NeedEmbedPatterns|NeedTarget)", }, { packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedDeps | packages.NeedExportFile | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypesSizes, @@ -2334,8 +2334,8 @@ func TestLoadModeStrings(t *testing.T) { "(NeedName|NeedModule)", }, { - packages.NeedName | 0x10000, // off the end (future use) - "(NeedName|0x10000)", + packages.NeedName | 0x100000, // off the end (future use) + "(NeedName|0x100000)", }, { packages.NeedName | 0x400, // needInternalDepsErrors @@ -2999,30 +2999,6 @@ func constant(p *packages.Package, name string) *types.Const { return c.(*types.Const) } -func copyAll(srcPath, dstPath string) error { - return filepath.Walk(srcPath, func(path string, info os.FileInfo, _ error) error { - if info.IsDir() { - return nil - } - contents, err := os.ReadFile(path) - if err != nil { - return err - } - rel, err := filepath.Rel(srcPath, path) - if err != nil { - return err - } - dstFilePath := strings.Replace(filepath.Join(dstPath, rel), "definitelynot_go.mod", "go.mod", -1) - if err := os.MkdirAll(filepath.Dir(dstFilePath), 0755); err != nil { - return err - } - if err := os.WriteFile(dstFilePath, contents, 0644); err != nil { - return err - } - return nil - }) -} - func TestExportFile(t *testing.T) { // This used to trigger the log.Fatal in loadFromExportData. // See go.dev/issue/45584. @@ -3222,7 +3198,7 @@ func TestIssue70394(t *testing.T) { } } -// TestNeedTypesInfoOnly tests when NeedTypesInfo was set and NeedSyntax & NeedTypes were not, +// TestLoadTypesInfoWithoutSyntaxOrTypes tests when NeedTypesInfo was set and NeedSyntax & NeedTypes were not, // Load should include the TypesInfo of packages properly func TestLoadTypesInfoWithoutSyntaxOrTypes(t *testing.T) { testAllOrModulesParallel(t, testLoadTypesInfoWithoutSyntaxOrTypes) @@ -3339,6 +3315,78 @@ func Foo() int { return a.Foo() } t.Logf("Packages: %+v", pkgs) } +// TestTarget tests the new field added as part of golang/go#38445. +// The test uses GOPATH mode because non-main packages don't usually +// have install targets in module mode. +func TestTarget(t *testing.T) { + testenv.NeedsGoPackages(t) + + dir := writeTree(t, ` +-- gopath/src/a/a.go -- +package a + +func Foo() {} +-- gopath/src/b/b.go -- +package main + +import "a" + +func main() { + a.Foo() +} +`) + gopath := filepath.Join(dir, "gopath") + + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedTarget, + Env: append(os.Environ(), "GOPATH=" + gopath, "GO111MODULE=off"), + }, filepath.Join(gopath, "src", "...")) + if err != nil { + t.Fatal(err) + } + var goexe string + if runtime.GOOS == "windows" { + goexe = ".exe" + } + want := map[string]string{ + "a": filepath.Join(gopath, "pkg", runtime.GOOS+"_"+runtime.GOARCH, "a.a"), + "b": filepath.Join(gopath, "bin", "b"+goexe), + } + got := make(map[string]string) + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + got[pkg.PkgPath] = pkg.Target + }) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Load returned mismatching Target fields (pkgpath->target -want +got):\n%s", diff) + } + t.Logf("Packages: %+v", pkgs) +} + +// TestMainPackagePathInModeTypes tests (*types.Package).Path() for +// main packages in mode NeedTypes, a regression test for #70742, a +// bug in cmd/compile's export data that caused them to appear as +// "main". (The PkgPath field was always correct.) +func TestMainPackagePathInModeTypes(t *testing.T) { + testenv.NeedsGoPackages(t) + + cfg := &packages.Config{Mode: packages.NeedName | packages.NeedTypes} + pkgs, err := packages.Load(cfg, "cmd/go") + if err != nil { + t.Fatal(err) + } + p := pkgs[0] + if p.PkgPath != "cmd/go" || + p.Name != "main" || + p.Types.Path() != "cmd/go" || + p.Types.Name() != "main" { + t.Errorf("PkgPath=%q Name=%q Types.Path=%q Types.Name=%q; want (cmd/go, main) both times)", + p.PkgPath, + p.Name, + p.Types.Name(), + p.Types.Path()) + } +} + func writeTree(t *testing.T, archive string) string { root := t.TempDir() diff --git a/go/ssa/const.go b/go/ssa/const.go index 4dc53ef83cc..764b73529e3 100644 --- a/go/ssa/const.go +++ b/go/ssa/const.go @@ -78,7 +78,7 @@ func zeroConst(t types.Type) *Const { func (c *Const) RelString(from *types.Package) string { var s string if c.Value == nil { - s = typesinternal.ZeroString(c.typ, types.RelativeTo(from)) + s, _ = typesinternal.ZeroString(c.typ, types.RelativeTo(from)) } else if c.Value.Kind() == constant.String { s = constant.StringVal(c.Value) const max = 20 diff --git a/go/ssa/const_test.go b/go/ssa/const_test.go index c8ecadf7f0f..6738f07b2ef 100644 --- a/go/ssa/const_test.go +++ b/go/ssa/const_test.go @@ -58,10 +58,10 @@ func TestConstString(t *testing.T) { {"interface{string}", nil, `"":interface{string}`}, {"interface{int|int64}", nil, "0:interface{int|int64}"}, {"interface{bool}", nil, "false:interface{bool}"}, - {"interface{bool|int}", nil, "nil:interface{bool|int}"}, - {"interface{int|string}", nil, "nil:interface{int|string}"}, - {"interface{bool|string}", nil, "nil:interface{bool|string}"}, - {"interface{struct{x string}}", nil, "nil:interface{struct{x string}}"}, + {"interface{bool|int}", nil, "invalid:interface{bool|int}"}, + {"interface{int|string}", nil, "invalid:interface{int|string}"}, + {"interface{bool|string}", nil, "invalid:interface{bool|string}"}, + {"interface{struct{x string}}", nil, "invalid:interface{struct{x string}}"}, {"interface{int|int64}", int64(1), "1:interface{int|int64}"}, {"interface{~bool}", true, "true:interface{~bool}"}, {"interface{Named}", "lorem ipsum", `"lorem ipsum":interface{P.Named}`}, diff --git a/go/ssa/dom.go b/go/ssa/dom.go index 02c1ae83ae3..f490986140c 100644 --- a/go/ssa/dom.go +++ b/go/ssa/dom.go @@ -318,6 +318,7 @@ func printDomTreeText(buf *bytes.Buffer, v *BasicBlock, indent int) { // printDomTreeDot prints the dominator tree of f in AT&T GraphViz // (.dot) format. +// (unused; retained for debugging) func printDomTreeDot(buf *bytes.Buffer, f *Function) { fmt.Fprintln(buf, "//", f) fmt.Fprintln(buf, "digraph domtree {") diff --git a/go/ssa/interp/external.go b/go/ssa/interp/external.go index 3e6fb01918a..2a3a7e5b79e 100644 --- a/go/ssa/interp/external.go +++ b/go/ssa/interp/external.go @@ -303,15 +303,6 @@ func ext۰time۰Sleep(fr *frame, args []value) value { return nil } -func valueToBytes(v value) []byte { - in := v.([]value) - b := make([]byte, len(in)) - for i := range in { - b[i] = in[i].(byte) - } - return b -} - func ext۰os۰Getenv(fr *frame, args []value) value { name := args[0].(string) switch name { diff --git a/go/ssa/interp/value.go b/go/ssa/interp/value.go index d1250b119d1..bd681cb6152 100644 --- a/go/ssa/interp/value.go +++ b/go/ssa/interp/value.go @@ -99,10 +99,7 @@ var ( // hashType returns a hash for t such that // types.Identical(x, y) => hashType(x) == hashType(y). func hashType(t types.Type) int { - mu.Lock() - h := int(hasher.Hash(t)) - mu.Unlock() - return h + return int(hasher.Hash(t)) } // usesBuiltinMap returns true if the built-in hash function and diff --git a/go/ssa/util.go b/go/ssa/util.go index cdc46209e7c..aa070eacdcb 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -14,6 +14,7 @@ import ( "io" "os" "sync" + _ "unsafe" // for go:linkname hack "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" @@ -408,14 +409,6 @@ func (canon *canonizer) instantiateMethod(m *types.Func, targs []types.Type, ctx } // Exposed to ssautil using the linkname hack. +// +//go:linkname isSyntactic golang.org/x/tools/go/ssa.isSyntactic func isSyntactic(pkg *Package) bool { return pkg.syntax } - -// mapValues returns a new unordered array of map values. -func mapValues[K comparable, V any](m map[K]V) []V { - vals := make([]V, 0, len(m)) - for _, fn := range m { - vals = append(vals, fn) - } - return vals - -} diff --git a/go/types/typeutil/map.go b/go/types/typeutil/map.go index 8d824f7140f..93b3090c687 100644 --- a/go/types/typeutil/map.go +++ b/go/types/typeutil/map.go @@ -2,30 +2,35 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package typeutil defines various utilities for types, such as Map, -// a mapping from types.Type to any values. -package typeutil // import "golang.org/x/tools/go/types/typeutil" +// Package typeutil defines various utilities for types, such as [Map], +// a hash table that maps [types.Type] to any value. +package typeutil import ( "bytes" "fmt" "go/types" - "reflect" + "hash/maphash" + "unsafe" "golang.org/x/tools/internal/typeparams" ) // Map is a hash-table-based mapping from types (types.Type) to -// arbitrary any values. The concrete types that implement +// arbitrary values. The concrete types that implement // the Type interface are pointers. Since they are not canonicalized, // == cannot be used to check for equivalence, and thus we cannot // simply use a Go map. // // Just as with map[K]V, a nil *Map is a valid empty map. // -// Not thread-safe. +// Read-only map operations ([Map.At], [Map.Len], and so on) may +// safely be called concurrently. +// +// TODO(adonovan): deprecate in favor of https://go.dev/issues/69420 +// and 69559, if the latter proposals for a generic hash-map type and +// a types.Hash function are accepted. type Map struct { - hasher Hasher // shared by many Maps table map[uint32][]entry // maps hash to bucket; entry.key==nil means unused length int // number of map entries } @@ -36,35 +41,17 @@ type entry struct { value any } -// SetHasher sets the hasher used by Map. -// -// All Hashers are functionally equivalent but contain internal state -// used to cache the results of hashing previously seen types. -// -// A single Hasher created by MakeHasher() may be shared among many -// Maps. This is recommended if the instances have many keys in -// common, as it will amortize the cost of hash computation. -// -// A Hasher may grow without bound as new types are seen. Even when a -// type is deleted from the map, the Hasher never shrinks, since other -// types in the map may reference the deleted type indirectly. +// SetHasher has no effect. // -// Hashers are not thread-safe, and read-only operations such as -// Map.Lookup require updates to the hasher, so a full Mutex lock (not a -// read-lock) is require around all Map operations if a shared -// hasher is accessed from multiple threads. -// -// If SetHasher is not called, the Map will create a private hasher at -// the first call to Insert. -func (m *Map) SetHasher(hasher Hasher) { - m.hasher = hasher -} +// It is a relic of an optimization that is no longer profitable. Do +// not use [Hasher], [MakeHasher], or [SetHasher] in new code. +func (m *Map) SetHasher(Hasher) {} // Delete removes the entry with the given key, if any. // It returns true if the entry was found. func (m *Map) Delete(key types.Type) bool { if m != nil && m.table != nil { - hash := m.hasher.Hash(key) + hash := hash(key) bucket := m.table[hash] for i, e := range bucket { if e.key != nil && types.Identical(key, e.key) { @@ -83,7 +70,7 @@ func (m *Map) Delete(key types.Type) bool { // The result is nil if the entry is not present. func (m *Map) At(key types.Type) any { if m != nil && m.table != nil { - for _, e := range m.table[m.hasher.Hash(key)] { + for _, e := range m.table[hash(key)] { if e.key != nil && types.Identical(key, e.key) { return e.value } @@ -96,7 +83,7 @@ func (m *Map) At(key types.Type) any { // and returns the previous entry, if any. func (m *Map) Set(key types.Type, value any) (prev any) { if m.table != nil { - hash := m.hasher.Hash(key) + hash := hash(key) bucket := m.table[hash] var hole *entry for i, e := range bucket { @@ -115,10 +102,7 @@ func (m *Map) Set(key types.Type, value any) (prev any) { m.table[hash] = append(bucket, entry{key, value}) } } else { - if m.hasher.memo == nil { - m.hasher = MakeHasher() - } - hash := m.hasher.Hash(key) + hash := hash(key) m.table = map[uint32][]entry{hash: {entry{key, value}}} } @@ -195,53 +179,35 @@ func (m *Map) KeysString() string { return m.toString(false) } -//////////////////////////////////////////////////////////////////////// -// Hasher - -// A Hasher maps each type to its hash value. -// For efficiency, a hasher uses memoization; thus its memory -// footprint grows monotonically over time. -// Hashers are not thread-safe. -// Hashers have reference semantics. -// Call MakeHasher to create a Hasher. -type Hasher struct { - memo map[types.Type]uint32 - - // ptrMap records pointer identity. - ptrMap map[any]uint32 - - // sigTParams holds type parameters from the signature being hashed. - // Signatures are considered identical modulo renaming of type parameters, so - // within the scope of a signature type the identity of the signature's type - // parameters is just their index. - // - // Since the language does not currently support referring to uninstantiated - // generic types or functions, and instantiated signatures do not have type - // parameter lists, we should never encounter a second non-empty type - // parameter list when hashing a generic signature. - sigTParams *types.TypeParamList -} +// -- Hasher -- -// MakeHasher returns a new Hasher instance. -func MakeHasher() Hasher { - return Hasher{ - memo: make(map[types.Type]uint32), - ptrMap: make(map[any]uint32), - sigTParams: nil, - } +// hash returns the hash of type t. +// TODO(adonovan): replace by types.Hash when Go proposal #69420 is accepted. +func hash(t types.Type) uint32 { + return theHasher.Hash(t) } +// A Hasher provides a [Hasher.Hash] method to map a type to its hash value. +// Hashers are stateless, and all are equivalent. +type Hasher struct{} + +var theHasher Hasher + +// MakeHasher returns Hasher{}. +// Hashers are stateless; all are equivalent. +func MakeHasher() Hasher { return theHasher } + // Hash computes a hash value for the given type t such that // Identical(t, t') => Hash(t) == Hash(t'). func (h Hasher) Hash(t types.Type) uint32 { - hash, ok := h.memo[t] - if !ok { - hash = h.hashFor(t) - h.memo[t] = hash - } - return hash + return hasher{inGenericSig: false}.hash(t) } +// hasher holds the state of a single Hash traversal: whether we are +// inside the signature of a generic function; this is used to +// optimize [hasher.hashTypeParam]. +type hasher struct{ inGenericSig bool } + // hashString computes the Fowler–Noll–Vo hash of s. func hashString(s string) uint32 { var h uint32 @@ -252,21 +218,21 @@ func hashString(s string) uint32 { return h } -// hashFor computes the hash of t. -func (h Hasher) hashFor(t types.Type) uint32 { +// hash computes the hash of t. +func (h hasher) hash(t types.Type) uint32 { // See Identical for rationale. switch t := t.(type) { case *types.Basic: return uint32(t.Kind()) case *types.Alias: - return h.Hash(types.Unalias(t)) + return h.hash(types.Unalias(t)) case *types.Array: - return 9043 + 2*uint32(t.Len()) + 3*h.Hash(t.Elem()) + return 9043 + 2*uint32(t.Len()) + 3*h.hash(t.Elem()) case *types.Slice: - return 9049 + 2*h.Hash(t.Elem()) + return 9049 + 2*h.hash(t.Elem()) case *types.Struct: var hash uint32 = 9059 @@ -277,12 +243,12 @@ func (h Hasher) hashFor(t types.Type) uint32 { } hash += hashString(t.Tag(i)) hash += hashString(f.Name()) // (ignore f.Pkg) - hash += h.Hash(f.Type()) + hash += h.hash(f.Type()) } return hash case *types.Pointer: - return 9067 + 2*h.Hash(t.Elem()) + return 9067 + 2*h.hash(t.Elem()) case *types.Signature: var hash uint32 = 9091 @@ -290,33 +256,11 @@ func (h Hasher) hashFor(t types.Type) uint32 { hash *= 8863 } - // Use a separate hasher for types inside of the signature, where type - // parameter identity is modified to be (index, constraint). We must use a - // new memo for this hasher as type identity may be affected by this - // masking. For example, in func[T any](*T), the identity of *T depends on - // whether we are mapping the argument in isolation, or recursively as part - // of hashing the signature. - // - // We should never encounter a generic signature while hashing another - // generic signature, but defensively set sigTParams only if h.mask is - // unset. tparams := t.TypeParams() - if h.sigTParams == nil && tparams.Len() != 0 { - h = Hasher{ - // There may be something more efficient than discarding the existing - // memo, but it would require detecting whether types are 'tainted' by - // references to type parameters. - memo: make(map[types.Type]uint32), - // Re-using ptrMap ensures that pointer identity is preserved in this - // hasher. - ptrMap: h.ptrMap, - sigTParams: tparams, - } - } - - for i := 0; i < tparams.Len(); i++ { + for i := range tparams.Len() { + h.inGenericSig = true tparam := tparams.At(i) - hash += 7 * h.Hash(tparam.Constraint()) + hash += 7 * h.hash(tparam.Constraint()) } return hash + 3*h.hashTuple(t.Params()) + 5*h.hashTuple(t.Results()) @@ -350,17 +294,17 @@ func (h Hasher) hashFor(t types.Type) uint32 { return hash case *types.Map: - return 9109 + 2*h.Hash(t.Key()) + 3*h.Hash(t.Elem()) + return 9109 + 2*h.hash(t.Key()) + 3*h.hash(t.Elem()) case *types.Chan: - return 9127 + 2*uint32(t.Dir()) + 3*h.Hash(t.Elem()) + return 9127 + 2*uint32(t.Dir()) + 3*h.hash(t.Elem()) case *types.Named: - hash := h.hashPtr(t.Obj()) + hash := h.hashTypeName(t.Obj()) targs := t.TypeArgs() for i := 0; i < targs.Len(); i++ { targ := targs.At(i) - hash += 2 * h.Hash(targ) + hash += 2 * h.hash(targ) } return hash @@ -374,17 +318,17 @@ func (h Hasher) hashFor(t types.Type) uint32 { panic(fmt.Sprintf("%T: %v", t, t)) } -func (h Hasher) hashTuple(tuple *types.Tuple) uint32 { +func (h hasher) hashTuple(tuple *types.Tuple) uint32 { // See go/types.identicalTypes for rationale. n := tuple.Len() hash := 9137 + 2*uint32(n) - for i := 0; i < n; i++ { - hash += 3 * h.Hash(tuple.At(i).Type()) + for i := range n { + hash += 3 * h.hash(tuple.At(i).Type()) } return hash } -func (h Hasher) hashUnion(t *types.Union) uint32 { +func (h hasher) hashUnion(t *types.Union) uint32 { // Hash type restrictions. terms, err := typeparams.UnionTermSet(t) // if err != nil t has invalid type restrictions. Fall back on a non-zero @@ -395,11 +339,11 @@ func (h Hasher) hashUnion(t *types.Union) uint32 { return h.hashTermSet(terms) } -func (h Hasher) hashTermSet(terms []*types.Term) uint32 { +func (h hasher) hashTermSet(terms []*types.Term) uint32 { hash := 9157 + 2*uint32(len(terms)) for _, term := range terms { // term order is not significant. - termHash := h.Hash(term.Type()) + termHash := h.hash(term.Type()) if term.Tilde() { termHash *= 9161 } @@ -408,36 +352,42 @@ func (h Hasher) hashTermSet(terms []*types.Term) uint32 { return hash } -// hashTypeParam returns a hash of the type parameter t, with a hash value -// depending on whether t is contained in h.sigTParams. -// -// If h.sigTParams is set and contains t, then we are in the process of hashing -// a signature, and the hash value of t must depend only on t's index and -// constraint: signatures are considered identical modulo type parameter -// renaming. To avoid infinite recursion, we only hash the type parameter -// index, and rely on types.Identical to handle signatures where constraints -// are not identical. -// -// Otherwise the hash of t depends only on t's pointer identity. -func (h Hasher) hashTypeParam(t *types.TypeParam) uint32 { - if h.sigTParams != nil { - i := t.Index() - if i >= 0 && i < h.sigTParams.Len() && t == h.sigTParams.At(i) { - return 9173 + 3*uint32(i) - } +// hashTypeParam returns the hash of a type parameter. +func (h hasher) hashTypeParam(t *types.TypeParam) uint32 { + // Within the signature of a generic function, TypeParams are + // identical if they have the same index and constraint, so we + // hash them based on index. + // + // When we are outside a generic function, free TypeParams are + // identical iff they are the same object, so we can use a + // more discriminating hash consistent with object identity. + // This optimization saves [Map] about 4% when hashing all the + // types.Info.Types in the forward closure of net/http. + if !h.inGenericSig { + // Optimization: outside a generic function signature, + // use a more discrimating hash consistent with object identity. + return h.hashTypeName(t.Obj()) } - return h.hashPtr(t.Obj()) + return 9173 + 3*uint32(t.Index()) } -// hashPtr hashes the pointer identity of ptr. It uses h.ptrMap to ensure that -// pointers values are not dependent on the GC. -func (h Hasher) hashPtr(ptr any) uint32 { - if hash, ok := h.ptrMap[ptr]; ok { - return hash - } - hash := uint32(reflect.ValueOf(ptr).Pointer()) - h.ptrMap[ptr] = hash - return hash +var theSeed = maphash.MakeSeed() + +// hashTypeName hashes the pointer of tname. +func (hasher) hashTypeName(tname *types.TypeName) uint32 { + // Since types.Identical uses == to compare TypeNames, + // the Hash function uses maphash.Comparable. + // TODO(adonovan): or will, when it becomes available in go1.24. + // In the meantime we use the pointer's numeric value. + // + // hash := maphash.Comparable(theSeed, tname) + // + // (Another approach would be to hash the name and package + // path, and whether or not it is a package-level typename. It + // is rare for a package to define multiple local types with + // the same name.) + hash := uintptr(unsafe.Pointer(tname)) + return uint32(hash ^ (hash >> 32)) } // shallowHash computes a hash of t without looking at any of its @@ -454,7 +404,7 @@ func (h Hasher) hashPtr(ptr any) uint32 { // include m itself; there is no mention of the named type X that // might help us break the cycle. // (See comment in go/types.identical, case *Interface, for more.) -func (h Hasher) shallowHash(t types.Type) uint32 { +func (h hasher) shallowHash(t types.Type) uint32 { // t is the type of an interface method (Signature), // its params or results (Tuples), or their immediate // elements (mostly Slice, Pointer, Basic, Named), @@ -475,7 +425,7 @@ func (h Hasher) shallowHash(t types.Type) uint32 { case *types.Tuple: n := t.Len() hash := 9137 + 2*uint32(n) - for i := 0; i < n; i++ { + for i := range n { hash += 53471161 * h.shallowHash(t.At(i).Type()) } return hash @@ -508,10 +458,10 @@ func (h Hasher) shallowHash(t types.Type) uint32 { return 9127 case *types.Named: - return h.hashPtr(t.Obj()) + return h.hashTypeName(t.Obj()) case *types.TypeParam: - return h.hashPtr(t.Obj()) + return h.hashTypeParam(t) } panic(fmt.Sprintf("shallowHash: %T: %v", t, t)) } diff --git a/go/types/typeutil/map_test.go b/go/types/typeutil/map_test.go index 22630cda740..920c8131257 100644 --- a/go/types/typeutil/map_test.go +++ b/go/types/typeutil/map_test.go @@ -16,7 +16,9 @@ import ( "go/types" "testing" + "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/testenv" ) var ( @@ -426,3 +428,42 @@ func instantiate(t *testing.T, origin types.Type, targs ...types.Type) types.Typ } return inst } + +// BenchmarkMap stores the type of every expression in the net/http +// package in a map. +func BenchmarkMap(b *testing.B) { + testenv.NeedsGoPackages(b) + + // Load all dependencies of net/http. + cfg := &packages.Config{Mode: packages.LoadAllSyntax} + pkgs, err := packages.Load(cfg, "net/http") + if err != nil { + b.Fatal(err) + } + + // Gather all unique types.Type pointers (>67K) annotating the syntax. + allTypes := make(map[types.Type]bool) + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + for _, tv := range pkg.TypesInfo.Types { + allTypes[tv.Type] = true + } + }) + b.ResetTimer() + + for range b.N { + // De-duplicate the logically identical types. + var tmap typeutil.Map + for t := range allTypes { + tmap.Set(t, nil) + } + + // For sanity, ensure we find a minimum number + // of distinct type equivalence classes. + if want := 12000; tmap.Len() < want { + b.Errorf("too few types (from %d types.Type values, got %d logically distinct types, want >=%d)", + len(allTypes), + tmap.Len(), + want) + } + } +} diff --git a/godoc/versions.go b/godoc/versions.go index 849f4d6470c..5a4dec33ea1 100644 --- a/godoc/versions.go +++ b/godoc/versions.go @@ -189,7 +189,7 @@ func parseRow(s string) (vr versionedRow, ok bool) { case strings.HasPrefix(rest, "func "): vr.kind = "func" rest = rest[len("func "):] - if i := strings.IndexByte(rest, '('); i != -1 { + if i := strings.IndexAny(rest, "[("); i != -1 { vr.name = rest[:i] return vr, true } diff --git a/godoc/versions_test.go b/godoc/versions_test.go index 0c5ca50c774..a021616ba11 100644 --- a/godoc/versions_test.go +++ b/godoc/versions_test.go @@ -65,6 +65,27 @@ func TestParseVersionRow(t *testing.T) { recv: "Encoding", }, }, + { + // Function with type parameters. + // Taken from "go/src/api/go1.21.txt". + row: "pkg cmp, func Compare[$0 Ordered]($0, $0) int #59488", + want: versionedRow{ + pkg: "cmp", + kind: "func", + name: "Compare", + }, + }, + { + // Function without type parameter but have "[" after + // "(" should have works as is. + // Taken from "go/src/api/go1.21.txt". + row: "pkg bytes, func ContainsFunc([]uint8, func(int32) bool) bool #54386", + want: versionedRow{ + pkg: "bytes", + kind: "func", + name: "ContainsFunc", + }, + }, } for i, tt := range tests { diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 38e246ecb47..2905a0e5336 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -436,6 +436,30 @@ Default: on. Package documentation: [lostcancel](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/lostcancel) + +## `modernize`: simplify code by using modern constructs + + +This analyzer reports opportunities for simplifying and clarifying +existing code by using more modern features of Go, such as: + + - replacing an if/else conditional assignment by a call to the + built-in min or max functions added in go1.21; + - replacing sort.Slice(x, func(i, j int) bool) { return s[i] < s[j] } + by a call to slices.Sort(s), added in go1.21; + - replacing interface{} by the 'any' type added in go1.18; + - replacing append([]T(nil), s...) by slices.Clone(s) or + slices.Concat(s), added in go1.21; + - replacing a loop around an m[k]=v map update by a call + to one of the Collect, Copy, Clone, or Insert functions + from the maps package, added in go1.21; + - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...), + added in go1.19; + +Default: on. + +Package documentation: [modernize](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize) + ## `nilfunc`: check for useless comparisons between functions and nil @@ -879,6 +903,37 @@ Default: on. Package documentation: [unsafeptr](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr) + +## `unusedfunc`: check for unused functions and methods + + +The unusedfunc analyzer reports functions and methods that are +never referenced outside of their own declaration. + +A function is considered unused if it is unexported and not +referenced (except within its own declaration). + +A method is considered unused if it is unexported, not referenced +(except within its own declaration), and its name does not match +that of any method of an interface type declared within the same +package. + +The tool may report a false positive for a declaration of an +unexported function that is referenced from another package using +the go:linkname mechanism, if the declaration's doc comment does +not also have a go:linkname comment. (Such code is in any case +strongly discouraged: linkname annotations, if they must be used at +all, should be used on both the declaration and the alias.) + +The unusedfunc algorithm is not as precise as the +golang.org/x/tools/cmd/deadcode tool, but it has the advantage that +it runs within the modular analysis framework, enabling near +real-time feedback within gopls. + +Default: on. + +Package documentation: [unusedfunc](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedfunc) + ## `unusedparams`: check for unused parameters of functions @@ -967,15 +1022,6 @@ Default: on. Package documentation: [unusedwrite](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedwrite) - -## `useany`: check for constraints that could be simplified to "any" - - - -Default: off. Enable by setting `"analyses": {"useany": true}`. - -Package documentation: [useany](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany) - ## `waitgroup`: check for misuses of sync.WaitGroup diff --git a/gopls/doc/assets/extract-val-all-before.png b/gopls/doc/assets/extract-val-all-before.png new file mode 100644 index 00000000000..1791283f30f Binary files /dev/null and b/gopls/doc/assets/extract-val-all-before.png differ diff --git a/gopls/doc/assets/extract-var-all-after.png b/gopls/doc/assets/extract-var-all-after.png new file mode 100644 index 00000000000..0340e4c6e7b Binary files /dev/null and b/gopls/doc/assets/extract-var-all-after.png differ diff --git a/gopls/doc/codelenses.md b/gopls/doc/codelenses.md index b7687bb3b30..d8aa8e1f479 100644 --- a/gopls/doc/codelenses.md +++ b/gopls/doc/codelenses.md @@ -23,30 +23,6 @@ Client support: -## `gc_details`: Toggle display of Go compiler optimization decisions - - -This codelens source causes the `package` declaration of -each file to be annotated with a command to toggle the -state of the per-session variable that controls whether -optimization decisions from the Go compiler (formerly known -as "gc") should be displayed as diagnostics. - -Optimization decisions include: -- whether a variable escapes, and how escape is inferred; -- whether a nil-pointer check is implied or eliminated; -- whether a function can be inlined. - -TODO(adonovan): this source is off by default because the -annotation is annoying and because VS Code has a separate -"Toggle gc details" command. Replace it with a Code Action -("Source action..."). - - -Default: off - -File type: Go - ## `generate`: Run `go generate` diff --git a/gopls/doc/features/diagnostics.md b/gopls/doc/features/diagnostics.md index 21015bcaa35..09b3cc33e90 100644 --- a/gopls/doc/features/diagnostics.md +++ b/gopls/doc/features/diagnostics.md @@ -49,6 +49,26 @@ build`. Gopls doesn't actually run the compiler; that would be too The example above shows a `printf` formatting mistake. The diagnostic contains a link to the documentation for the `printf` analyzer. +There is an optional third source of diagnostics: + + + +- **Compiler optimization details** are diagnostics that report + details relevant to optimization decisions made by the Go + compiler, such as whether a variable escapes or a slice index + requires a bounds check. + + Optimization decisions include: + whether a variable escapes, and how escape is inferred; + whether a nil-pointer check is implied or eliminated; and + whether a function can be inlined. + + This source is disabled by default but can be enabled on a + package-by-package basis by invoking the + `source.toggleCompilerOptDetails` ("Toggle compiler optimization + details") code action. + + ## Recomputation of diagnostics By default, diagnostics are automatically recomputed each time the source files @@ -299,7 +319,7 @@ dorky details and deletia: - **Experimental analyzers**. Gopls has some analyzers that are not enabled by default, because they produce too high a rate of false - positives. For example, fieldalignment, shadow, useany. + positives. For example, fieldalignment, shadow. Note: fillstruct is not a real analyzer. diff --git a/gopls/doc/features/navigation.md b/gopls/doc/features/navigation.md index 00371341a7c..f46f2935683 100644 --- a/gopls/doc/features/navigation.md +++ b/gopls/doc/features/navigation.md @@ -24,6 +24,10 @@ A definition query also works in these unexpected places: it returns the location of the embedded file. - On the declaration of a non-Go function (a `func` with no body), it returns the location of the assembly implementation, if any, +- On a **return statement**, it returns the location of the function's result variables. +- On a **goto**, **break**, or **continue** statement, it returns the + location of the label, the closing brace of the relevant block statement, or the + start of the relevant loop, respectively. -Generic types are currently not fully supported; see golang/go#59224. +If either the target type or the candidate type are generic, the +results will include the candidate type if there is any instantiation +of the two types that would allow one to implement the other. +(Note: the matcher doesn't current implement full unification, so type +parameters are treated like wildcards that may match arbitrary +types, without regard to consistency of substitutions across the +method set or even within a single method. +This may lead to occasional spurious matches.) Client support: - **VS Code**: Use [Go to Implementations](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-implementation) (`⌘F12`). diff --git a/gopls/doc/features/passive.md b/gopls/doc/features/passive.md index 7238753b22a..4557880fdcd 100644 --- a/gopls/doc/features/passive.md +++ b/gopls/doc/features/passive.md @@ -233,7 +233,7 @@ Gopls reports the following token types: Gopls also reports the following standard modifiers: -- `"defaultLibrary": predeclared symbols +- `"defaultLibrary"`: predeclared symbols - `"definition"`: the declaring identifier of a symbol - `"readonly"`: for constants diff --git a/gopls/doc/features/transformation.md b/gopls/doc/features/transformation.md index bfc30eb21d0..caf13221cfa 100644 --- a/gopls/doc/features/transformation.md +++ b/gopls/doc/features/transformation.md @@ -6,6 +6,7 @@ formatting, simplifications), code repair (fixes), and editing support (filling in struct literals and switch statements). Code transformations are not a single category in the LSP: + - A few, such as Formatting and Rename, are primary operations in the protocol. - Some transformations are exposed through [Code Lenses](../codelenses.md), @@ -43,6 +44,7 @@ or to cause the server to send other requests to the client, such as a `showDocument` request to open a report in a web browser. The main difference between code lenses and code actions is this: + - a `codeLens` request obtains commands for the entire file. Each command specifies its applicable source range, and typically appears as an annotation on that source range. @@ -68,12 +70,14 @@ Gopls supports the following code actions: - [`source.freesymbols`](web.md#freesymbols) - `source.test` (undocumented) - [`source.addTest`](#source.addTest) +- [`source.toggleCompilerOptDetails`](diagnostics.md#toggleCompilerOptDetails) - [`gopls.doc.features`](README.md), which opens gopls' index of features in a browser - [`refactor.extract.constant`](#extract) - [`refactor.extract.function`](#extract) - [`refactor.extract.method`](#extract) - [`refactor.extract.toNewFile`](#extract.toNewFile) - [`refactor.extract.variable`](#extract) +- [`refactor.extract.variable-all`](#extract) - [`refactor.inline.call`](#refactor.inline.call) - [`refactor.rewrite.changeQuote`](#refactor.rewrite.changeQuote) - [`refactor.rewrite.fillStruct`](#refactor.rewrite.fillStruct) @@ -82,6 +86,8 @@ Gopls supports the following code actions: - [`refactor.rewrite.joinLines`](#refactor.rewrite.joinLines) - [`refactor.rewrite.removeUnusedParam`](#refactor.rewrite.removeUnusedParam) - [`refactor.rewrite.splitLines`](#refactor.rewrite.splitLines) +- [`refactor.rewrite.moveParamLeft`](#refactor.rewrite.moveParamLeft) +- [`refactor.rewrite.moveParamRight`](#refactor.rewrite.moveParamRight) Gopls reports some code actions twice, with two different kinds, so that they appear in multiple UI elements: simplifications, @@ -99,6 +105,7 @@ that, in the course of reporting a diagnostic about a problem, also suggest a fix. A `codeActions` request will return any fixes accompanying diagnostics for the current selection. + Caveats: + - Many of gopls code transformations are limited by Go's syntax tree representation, which currently records comments not in the tree but in a side table; consequently, transformations such as Extract @@ -158,10 +166,12 @@ Most clients are configured to format files and organize imports whenever a file is saved. Settings: + - The [`gofumpt`](../settings.md#gofumpt) setting causes gopls to use an alternative formatter, [`github.com/mvdan/gofumpt`](https://pkg.go.dev/mvdan.cc/gofumpt). Client support: + - **VS Code**: Formats on save by default. Use `Format document` menu item (`⌥⇧F`) to invoke manually. - **Emacs + eglot**: Use `M-x eglot-format-buffer` to format. Attach it to `before-save-hook` to format on save. For formatting combined with organize-imports, many users take the legacy approach of setting `"goimports"` as their `gofmt-command` using [go-mode](https://github.com/dominikh/go-mode.el), and adding `gofmt-before-save` to `before-save-hook`. An LSP-based solution requires code such as https://github.com/joaotavora/eglot/discussions/1409. - **CLI**: `gopls format file.go` @@ -194,6 +204,7 @@ Settings: should appear after standard and third-party packages in the sort order. Client support: + - **VS Code**: automatically invokes `source.organizeImports` before save. To disable it, use the snippet below, and invoke the "Organize Imports" command manually as needed. ``` @@ -243,7 +254,7 @@ boolean. **Method receivers**: When testing a method `T.F` or `(*T).F`, the test must construct an instance of T to pass as the receiver. Gopls searches the package -for a suitable function that constructs a value of type T or *T, optionally with +for a suitable function that constructs a value of type T or \*T, optionally with an error, preferring a function named `NewT`. **Imports**: Gopls adds missing imports to the test file, using the last @@ -305,9 +316,10 @@ Similar problems may arise with packages that use reflection, such as judgment and testing. Some tips for best results: + - There is currently no special support for renaming all receivers of a family of methods at once, so you will need to rename one receiver - one at a time (golang/go#41892). + one at a time (golang/go#41892). - The safety checks performed by the Rename algorithm require type information. If the program is grossly malformed, there may be insufficient information for it to run (golang/go#41870), @@ -328,12 +340,12 @@ in the latter half of this 2015 GothamGo talk: [Using go/types for Code Comprehension and Refactoring Tools](https://www.youtube.com/watch?v=p_cz7AxVdfg). Client support: + - **VS Code**: Use "[Rename symbol](https://code.visualstudio.com/docs/editor/editingevolved#_rename-symbol)" menu item (`F2`). - **Emacs + eglot**: Use `M-x eglot-rename`, or `M-x go-rename` from [go-mode](https://github.com/dominikh/go-mode.el). - **Vim + coc.nvim**: Use the `coc-rename` command. - **CLI**: `gopls rename file.go:#offset newname` - ## `refactor.extract`: Extract function/method/variable @@ -354,14 +366,22 @@ newly created declaration that contains the selected code: will be a method of the same receiver type. - **`refactor.extract.variable`** replaces an expression by a reference to a new - local variable named `x` initialized by the expression: + local variable named `newVar` initialized by the expression: ![Before extracting a var](../assets/extract-var-before.png) ![After extracting a var](../assets/extract-var-after.png) - **`refactor.extract.constant** does the same thing for a constant expression, introducing a local const declaration. +- **`refactor.extract.variable-all`** replaces all occurrences of the selected expression +within the function with a reference to a new local variable named `newVar`. +This extracts the expression once and reuses it wherever it appears in the function. + + ![Before extracting all occurrences of EXPR](../assets/extract-var-all-before.png) + ![After extracting all occurrences of EXPR](../assets/extract-var-all-after.png) + - **`refactor.extract.constant-all** does the same thing for a constant + expression, introducing a local const declaration. If the default name for the new declaration is already in use, gopls generates a fresh name. @@ -377,10 +397,8 @@ number of cases where it falls short, including: - https://github.com/golang/go/issues/66289 - https://github.com/golang/go/issues/65944 -- https://github.com/golang/go/issues/64821 - https://github.com/golang/go/issues/63394 - https://github.com/golang/go/issues/61496 -- https://github.com/golang/go/issues/50851 The following Extract features are planned for 2024 but not yet supported: @@ -392,7 +410,6 @@ The following Extract features are planned for 2024 but not yet supported: interface type with all the methods of the selected concrete type; see golang/go#65721 and golang/go#46665. - ## `refactor.extract.toNewFile`: Extract declarations to new file @@ -409,8 +426,8 @@ first token of the declaration, such as `func` or `type`. ![Before: select the declarations to move](../assets/extract-to-new-file-before.png) ![After: the new file is based on the first symbol name](../assets/extract-to-new-file-after.png) - + ## `refactor.inline.call`: Inline call to function For a `codeActions` request where the selection is (or is within) a @@ -418,6 +435,7 @@ call of a function or method, gopls will return a command of kind `refactor.inline.call`, whose effect is to inline the function call. The screenshots below show a call to `sum` before and after inlining: + + ![Before: select Refactor... Inline call to sum](../inline-before.png) ![After: the call has been replaced by the sum logic](../inline-after.png) @@ -484,13 +503,16 @@ func f(s string) { fmt.Println(s) } ``` + a call `f("hello")` will be inlined to: + ```go func() { defer fmt.Println("goodbye") fmt.Println("hello") }() ``` + Although the parameter was eliminated, the function call remains. An inliner is a bit like an optimizing compiler. @@ -539,18 +561,17 @@ Here are some of the technical challenges involved in sound inlining: `Printf` by qualified references such as `fmt.Printf`, and add an import of package `fmt` as needed. -- **Implicit conversions:** When passing an argument to a function, it - is implicitly converted to the parameter type. - If we eliminate the parameter variable, we don't want to - lose the conversion as it may be important. - For example, in `func f(x any) { y := x; fmt.Printf("%T", &y) }` the - type of variable y is `any`, so the program prints `"*interface{}"`. - But if inlining the call `f(1)` were to produce the statement `y := - 1`, then the type of y would have changed to `int`, which could - cause a compile error or, as in this case, a bug, as the program - now prints `"*int"`. When the inliner substitutes a parameter variable - by its argument value, it may need to introduce explicit conversions - of each value to the original parameter type, such as `y := any(1)`. +- **Implicit conversions:** When passing an argument to a function, it is + implicitly converted to the parameter type. If we eliminate the parameter + variable, we don't want to lose the conversion as it may be important. For + example, in `func f(x any) { y := x; fmt.Printf("%T", &y) }` the type of + variable y is `any`, so the program prints `"*interface{}"`. But if inlining + the call `f(1)` were to produce the statement `y := 1`, then the type of y + would have changed to `int`, which could cause a compile error or, as in this + case, a bug, as the program now prints `"*int"`. When the inliner substitutes + a parameter variable by its argument value, it may need to introduce explicit + conversions of each value to the original parameter type, such as `y := + any(1)`. - **Last reference:** When an argument expression has no effects and its corresponding parameter is never used, the expression @@ -577,6 +598,7 @@ code actions whose kinds are children of `refactor.rewrite`. The [`unusedparams` analyzer](../analyzers.md#unusedparams) reports a diagnostic for each parameter that is not used within the function body. For example: + ```go func f(x, y int) { // "unused parameter: x" fmt.Println(y) @@ -607,6 +629,50 @@ 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. + + +### `refactor.rewrite.moveParam{Left,Right}`: Move function parameters + +When the selection is a parameter in a function or method signature, gopls +offers a code action to move the parameter left or right (if feasible), +updating all callers accordingly. + +For example: + +```go +func Foo(x, y int) int { + return x + y +} + +func _() { + _ = Foo(0, 1) +} +``` + +becomes + +```go +func Foo(y, x int) int { + return x + y +} + +func _() { + _ = Foo(1, 0) +} +``` + +following a request to move `x` right, or `y` left. + +This is a primitive building block of more general "Change signature" +operations. We plan to generalize this to arbitrary signature rewriting, but +the language server protocol does not currently offer good support for user +input into refactoring operations (see +[microsoft/language-server-protocol#1164](https://github.com/microsoft/language-server-protocol/issues/1164)). +Therefore, any such refactoring will require custom client-side logic. (As a +very hacky workaround, you can express arbitrary parameter movement by invoking +Rename on the `func` keyword of a function declaration, but this interface is +just a temporary stopgap.) + ### `refactor.rewrite.changeQuote`: Convert string literal between raw and interpreted @@ -673,6 +739,7 @@ func() ( z rune, ) ``` + Observe that in the last two cases, each [group](https://pkg.go.dev/go/ast#Field) of parameters or results is treated as a single item. @@ -683,6 +750,7 @@ respectively, or trivial (fewer than two items). These code actions are not offered for lists containing `//`-style comments, which run to the end of the line. + diff --git a/gopls/doc/release/v0.16.0.md b/gopls/doc/release/v0.16.0.md index 1bcb5ec3d06..7ee2775e9b1 100644 --- a/gopls/doc/release/v0.16.0.md +++ b/gopls/doc/release/v0.16.0.md @@ -1,12 +1,7 @@ # gopls/v0.16.0 - ``` -go install golang.org/x/tools/gopls@v0.16.0-pre.2 +go install golang.org/x/tools/gopls@v0.16.2 ``` This release includes several features and bug fixes, and is the first @@ -24,6 +19,7 @@ and also the last to support integrating with go command versions 1.19 and a message advising the user to upgrade. When using gopls, there are three versions to be aware of: + 1. The _gopls build go version_: the version of Go used to build gopls. 2. The _go command version_: the version of the go list command executed by gopls to load information about your workspace. @@ -97,6 +93,7 @@ cause your editor to navigate to the declaration. Editor support: + - VS Code: use the "Source action > Browse documentation for func fmt.Println" menu item. Note: source links navigate the editor but don't yet raise the window yet. Please upvote microsoft/vscode#208093 and microsoft/vscode#207634 (temporarily closed). @@ -106,7 +103,6 @@ The `linksInHover` setting now supports a new value, `"gopls"`, that causes documentation links in the the Markdown output of the Hover operation to link to gopls' internal doc viewer. - ### Browse free symbols Gopls offers another web-based code action, "Browse free symbols", @@ -133,6 +129,7 @@ the function by choosing a different type for that parameter. Editor support: + - VS Code: use the `Source action > Browse free symbols` menu item. - Emacs: requires eglot v1.17. Use `M-x go-browse-freesymbols` from github.com/dominikh/go-mode.el. @@ -157,12 +154,14 @@ Gopls cannot yet display assembly for generic functions: generic functions are not fully compiled until they are instantiated, but any function declaration enclosing the selection cannot be an instantiated generic function. + Editor support: + - VS Code: use the "Source action > Browse assembly for f" menu item. - Emacs: requires eglot v1.17. Use `M-x go-browse-assembly` from github.com/dominikh/go-mode.el. @@ -252,8 +251,7 @@ suboptimal ordering of struct fields, if this figure is 20% or higher: In the struct above, alignment rules require each of the two boolean -fields (1 byte) to occupy a complete word (8 bytes), leading to (7 + -7) / (3 * 8) = 58% waste. +fields (1 byte) to occupy a complete word (8 bytes), leading to (7 + 7) / (3 \* 8) = 58% waste. Placing the two booleans together would save a word. This information may be helpful when making space optimizations to diff --git a/gopls/doc/release/v0.17.0.md b/gopls/doc/release/v0.17.0.md index 1a278b013cb..e6af9c6bf26 100644 --- a/gopls/doc/release/v0.17.0.md +++ b/gopls/doc/release/v0.17.0.md @@ -1,8 +1,67 @@ +# gopls/v0.17.0 + + + +``` +go install golang.org/x/tools/gopls@v0.17.0-pre.4 +``` + +# New support policies + +With this release, we are narrowing our official support window to align with +the [Go support policy](https://go.dev/doc/devel/release#policy). This will +reduce the considerable costs to us of testing against older Go versions, +allowing us to spend more time fixing bugs and adding features that benefit the +majority of gopls users who run recent versions of Go. + +This narrowing is occuring in two dimensions: **build compatibility** refers to +the versions of the Go toolchain that can be used to build gopls, and **go +command compatibility** refers to the versions of the `go` command that can be +used by gopls to list information about packages and modules in your workspace. + +## Build compatibility: the most recent major Go version + +As described in the [v0.16.0 release +notes](https://github.com/golang/tools/releases/tag/gopls%2Fv0.16.0), building the +latest version of gopls will now require the latest major version of the Go +toolchain. Therefore this release (gopls@v0.17.0) must be built with Go 1.23.0 +or later. Thanks to [automatic toolchain +upgrades](https://go.dev/blog/toolchain), if your system Go version is at least +Go 1.21.0 and you have `GOTOOLCHAIN=auto` set (the default), the `go` command +will automatically download the new Go toolchain as needed, similar to +upgrading a module dependency. + +## Go command compatibility: the 2 most recent major Go versions + +The gopls@v0.17.x releases will be the final versions of gopls to nominally +support integrating with more than the 2 most recent Go releases. In the past, +we implied "best effort" support for up to 4 versions, though in practice we +did not have resources to fix bugs that were present only with older Go +versions. With gopls@v0.17.0, we narrowed this best effort support to 3 +versions, primarily because users need at least Go 1.21 to benefit from +automatic toolchain upgrades (see above). + +Starting with gopls@v0.18.0, we will officially support integrating with only +the 2 most recent major versions of the `go` command. This is consistent with +the Go support policy. See golang/go#69321 (or [this +comment](https://github.com/golang/go/issues/69321#issuecomment-2344996677) +specifically) for details. + +We won't prevent gopls from being used with older Go versions (just as we +don't disallow integration with arbitrary +[`go/packages`](https://pkg.go.dev/golang.org/x/tools/go/packages) drivers), +but we won't run integration tests against older Go versions, and won't fix +bugs that are only present when used with old Go versions. + # Configuration Changes - 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 `undeclaredname` analyzer has been replaced with an ordinary code action. - 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`. @@ -13,16 +72,31 @@ # New features -## Change signature refactoring +## Refactoring + +This release contains a number of new features related to refactoring. +Additionally, it fixes [many +bugs](https://github.com/golang/go/issues?q=is%3Aissue+milestone%3Agopls%2Fv0.17.0+label%3ARefactoring+is%3Aclosed) +in existing refactoring operations, primarily related to **extract**, and **inline**. -TODO(rfindley): document the state of change signature refactoring once the -feature set stabilizes. +These improvements move us toward a longer term goal of offering a more robust +and complete set of refactoring tools. We still have [much to +do](https://github.com/golang/go/issues?q=is%3Aissue+label%3Agopls+label%3ARefactoring+is%3Aopen+), +and this effort will continue into 2025. -## Improvements to existing refactoring operations +### Move parameter refactorings -TODO(rfindley): document the full set of improvements to rename/extract/inline. +Gopls now offers code actions to move function and method parameters left or +right in the function signature, updating all callers. -## Extract declarations to new file +Unfortunately, there is no native LSP operation that provides a good user +interface for arbitrary "change signature" refactoring. We plan to build such +an interface within VS Code. In the short term, we have made it possible to +express more complicated parameter transformations by invoking 'rename' on the +'func' keyword. This user interface is a temporary stop-gap until a better +mechanism is available for LSP commands that enable client-side dialogs. + +### Extract declarations to new file Gopls now offers another code action, "Extract declarations to new file" (`refactor.extract.toNewFile`), @@ -38,7 +112,7 @@ or by selecting a whole declaration or multiple declarations. In order to avoid ambiguity and surprise about what to extract, some kinds of paritial selection of a declaration cannot invoke this code action. -## Extract constant +### Extract constant When the selection is a constant expression, gopls now offers "Extract constant" instead of "Extract variable", and generates a `const` @@ -47,7 +121,27 @@ declaration instead of a local variable. Also, extraction of a constant or variable now works at top-level, outside of any function. -## Pull diagnostics +### Generate missing method from function call + +When you attempt to call a method on a type that lacks that method, the +compiler will report an error like “type T has no field or method f”. Gopls now +offers a new code action, “Declare missing method of T.f”, where T is the +concrete type and f is the undefined method. The stub method's signature is +inferred from the context of the call. + +### Generate a test for a function or method + +If the selected chunk of code is part of a function or method declaration F, +gopls will offer the "Add test for F" code action, which adds a new test for the +selected function in the corresponding `_test.go` file. The generated test takes +into account its signature, including input parameters and results. + +Since this feature is implemented by the server (gopls), it is compatible with +all LSP-compliant editors. VS Code users may continue to use the client-side +`Go: Generate Unit Tests For file/function/package` command, which runs the +[gotests](https://github.com/cweill/gotests) tool. + +## Initial support for pull diagnostics When initialized with the option `"pullDiagnostics": true`, gopls will advertise support for the `textDocument.diagnostic` @@ -55,7 +149,7 @@ When initialized with the option `"pullDiagnostics": true`, gopls will advertise which allows editors to request diagnostics directly from gopls using a `textDocument/diagnostic` request, rather than wait for a `textDocument/publishDiagnostics` notification. This feature is off by default -until the performance of pull diagnostics is comparable to push diagnostics. +until the feature set of pull diagnostics is comparable to push diagnostics. ## Hover improvements @@ -88,15 +182,6 @@ 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. -## Generate missing method from function call - -When you attempt to call a method on a type that does not have that method, -the compiler will report an error like “type X has no field or method Y”. -Gopls now offers a new code action, “Declare missing method of T.f”, -where T is the concrete type and f is the undefined method. -The stub method's signature is inferred -from the context of the call. - ## `yield` analyzer The new `yield` analyzer detects mistakes using the `yield` function @@ -111,15 +196,3 @@ causing `Add` to race with `Wait`. (This check is equivalent to [staticcheck's SA2000](https://staticcheck.dev/docs/checks#SA2000), but is enabled by default.) - -## Add test for function or method - -If the selected chunk of code is part of a function or method declaration F, -gopls will offer the "Add test for F" code action, which adds a new test for the -selected function in the corresponding `_test.go` file. The generated test takes -into account its signature, including input parameters and results. - -Since this feature is implemented by the server (gopls), it is compatible with -all LSP-compliant editors. VS Code users may continue to use the client-side -`Go: Generate Unit Tests For file/function/package` command which utilizes the -[gotests](https://github.com/cweill/gotests) tool. \ No newline at end of file diff --git a/gopls/doc/release/v0.18.0.md b/gopls/doc/release/v0.18.0.md new file mode 100644 index 00000000000..9f7ddd0909b --- /dev/null +++ b/gopls/doc/release/v0.18.0.md @@ -0,0 +1,86 @@ +# Configuration Changes + +- The experimental `hoverKind=Structured` setting is no longer supported. + +- The `gc_details` code lens has been deleted. (It was previously + disabled by default.) This functionality is now available through + the `settings.toggleCompilerOptDetails` code action (documented + below), as code actions are better supported than code lenses across + a range of clients. + + VS Code's special "Go: Toggle GC details" command continues to work. + +# New features + +## "Toggle compiler optimization details" code action + +This code action, accessible through the "Source Action" menu in VS +Code, toggles a per-package flag that causes Go compiler optimization +details to be reported as diagnostics. For example, it indicates which +variables escape to the heap, and which array accesses require bounds +checks. + +## New `modernize` analyzer + +Gopls now reports when code could be simplified or clarified by +using more modern features of Go, and provides a quick fix to apply +the change. + +Examples: + +- replacement of conditional assignment using an if/else statement by + a call to the `min` or `max` built-in functions added in Go 1.18; + +## New `unusedfunc` analyzer + +Gopls now reports unused functions and methods, giving you near +real-time feedback about dead code that may be safely deleted. +Because the analysis is local to each package, only unexported +functions and methods are candidates. +(For a more precise analysis that may report unused exported +functions too, use the `golang.org/x/tools/cmd/deadcode` command.) + +## "Implementations" supports generics + +At long last, the "Go to Implementations" feature now fully supports +generic types and functions (#59224). + +For example, invoking the feature on the interface method `Stack.Push` +below will report the concrete method `C[T].Push`, and vice versa. + +```go +package p + +type Stack[T any] interface { + Push(T) error + Pop() (T, bool) +} + +type C[T any] struct{} + +func (C[T]) Push(t T) error { ... } +func (C[T]) Pop() (T, bool) { ... } + +var _ Stack[int] = C[int]{} +``` + +## Extract all occurrences of the same expression under selection + +When you have multiple instances of the same expression in a function, +you can use this code action to extract it into a variable. +All occurrences of the expression will be replaced with a reference to the new variable. + +## Improvements to "Definition" + +The Definition query now supports additional locations: + +- When invoked on a return statement, it reports the location + of the function's result variables. +- When invoked on a break, goto, or continue statement, it reports + the location of the label, the closing brace of the relevant + block statement, or the start of the relevant loop, respectively. + +## Improvements to "Hover" + +When invoked on a return statement, hover reports the types of + the function's result variables. diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 135fcca70af..1350e8f7840 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -184,13 +184,12 @@ Example Usage: ... "codelenses": { "generate": false, // Don't show the `go generate` lens. - "gc_details": true // Show a code lens toggling the display of gc's choices. } ... } ``` -Default: `{"gc_details":false,"generate":true,"regenerate_cgo":true,"run_govulncheck":false,"tidy":true,"upgrade_dependency":true,"vendor":true}`. +Default: `{"generate":true,"regenerate_cgo":true,"run_govulncheck":false,"tidy":true,"upgrade_dependency":true,"vendor":true}`. ### `semanticTokens bool` @@ -316,23 +315,6 @@ These analyses are documented on Default: `false`. - -### `annotations map[enum]bool` - -**This setting is experimental and may be deleted.** - -annotations specifies the various kinds of optimization diagnostics -that should be reported by the gc_details command. - -Each enum must be one of: - -* `"bounds"` controls bounds checking diagnostics. -* `"escape"` controls diagnostics about escape choices. -* `"inline"` controls diagnostics about inlining choices. -* `"nil"` controls nil checks. - -Default: `{"bounds":true,"escape":true,"inline":true,"nil":true}`. - ### `vulncheck enum` @@ -399,17 +381,13 @@ Default: `true`. ### `hoverKind enum` hoverKind controls the information that appears in the hover text. -SingleLine and Structured are intended for use only by authors of editor plugins. +SingleLine is intended for use only by authors of editor plugins. Must be one of: * `"FullDocumentation"` * `"NoDocumentation"` * `"SingleLine"` -* `"Structured"` is an experimental setting that returns a structured hover format. -This format separates the signature from the documentation, so that the client -can do more manipulation of these fields.\ -This should only be used by clients that support this behavior. * `"SynopsisDocumentation"` Default: `"FullDocumentation"`. diff --git a/gopls/go.mod b/gopls/go.mod index 03f7956025d..173614714cc 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -1,19 +1,19 @@ module golang.org/x/tools/gopls -// go 1.23.1 fixes some bugs in go/types Alias support. -// (golang/go#68894 and golang/go#68905). -go 1.23.1 +// go 1.23.1 fixes some bugs in go/types Alias support (golang/go#68894, golang/go#68905). +// go 1.23.4 fixes a miscompilation of range-over-func (golang/go#70035). +go 1.23.4 require ( github.com/google/go-cmp v0.6.0 - github.com/jba/templatecheck v0.7.0 + github.com/jba/templatecheck v0.7.1 golang.org/x/mod v0.22.0 golang.org/x/sync v0.10.0 - golang.org/x/sys v0.28.0 - golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 + golang.org/x/sys v0.29.0 + golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9 golang.org/x/text v0.21.0 - golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 - golang.org/x/vuln v1.0.4 + golang.org/x/tools v0.28.0 + golang.org/x/vuln v1.1.3 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.5.1 mvdan.cc/gofumpt v0.7.0 @@ -23,7 +23,7 @@ require ( require ( github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/google/safehtml v0.1.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/gopls/go.sum b/gopls/go.sum index 7785bbed7f6..bba08403559 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -6,8 +6,8 @@ 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= github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= -github.com/jba/templatecheck v0.7.0 h1:wjTb/VhGgSFeim5zjWVePBdaMo28X74bGLSABZV+zIA= -github.com/jba/templatecheck v0.7.0/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo= +github.com/jba/templatecheck v0.7.1 h1:yOEIFazBEwzdTPYHZF3Pm81NF1ksxx1+vJncSEwvjKc= +github.com/jba/templatecheck v0.7.1/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -16,16 +16,16 @@ 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.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= -golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 h1:1xaZTydL5Gsg78QharTwKfA9FY9CZ1VQj6D/AZEvHR0= +golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.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.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -33,21 +33,21 @@ 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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.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-20241106142447-58a1122356f5 h1:TCDqnvbBsFapViksHcHySl/sW4+rTGNIAoJJesHRuMM= -golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5/go.mod h1:8nZWdGp9pq73ZI//QJyckMQab3yq7hoWi7SI0UIusVI= +golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9 h1:L2k9GUV2TpQKVRGMjN94qfUMgUwOFimSQ6gipyJIjKw= +golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9/go.mod h1:8h4Hgq+jcTvCDv2+i7NrfWwpYHcESleo2nGHxLbFLJ4= golang.org/x/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.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= -golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ= +golang.org/x/vuln v1.1.3 h1:NPGnvPOTgnjBc9HTaUx+nj+EaUYxl5SJOWqaDYGaFYw= +golang.org/x/vuln v1.1.3/go.mod h1:7Le6Fadm5FOqE9C926BCD0g12NWyhg7cxV4BwcPFuNY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/gopls/internal/analysis/fillreturns/fillreturns.go b/gopls/internal/analysis/fillreturns/fillreturns.go index 145b03f4a42..8a602dc2eef 100644 --- a/gopls/internal/analysis/fillreturns/fillreturns.go +++ b/gopls/internal/analysis/fillreturns/fillreturns.go @@ -155,6 +155,7 @@ outer: retTyps = append(retTyps, retTyp) } matches := analysisinternal.MatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) + qual := typesinternal.FileQualifier(file, pass.Pkg) for i, retTyp := range retTyps { var match ast.Expr var idx int @@ -184,7 +185,7 @@ outer: // If no identifier matches the pattern, generate a zero value. if best := fuzzy.BestMatch(retTyp.String(), names); best != "" { fixed[i] = ast.NewIdent(best) - } else if zero := typesinternal.ZeroExpr(file, pass.Pkg, retTyp); zero != nil { + } else if zero, isValid := typesinternal.ZeroExpr(retTyp, qual); isValid { fixed[i] = zero } else { return nil, nil diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go index 6fa64182a07..1181693c3d9 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/analysis/fillstruct/fillstruct.go @@ -199,6 +199,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil fieldTyps = append(fieldTyps, field.Type()) } matches := analysisinternal.MatchingIdents(fieldTyps, file, start, info, pkg) + qual := typesinternal.FileQualifier(file, pkg) var elts []ast.Expr for i, fieldTyp := range fieldTyps { if fieldTyp == nil { @@ -232,8 +233,8 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil // NOTE: We currently match on the name of the field key rather than the field type. if best := fuzzy.BestMatch(fieldName, names); best != "" { kv.Value = ast.NewIdent(best) - } else if v := populateValue(file, pkg, fieldTyp); v != nil { - kv.Value = v + } else if expr, isValid := populateValue(fieldTyp, qual); isValid { + kv.Value = expr } else { return nil, nil, nil // no fix to suggest } @@ -329,69 +330,43 @@ func indent(str, ind []byte) []byte { // The reasoning here is that users will call fillstruct with the intention of // initializing the struct, in which case setting these fields to nil has no effect. // -// populateValue returns nil if the value cannot be filled. -func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { - switch u := typ.Underlying().(type) { - case *types.Basic: - switch { - case u.Info()&types.IsNumeric != 0: - return &ast.BasicLit{Kind: token.INT, Value: "0"} - case u.Info()&types.IsBoolean != 0: - return &ast.Ident{Name: "false"} - case u.Info()&types.IsString != 0: - return &ast.BasicLit{Kind: token.STRING, Value: `""`} - case u.Kind() == types.UnsafePointer: - return ast.NewIdent("nil") - case u.Kind() == types.Invalid: - return nil +// If the input contains an invalid type, populateValue may panic or return +// expression that may not compile. +func populateValue(typ types.Type, qual types.Qualifier) (_ ast.Expr, isValid bool) { + switch t := typ.(type) { + case *types.TypeParam, *types.Interface, *types.Struct, *types.Basic: + return typesinternal.ZeroExpr(t, qual) + + case *types.Alias, *types.Named: + switch t.Underlying().(type) { + // Avoid typesinternal.ZeroExpr here as we don't want to return nil. + case *types.Map, *types.Slice: + return &ast.CompositeLit{ + Type: typesinternal.TypeExpr(t, qual), + }, true default: - panic(fmt.Sprintf("unknown basic type %v", u)) + return typesinternal.ZeroExpr(t, qual) } - case *types.Map: - k := typesinternal.TypeExpr(f, pkg, u.Key()) - v := typesinternal.TypeExpr(f, pkg, u.Elem()) - if k == nil || v == nil { - return nil - } - return &ast.CompositeLit{ - Type: &ast.MapType{ - Key: k, - Value: v, - }, - } - case *types.Slice: - s := typesinternal.TypeExpr(f, pkg, u.Elem()) - if s == nil { - return nil - } + // Avoid typesinternal.ZeroExpr here as we don't want to return nil. + case *types.Map, *types.Slice: return &ast.CompositeLit{ - Type: &ast.ArrayType{ - Elt: s, - }, - } + Type: typesinternal.TypeExpr(t, qual), + }, true case *types.Array: - a := typesinternal.TypeExpr(f, pkg, u.Elem()) - if a == nil { - return nil - } return &ast.CompositeLit{ Type: &ast.ArrayType{ - Elt: a, + Elt: typesinternal.TypeExpr(t.Elem(), qual), Len: &ast.BasicLit{ - Kind: token.INT, Value: fmt.Sprintf("%v", u.Len()), + Kind: token.INT, Value: fmt.Sprintf("%v", t.Len()), }, }, - } + }, true case *types.Chan: - v := typesinternal.TypeExpr(f, pkg, u.Elem()) - if v == nil { - return nil - } - dir := ast.ChanDir(u.Dir()) - if u.Dir() == types.SendRecv { + dir := ast.ChanDir(t.Dir()) + if t.Dir() == types.SendRecv { dir = ast.SEND | ast.RECV } return &ast.CallExpr{ @@ -399,60 +374,35 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { Args: []ast.Expr{ &ast.ChanType{ Dir: dir, - Value: v, + Value: typesinternal.TypeExpr(t.Elem(), qual), }, }, - } - - case *types.Struct: - s := typesinternal.TypeExpr(f, pkg, typ) - if s == nil { - return nil - } - return &ast.CompositeLit{ - Type: s, - } + }, true case *types.Signature: - var params []*ast.Field - for i := 0; i < u.Params().Len(); i++ { - p := typesinternal.TypeExpr(f, pkg, u.Params().At(i).Type()) - if p == nil { - return nil - } - params = append(params, &ast.Field{ - Type: p, - Names: []*ast.Ident{ - { - Name: u.Params().At(i).Name(), - }, - }, - }) - } - var returns []*ast.Field - for i := 0; i < u.Results().Len(); i++ { - r := typesinternal.TypeExpr(f, pkg, u.Results().At(i).Type()) - if r == nil { - return nil - } - returns = append(returns, &ast.Field{ - Type: r, - }) - } return &ast.FuncLit{ - Type: &ast.FuncType{ - Params: &ast.FieldList{ - List: params, - }, - Results: &ast.FieldList{ - List: returns, + Type: typesinternal.TypeExpr(t, qual).(*ast.FuncType), + // The body of the function literal contains a panic statement to + // avoid type errors. + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: ast.NewIdent("panic"), + Args: []ast.Expr{ + &ast.BasicLit{ + Kind: token.STRING, + Value: `"TODO"`, + }, + }, + }, + }, }, }, - Body: &ast.BlockStmt{}, - } + }, true case *types.Pointer: - switch types.Unalias(u.Elem()).(type) { + switch tt := types.Unalias(t.Elem()).(type) { case *types.Basic: return &ast.CallExpr{ Fun: &ast.Ident{ @@ -460,38 +410,31 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { }, Args: []ast.Expr{ &ast.Ident{ - Name: u.Elem().String(), + Name: t.Elem().String(), }, }, - } + }, true + // Pointer to type parameter should return new(T) instead of &*new(T). + case *types.TypeParam: + return &ast.CallExpr{ + Fun: &ast.Ident{ + Name: "new", + }, + Args: []ast.Expr{ + &ast.Ident{ + Name: tt.Obj().Name(), + }, + }, + }, true default: - x := populateValue(f, pkg, u.Elem()) - if x == nil { - return nil - } + // TODO(hxjiang): & prefix only works if populateValue returns a + // composite literal T{} or the expression new(T). + expr, isValid := populateValue(t.Elem(), qual) return &ast.UnaryExpr{ Op: token.AND, - X: x, - } - } - - case *types.Interface: - 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, - // always a number or a pointer. See go/ssa for details. - return &ast.StarExpr{ - X: &ast.CallExpr{ - Fun: ast.NewIdent("new"), - Args: []ast.Expr{ - ast.NewIdent(param.Obj().Name()), - }, - }, - } + X: expr, + }, isValid } - - return ast.NewIdent("nil") } - return nil + return nil, false } diff --git a/gopls/internal/analysis/modernize/bloop.go b/gopls/internal/analysis/modernize/bloop.go new file mode 100644 index 00000000000..18be946281e --- /dev/null +++ b/gopls/internal/analysis/modernize/bloop.go @@ -0,0 +1,209 @@ +// 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 modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/astutil/cursor" + "golang.org/x/tools/internal/typesinternal" +) + +// bloop updates benchmarks that use "for range b.N", replacing it +// with go1.24's b.Loop() and eliminating any preceding +// b.{Start,Stop,Reset}Timer calls. +// +// Variants: +// +// for i := 0; i < b.N; i++ {} => for b.Loop() {} +// for range b.N {} +func bloop(pass *analysis.Pass) { + if !_imports(pass.Pkg, "testing") { + return + } + + info := pass.TypesInfo + + // edits computes the text edits for a matched for/range loop + // at the specified cursor. b is the *testing.B value, and + // (start, end) is the portion using b.N to delete. + edits := func(cur cursor.Cursor, b ast.Expr, start, end token.Pos) (edits []analysis.TextEdit) { + // Within the same function, delete all calls to + // b.{Start,Stop,Timer} that precede the loop. + filter := []ast.Node{(*ast.ExprStmt)(nil), (*ast.FuncLit)(nil)} + fn, _ := enclosingFunc(cur) + fn.Inspect(filter, func(cur cursor.Cursor, push bool) (descend bool) { + if push { + node := cur.Node() + if is[*ast.FuncLit](node) { + return false // don't descend into FuncLits (e.g. sub-benchmarks) + } + stmt := node.(*ast.ExprStmt) + if stmt.Pos() > start { + return false // not preceding: stop + } + if call, ok := stmt.X.(*ast.CallExpr); ok { + fn := typeutil.StaticCallee(info, call) + if fn != nil && + (isMethod(fn, "testing", "B", "StopTimer") || + isMethod(fn, "testing", "B", "StartTimer") || + isMethod(fn, "testing", "B", "ResetTimer")) { + + // Delete call statement. + // TODO(adonovan): delete following newline, or + // up to start of next stmt? (May delete a comment.) + edits = append(edits, analysis.TextEdit{ + Pos: stmt.Pos(), + End: stmt.End(), + }) + } + } + } + return true + }) + + // Replace ...b.N... with b.Loop(). + return append(edits, analysis.TextEdit{ + Pos: start, + End: end, + NewText: fmt.Appendf(nil, "%s.Loop()", formatNode(pass.Fset, b)), + }) + } + + // Find all for/range statements. + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + loops := []ast.Node{ + (*ast.ForStmt)(nil), + (*ast.RangeStmt)(nil), + } + for curFile := range filesUsing(inspect, info, "go1.24") { + for curLoop := range curFile.Preorder(loops...) { + switch n := curLoop.Node().(type) { + case *ast.ForStmt: + // for _; i < b.N; _ {} + if cmp, ok := n.Cond.(*ast.BinaryExpr); ok && cmp.Op == token.LSS { + if sel, ok := cmp.Y.(*ast.SelectorExpr); ok && + sel.Sel.Name == "N" && + isPtrToNamed(info.TypeOf(sel.X), "testing", "B") { + + delStart, delEnd := n.Cond.Pos(), n.Cond.End() + + // Eliminate variable i if no longer needed: + // for i := 0; i < b.N; i++ { + // ...no references to i... + // } + body, _ := curLoop.LastChild() + if assign, ok := n.Init.(*ast.AssignStmt); ok && + assign.Tok == token.DEFINE && + len(assign.Rhs) == 1 && + isZeroLiteral(assign.Rhs[0]) && + is[*ast.IncDecStmt](n.Post) && + n.Post.(*ast.IncDecStmt).Tok == token.INC && + equalSyntax(n.Post.(*ast.IncDecStmt).X, assign.Lhs[0]) && + !uses(info, body, info.Defs[assign.Lhs[0].(*ast.Ident)]) { + + delStart, delEnd = n.Init.Pos(), n.Post.End() + } + + pass.Report(analysis.Diagnostic{ + // Highlight "i < b.N". + Pos: n.Cond.Pos(), + End: n.Cond.End(), + Category: "bloop", + Message: "b.N can be modernized using b.Loop()", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace b.N with b.Loop()", + TextEdits: edits(curLoop, sel.X, delStart, delEnd), + }}, + }) + } + } + + case *ast.RangeStmt: + // for range b.N {} -> for b.Loop() {} + // + // TODO(adonovan): handle "for i := range b.N". + if sel, ok := n.X.(*ast.SelectorExpr); ok && + n.Key == nil && + n.Value == nil && + sel.Sel.Name == "N" && + isPtrToNamed(info.TypeOf(sel.X), "testing", "B") { + + pass.Report(analysis.Diagnostic{ + // Highlight "range b.N". + Pos: n.Range, + End: n.X.End(), + Category: "bloop", + Message: "b.N can be modernized using b.Loop()", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace b.N with b.Loop()", + TextEdits: edits(curLoop, sel.X, n.Range, n.X.End()), + }}, + }) + } + } + } + } +} + +// isPtrToNamed reports whether t is type "*pkgpath.Name". +func isPtrToNamed(t types.Type, pkgpath, name string) bool { + if ptr, ok := t.(*types.Pointer); ok { + named, ok := ptr.Elem().(*types.Named) + return ok && + named.Obj().Name() == name && + named.Obj().Pkg().Path() == pkgpath + } + return false +} + +// uses reports whether the subtree cur contains a use of obj. +func uses(info *types.Info, cur cursor.Cursor, obj types.Object) bool { + for curId := range cur.Preorder((*ast.Ident)(nil)) { + if info.Uses[curId.Node().(*ast.Ident)] == obj { + return true + } + } + return false +} + +// isMethod reports whether fn is pkgpath.(T).Name. +func isMethod(fn *types.Func, pkgpath, T, name string) bool { + if recv := fn.Signature().Recv(); recv != nil { + _, recvName := typesinternal.ReceiverNamed(recv) + return recvName != nil && + isPackageLevel(recvName.Obj(), pkgpath, T) && + fn.Name() == name + } + return false +} + +// enclosingFunc returns the cursor for the innermost Func{Decl,Lit} +// that encloses (or is) c, if any. +// +// TODO(adonovan): consider adding: +// +// func (Cursor) AnyEnclosing(filter ...ast.Node) (Cursor bool) +// func (Cursor) Enclosing[N ast.Node]() (Cursor, bool) +// +// See comments at [cursor.Cursor.Stack]. +func enclosingFunc(c cursor.Cursor) (cursor.Cursor, bool) { + for { + switch c.Node().(type) { + case *ast.FuncLit, *ast.FuncDecl: + return c, true + case nil: + return cursor.Cursor{}, false + } + c = c.Parent() + } +} diff --git a/gopls/internal/analysis/modernize/doc.go b/gopls/internal/analysis/modernize/doc.go new file mode 100644 index 00000000000..379e29b9b0b --- /dev/null +++ b/gopls/internal/analysis/modernize/doc.go @@ -0,0 +1,26 @@ +// 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 modernize providers the modernizer analyzer. +// +// # Analyzer modernize +// +// modernize: simplify code by using modern constructs +// +// This analyzer reports opportunities for simplifying and clarifying +// existing code by using more modern features of Go, such as: +// +// - replacing an if/else conditional assignment by a call to the +// built-in min or max functions added in go1.21; +// - replacing sort.Slice(x, func(i, j int) bool) { return s[i] < s[j] } +// by a call to slices.Sort(s), added in go1.21; +// - replacing interface{} by the 'any' type added in go1.18; +// - replacing append([]T(nil), s...) by slices.Clone(s) or +// slices.Concat(s), added in go1.21; +// - replacing a loop around an m[k]=v map update by a call +// to one of the Collect, Copy, Clone, or Insert functions +// from the maps package, added in go1.21; +// - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...), +// added in go1.19; +package modernize diff --git a/gopls/internal/analysis/modernize/efaceany.go b/gopls/internal/analysis/modernize/efaceany.go new file mode 100644 index 00000000000..e22094fee30 --- /dev/null +++ b/gopls/internal/analysis/modernize/efaceany.go @@ -0,0 +1,50 @@ +// 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 modernize + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +// The efaceany pass replaces interface{} with go1.18's 'any'. +func efaceany(pass *analysis.Pass) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.18") { + file := curFile.Node().(*ast.File) + + for curIface := range curFile.Preorder((*ast.InterfaceType)(nil)) { + iface := curIface.Node().(*ast.InterfaceType) + + if iface.Methods.NumFields() == 0 { + // Check that 'any' is not shadowed. + // TODO(adonovan): find scope using only local Cursor operations. + scope := pass.TypesInfo.Scopes[file].Innermost(iface.Pos()) + if _, obj := scope.LookupParent("any", iface.Pos()); obj == builtinAny { + pass.Report(analysis.Diagnostic{ + Pos: iface.Pos(), + End: iface.End(), + Category: "efaceany", + Message: "interface{} can be replaced by any", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace interface{} by any", + TextEdits: []analysis.TextEdit{ + { + Pos: iface.Pos(), + End: iface.End(), + NewText: []byte("any"), + }, + }, + }}, + }) + } + } + } + } +} diff --git a/gopls/internal/analysis/modernize/fmtappendf.go b/gopls/internal/analysis/modernize/fmtappendf.go new file mode 100644 index 00000000000..dd1013e511a --- /dev/null +++ b/gopls/internal/analysis/modernize/fmtappendf.go @@ -0,0 +1,73 @@ +// 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 modernize + +import ( + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +// The fmtappend function replaces []byte(fmt.Sprintf(...)) by +// fmt.Appendf(nil, ...). +func fmtappendf(pass *analysis.Pass) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + info := pass.TypesInfo + for curFile := range filesUsing(inspect, info, "go1.19") { + for curCallExpr := range curFile.Preorder((*ast.CallExpr)(nil)) { + conv := curCallExpr.Node().(*ast.CallExpr) + tv := info.Types[conv.Fun] + if tv.IsType() && types.Identical(tv.Type, byteSliceType) { + call, ok := conv.Args[0].(*ast.CallExpr) + if ok { + var appendText = "" + var id *ast.Ident + if id = isQualifiedIdent(info, call.Fun, "fmt", "Sprintf"); id != nil { + appendText = "Appendf" + } else if id = isQualifiedIdent(info, call.Fun, "fmt", "Sprint"); id != nil { + appendText = "Append" + } else if id = isQualifiedIdent(info, call.Fun, "fmt", "Sprintln"); id != nil { + appendText = "Appendln" + } else { + continue + } + pass.Report(analysis.Diagnostic{ + Pos: conv.Pos(), + End: conv.End(), + Category: "fmtappendf", + Message: "Replace []byte(fmt.Sprintf...) with fmt.Appendf", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace []byte(fmt.Sprintf...) with fmt.Appendf", + TextEdits: []analysis.TextEdit{ + { + // delete "[]byte(" + Pos: conv.Pos(), + End: conv.Lparen + 1, + }, + { + // remove ")" + Pos: conv.Rparen, + End: conv.Rparen + 1, + }, + { + Pos: id.Pos(), + End: id.End(), + NewText: []byte(appendText), // replace Sprint with Append + }, + { + Pos: call.Lparen + 1, + NewText: []byte("nil, "), + }, + }, + }}, + }) + } + } + } + } +} diff --git a/gopls/internal/analysis/modernize/main.go b/gopls/internal/analysis/modernize/main.go new file mode 100644 index 00000000000..e1276e333ae --- /dev/null +++ b/gopls/internal/analysis/modernize/main.go @@ -0,0 +1,16 @@ +// 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 ignore + +// The modernize command suggests (or, with -fix, applies) fixes that +// clarify Go code by using more modern features. +package main + +import ( + "golang.org/x/tools/go/analysis/singlechecker" + "golang.org/x/tools/gopls/internal/analysis/modernize" +) + +func main() { singlechecker.Main(modernize.Analyzer) } diff --git a/gopls/internal/analysis/modernize/maps.go b/gopls/internal/analysis/modernize/maps.go new file mode 100644 index 00000000000..071d074533a --- /dev/null +++ b/gopls/internal/analysis/modernize/maps.go @@ -0,0 +1,220 @@ +// 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 modernize + +// This file defines modernizers that use the "maps" package. + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/astutil/cursor" + "golang.org/x/tools/internal/typeparams" +) + +// The mapsloop pass offers to simplify a loop of map insertions: +// +// for k, v := range x { +// m[k] = v +// } +// +// by a call to go1.23's maps package. There are four variants, the +// product of two axes: whether the source x is a map or an iter.Seq2, +// and whether the destination m is a newly created map: +// +// maps.Copy(m, x) (x is map) +// maps.Insert(m, x) (x is iter.Seq2) +// m = maps.Clone(x) (x is map, m is a new map) +// m = maps.Collect(x) (x is iter.Seq2, m is a new map) +// +// A map is newly created if the preceding statement has one of these +// forms, where M is a map type: +// +// m = make(M) +// m = M{} +func mapsloop(pass *analysis.Pass) { + if pass.Pkg.Path() == "maps " { + return + } + + info := pass.TypesInfo + + // check is called for each statement of this form: + // for k, v := range x { m[k] = v } + check := func(file *ast.File, curRange cursor.Cursor, assign *ast.AssignStmt, m, x ast.Expr) { + + // Is x a map or iter.Seq2? + tx := types.Unalias(info.TypeOf(x)) + var xmap bool + switch typeparams.CoreType(tx).(type) { + case *types.Map: + xmap = true + + case *types.Signature: + k, v, ok := assignableToIterSeq2(tx) + if !ok { + return // a named isomer of Seq2 + } + xmap = false + + // Record in tx the unnamed map[K]V type + // derived from the yield function. + // This is the type of maps.Collect(x). + tx = types.NewMap(k, v) + + default: + return // e.g. slice, channel (or no core type!) + } + + // Is the preceding statement of the form + // m = make(M) or M{} + // and can we replace its RHS with slices.{Clone,Collect}? + var mrhs ast.Expr // make(M) or M{}, or nil + if curPrev, ok := curRange.PrevSibling(); ok { + if assign, ok := curPrev.Node().(*ast.AssignStmt); ok && + len(assign.Lhs) == 1 && + len(assign.Rhs) == 1 && + equalSyntax(assign.Lhs[0], m) { + + // Have: m = rhs; for k, v := range x { m[k] = v } + var newMap bool + rhs := assign.Rhs[0] + switch rhs := rhs.(type) { + case *ast.CallExpr: + if id, ok := rhs.Fun.(*ast.Ident); ok && + info.Uses[id] == builtinMake { + // Have: m = make(...) + newMap = true + } + case *ast.CompositeLit: + if len(rhs.Elts) == 0 { + // Have m = M{} + newMap = true + } + } + + // Take care not to change type of m's RHS expression. + if newMap { + trhs := info.TypeOf(rhs) + + // Inv: tx is the type of maps.F(x) + // - maps.Clone(x) has the same type as x. + // - maps.Collect(x) returns an unnamed map type. + + if assign.Tok == token.DEFINE { + // DEFINE (:=): we must not + // change the type of RHS. + if types.Identical(tx, trhs) { + mrhs = rhs + } + } else { + // ASSIGN (=): the types of LHS + // and RHS may differ in namedness. + if types.AssignableTo(tx, trhs) { + mrhs = rhs + } + } + } + } + } + + // Choose function, report diagnostic, and suggest fix. + rng := curRange.Node() + mapsName, importEdits := analysisinternal.AddImport(info, file, rng.Pos(), "maps", "maps") + var ( + funcName string + newText []byte + start, end token.Pos + ) + if mrhs != nil { + // Replace RHS of preceding m=... assignment (and loop) with expression. + start, end = mrhs.Pos(), rng.End() + funcName = cond(xmap, "Clone", "Collect") + newText = fmt.Appendf(nil, "%s.%s(%s)", + mapsName, + funcName, + formatNode(pass.Fset, x)) + } else { + // Replace loop with call statement. + start, end = rng.Pos(), rng.End() + funcName = cond(xmap, "Copy", "Insert") + newText = fmt.Appendf(nil, "%s.%s(%s, %s)", + mapsName, + funcName, + formatNode(pass.Fset, m), + formatNode(pass.Fset, x)) + } + pass.Report(analysis.Diagnostic{ + Pos: assign.Lhs[0].Pos(), + End: assign.Lhs[0].End(), + Category: "mapsloop", + Message: "Replace m[k]=v loop with maps." + funcName, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace m[k]=v loop with maps." + funcName, + TextEdits: append(importEdits, []analysis.TextEdit{{ + Pos: start, + End: end, + NewText: newText, + }}...), + }}, + }) + + } + + // Find all range loops around m[k] = v. + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.23") { + file := curFile.Node().(*ast.File) + + for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) { + rng := curRange.Node().(*ast.RangeStmt) + + if rng.Tok == token.DEFINE && rng.Key != nil && rng.Value != nil && len(rng.Body.List) == 1 { + // Have: for k, v := range x { S } + if assign, ok := rng.Body.List[0].(*ast.AssignStmt); ok && len(assign.Lhs) == 1 { + if index, ok := assign.Lhs[0].(*ast.IndexExpr); ok && + equalSyntax(rng.Key, index.Index) && + equalSyntax(rng.Value, assign.Rhs[0]) { + + // Have: for k, v := range x { m[k] = v } + check(file, curRange, assign, index.X, rng.X) + } + } + } + } + } +} + +// assignableToIterSeq2 reports whether t is assignable to +// iter.Seq[K, V] and returns K and V if so. +func assignableToIterSeq2(t types.Type) (k, v types.Type, ok bool) { + // The only named type assignable to iter.Seq2 is iter.Seq2. + if named, isNamed := t.(*types.Named); isNamed { + if !isPackageLevel(named.Obj(), "iter", "Seq2") { + return + } + t = t.Underlying() + } + + if t, ok := t.(*types.Signature); ok { + // func(yield func(K, V) bool)? + if t.Params().Len() == 1 && t.Results().Len() == 0 { + if yield, ok := t.Params().At(0).Type().(*types.Signature); ok { // sic, no Underlying/CoreType + if yield.Params().Len() == 2 && + yield.Results().Len() == 1 && + types.Identical(yield.Results().At(0).Type(), builtinBool.Type()) { + return yield.Params().At(0).Type(), yield.Params().At(1).Type(), true + } + } + } + } + return +} diff --git a/gopls/internal/analysis/modernize/minmax.go b/gopls/internal/analysis/modernize/minmax.go new file mode 100644 index 00000000000..06330657876 --- /dev/null +++ b/gopls/internal/analysis/modernize/minmax.go @@ -0,0 +1,199 @@ +// 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 modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/astutil/cursor" +) + +// The minmax pass replaces if/else statements with calls to min or max. +// +// Patterns: +// +// 1. if a < b { x = a } else { x = b } => x = min(a, b) +// 2. x = a; if a < b { x = b } => x = max(a, b) +// +// Variants: +// - all four ordered comparisons +// - "x := a" or "x = a" or "var x = a" in pattern 2 +// - "x < b" or "a < b" in pattern 2 +func minmax(pass *analysis.Pass) { + + // check is called for all statements of this form: + // if a < b { lhs = rhs } + check := func(curIfStmt cursor.Cursor, compare *ast.BinaryExpr) { + var ( + ifStmt = curIfStmt.Node().(*ast.IfStmt) + tassign = ifStmt.Body.List[0].(*ast.AssignStmt) + a = compare.X + b = compare.Y + lhs = tassign.Lhs[0] + rhs = tassign.Rhs[0] + scope = pass.TypesInfo.Scopes[ifStmt.Body] + sign = isInequality(compare.Op) + ) + + if fblock, ok := ifStmt.Else.(*ast.BlockStmt); ok && isAssignBlock(fblock) { + fassign := fblock.List[0].(*ast.AssignStmt) + + // Have: if a < b { lhs = rhs } else { lhs2 = rhs2 } + lhs2 := fassign.Lhs[0] + rhs2 := fassign.Rhs[0] + + // For pattern 1, check that: + // - lhs = lhs2 + // - {rhs,rhs2} = {a,b} + if equalSyntax(lhs, lhs2) { + if equalSyntax(rhs, a) && equalSyntax(rhs2, b) { + sign = +sign + } else if equalSyntax(rhs2, a) || equalSyntax(rhs, b) { + sign = -sign + } else { + return + } + + sym := cond(sign < 0, "min", "max") + + if _, obj := scope.LookupParent(sym, ifStmt.Pos()); !is[*types.Builtin](obj) { + return // min/max function is shadowed + } + + // pattern 1 + // + // TODO(adonovan): if lhs is declared "var lhs T" on preceding line, + // simplify the whole thing to "lhs := min(a, b)". + pass.Report(analysis.Diagnostic{ + // Highlight the condition a < b. + Pos: compare.Pos(), + End: compare.End(), + Category: "minmax", + Message: fmt.Sprintf("if/else statement can be modernized using %s", sym), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace if statement with %s", sym), + TextEdits: []analysis.TextEdit{{ + // Replace IfStmt with lhs = min(a, b). + Pos: ifStmt.Pos(), + End: ifStmt.End(), + NewText: fmt.Appendf(nil, "%s = %s(%s, %s)", + formatNode(pass.Fset, lhs), + sym, + formatNode(pass.Fset, a), + formatNode(pass.Fset, b)), + }}, + }}, + }) + } + + } else if prev, ok := curIfStmt.PrevSibling(); ok && is[*ast.AssignStmt](prev.Node()) { + fassign := prev.Node().(*ast.AssignStmt) + + // Have: lhs2 = rhs2; if a < b { lhs = rhs } + // For pattern 2, check that + // - lhs = lhs2 + // - {rhs,rhs2} = {a,b}, but allow lhs2 to + // stand for rhs2. + // TODO(adonovan): accept "var lhs2 = rhs2" form too. + lhs2 := fassign.Lhs[0] + rhs2 := fassign.Rhs[0] + + if equalSyntax(lhs, lhs2) { + if equalSyntax(rhs, a) && (equalSyntax(rhs2, b) || equalSyntax(lhs2, b)) { + sign = +sign + } else if (equalSyntax(rhs2, a) || equalSyntax(lhs2, a)) && equalSyntax(rhs, b) { + sign = -sign + } else { + return + } + sym := cond(sign < 0, "min", "max") + + if _, obj := scope.LookupParent(sym, ifStmt.Pos()); !is[*types.Builtin](obj) { + return // min/max function is shadowed + } + + // pattern 2 + pass.Report(analysis.Diagnostic{ + // Highlight the condition a < b. + Pos: compare.Pos(), + End: compare.End(), + Category: "minmax", + Message: fmt.Sprintf("if statement can be modernized using %s", sym), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace if/else with %s", sym), + TextEdits: []analysis.TextEdit{{ + // Replace rhs2 and IfStmt with min(a, b) + Pos: rhs2.Pos(), + End: ifStmt.End(), + NewText: fmt.Appendf(nil, "%s(%s, %s)", + sym, + formatNode(pass.Fset, a), + formatNode(pass.Fset, b)), + }}, + }}, + }) + } + } + } + + // Find all "if a < b { lhs = rhs }" statements. + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.21") { + for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) { + ifStmt := curIfStmt.Node().(*ast.IfStmt) + + if compare, ok := ifStmt.Cond.(*ast.BinaryExpr); ok && + ifStmt.Init == nil && + isInequality(compare.Op) != 0 && + isAssignBlock(ifStmt.Body) { + + // Have: if a < b { lhs = rhs } + check(curIfStmt, compare) + } + } + } +} + +// isInequality reports non-zero if tok is one of < <= => >: +// +1 for > and -1 for <. +func isInequality(tok token.Token) int { + switch tok { + case token.LEQ, token.LSS: + return -1 + case token.GEQ, token.GTR: + return +1 + } + return 0 +} + +// isAssignBlock reports whether b is a block of the form { lhs = rhs }. +func isAssignBlock(b *ast.BlockStmt) bool { + if len(b.List) != 1 { + return false + } + assign, ok := b.List[0].(*ast.AssignStmt) + return ok && assign.Tok == token.ASSIGN && len(assign.Lhs) == 1 && len(assign.Rhs) == 1 +} + +// -- utils -- + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +} diff --git a/gopls/internal/analysis/modernize/modernize.go b/gopls/internal/analysis/modernize/modernize.go new file mode 100644 index 00000000000..a117afa994c --- /dev/null +++ b/gopls/internal/analysis/modernize/modernize.go @@ -0,0 +1,152 @@ +// 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 modernize + +import ( + "bytes" + _ "embed" + "go/ast" + "go/format" + "go/token" + "go/types" + "iter" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/util/astutil" + "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/astutil/cursor" + "golang.org/x/tools/internal/versions" +) + +//go:embed doc.go +var doc string + +var Analyzer = &analysis.Analyzer{ + Name: "modernize", + Doc: analysisinternal.MustExtractDoc(doc, "modernize"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize", +} + +func run(pass *analysis.Pass) (any, error) { + // Decorate pass.Report to suppress diagnostics in generated files. + // + // TODO(adonovan): opt: do this more efficiently by interleaving + // the micro-passes (as described below) and preemptively skipping + // the entire subtree for each generated *ast.File. + { + // Gather information whether file is generated or not. + generated := make(map[*token.File]bool) + for _, file := range pass.Files { + if ast.IsGenerated(file) { + generated[pass.Fset.File(file.FileStart)] = true + } + } + report := pass.Report + pass.Report = func(diag analysis.Diagnostic) { + if _, ok := generated[pass.Fset.File(diag.Pos)]; ok { + return // skip checking if it's generated code + } + report(diag) + } + } + + appendclipped(pass) + bloop(pass) + efaceany(pass) + fmtappendf(pass) + mapsloop(pass) + minmax(pass) + sortslice(pass) + + // TODO(adonovan): + // - more modernizers here; see #70815. + // - opt: interleave these micro-passes within a single inspection. + // - solve the "duplicate import" problem (#68765) when a number of + // fixes in the same file are applied in parallel and all add + // the same import. The tests exhibit the problem. + // - should all diagnostics be of the form "x can be modernized by y" + // or is that a foolish consistency? + + return nil, nil +} + +// -- helpers -- + +// TODO(adonovan): factor with analysisutil.Imports. +func _imports(pkg *types.Package, path string) bool { + for _, imp := range pkg.Imports() { + if imp.Path() == path { + return true + } + } + return false +} + +// equalSyntax reports whether x and y are syntactically equal (ignoring comments). +func equalSyntax(x, y ast.Expr) bool { + sameName := func(x, y *ast.Ident) bool { return x.Name == y.Name } + return astutil.Equal(x, y, sameName) +} + +// formatNode formats n. +func formatNode(fset *token.FileSet, n ast.Node) []byte { + var buf bytes.Buffer + format.Node(&buf, fset, n) // ignore errors + return buf.Bytes() +} + +// formatExprs formats a comma-separated list of expressions. +func formatExprs(fset *token.FileSet, exprs []ast.Expr) string { + var buf strings.Builder + for i, e := range exprs { + if i > 0 { + buf.WriteString(", ") + } + format.Node(&buf, fset, e) // ignore errors + } + return buf.String() +} + +// isZeroLiteral reports whether e is the literal 0. +func isZeroLiteral(e ast.Expr) bool { + lit, ok := e.(*ast.BasicLit) + return ok && lit.Kind == token.INT && lit.Value == "0" +} + +// isPackageLevel reports whether obj is the package-level symbol pkg.Name. +func isPackageLevel(obj types.Object, pkgpath, name string) bool { + pkg := obj.Pkg() + return pkg != nil && + obj.Parent() == pkg.Scope() && + obj.Pkg().Path() == pkgpath && + obj.Name() == name +} + +// filesUsing returns a cursor for each *ast.File in the inspector +// that uses at least the specified version of Go (e.g. "go1.24"). +func filesUsing(inspect *inspector.Inspector, info *types.Info, version string) iter.Seq[cursor.Cursor] { + return func(yield func(cursor.Cursor) bool) { + for curFile := range cursor.Root(inspect).Children() { + file := curFile.Node().(*ast.File) + if !versions.Before(info.FileVersions[file], version) && !yield(curFile) { + break + } + } + } +} + +var ( + builtinAny = types.Universe.Lookup("any") + builtinAppend = types.Universe.Lookup("append") + builtinBool = types.Universe.Lookup("bool") + builtinMake = types.Universe.Lookup("make") + builtinNil = types.Universe.Lookup("nil") + byteSliceType = types.NewSlice(types.Typ[types.Byte]) +) diff --git a/gopls/internal/analysis/modernize/modernize_test.go b/gopls/internal/analysis/modernize/modernize_test.go new file mode 100644 index 00000000000..218c2238762 --- /dev/null +++ b/gopls/internal/analysis/modernize/modernize_test.go @@ -0,0 +1,24 @@ +// 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 modernize_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/gopls/internal/analysis/modernize" +) + +func Test(t *testing.T) { + analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), modernize.Analyzer, + "appendclipped", + "bloop", + "efaceany", + "fmtappendf", + "mapsloop", + "minmax", + "sortslice", + ) +} diff --git a/gopls/internal/analysis/modernize/slices.go b/gopls/internal/analysis/modernize/slices.go new file mode 100644 index 00000000000..695ade3f652 --- /dev/null +++ b/gopls/internal/analysis/modernize/slices.go @@ -0,0 +1,214 @@ +// 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 modernize + +// This file defines modernizers that use the "slices" package. + +import ( + "fmt" + "go/ast" + "go/types" + "slices" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysisinternal" +) + +// The appendclipped pass offers to simplify a tower of append calls: +// +// append(append(append(base, a...), b..., c...) +// +// with a call to go1.21's slices.Concat(base, a, b, c), or simpler +// replacements such as slices.Clone(a) in degenerate cases. +// +// The base expression must denote a clipped slice (see [isClipped] +// for definition), otherwise the replacement might eliminate intended +// side effects to the base slice's array. +// +// Examples: +// +// append(append(append(x[:0:0], a...), b...), c...) -> slices.Concat(a, b, c) +// append(append(slices.Clip(a), b...) -> slices.Concat(a, b) +// append([]T{}, a...) -> slices.Clone(a) +// append([]string(nil), os.Environ()...) -> os.Environ() +// +// The fix does not always preserve nilness the of base slice when the +// addends (a, b, c) are all empty. +func appendclipped(pass *analysis.Pass) { + if pass.Pkg.Path() == "slices" { + return + } + + info := pass.TypesInfo + + // sliceArgs is a non-empty (reversed) list of slices to be concatenated. + simplifyAppendEllipsis := func(file *ast.File, call *ast.CallExpr, base ast.Expr, sliceArgs []ast.Expr) { + // Only appends whose base is a clipped slice can be simplified: + // We must conservatively assume an append to an unclipped slice + // such as append(y[:0], x...) is intended to have effects on y. + clipped, empty := isClippedSlice(info, base) + if !clipped { + return + } + + // If the (clipped) base is empty, it may be safely ignored. + // Otherwise treat it as just another arg (the first) to Concat. + if !empty { + sliceArgs = append(sliceArgs, base) + } + slices.Reverse(sliceArgs) + + // Concat of a single (non-trivial) slice degenerates to Clone. + if len(sliceArgs) == 1 { + s := sliceArgs[0] + + // Special case for common but redundant clone of os.Environ(). + // append(zerocap, os.Environ()...) -> os.Environ() + if scall, ok := s.(*ast.CallExpr); ok { + if id := isQualifiedIdent(info, scall.Fun, "os", "Environ"); id != nil { + + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Category: "slicesclone", + Message: "Redundant clone of os.Environ()", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Eliminate redundant clone", + TextEdits: []analysis.TextEdit{{ + Pos: call.Pos(), + End: call.End(), + NewText: formatNode(pass.Fset, s), + }}, + }}, + }) + return + } + } + + // append(zerocap, s...) -> slices.Clone(s) + slicesName, importEdits := analysisinternal.AddImport(info, file, call.Pos(), "slices", "slices") + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Category: "slicesclone", + Message: "Replace append with slices.Clone", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace append with slices.Clone", + TextEdits: append(importEdits, []analysis.TextEdit{{ + Pos: call.Pos(), + End: call.End(), + NewText: []byte(fmt.Sprintf("%s.Clone(%s)", slicesName, formatNode(pass.Fset, s))), + }}...), + }}, + }) + return + } + + // append(append(append(base, a...), b..., c...) -> slices.Concat(base, a, b, c) + // + // TODO(adonovan): simplify sliceArgs[0] further: + // - slices.Clone(s) -> s + // - s[:len(s):len(s)] -> s + // - slices.Clip(s) -> s + slicesName, importEdits := analysisinternal.AddImport(info, file, call.Pos(), "slices", "slices") + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Category: "slicesclone", + Message: "Replace append with slices.Concat", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace append with slices.Concat", + TextEdits: append(importEdits, []analysis.TextEdit{{ + Pos: call.Pos(), + End: call.End(), + NewText: []byte(fmt.Sprintf("%s.Concat(%s)", slicesName, formatExprs(pass.Fset, sliceArgs))), + }}...), + }}, + }) + } + + // Mark nested calls to append so that we don't emit diagnostics for them. + skip := make(map[*ast.CallExpr]bool) + + // Visit calls of form append(x, y...). + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + for curFile := range filesUsing(inspect, info, "go1.21") { + file := curFile.Node().(*ast.File) + + for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) { + call := curCall.Node().(*ast.CallExpr) + if skip[call] { + continue + } + + // Recursively unwrap ellipsis calls to append, so + // append(append(append(base, a...), b..., c...) + // yields (base, [c b a]). + base, slices := ast.Expr(call), []ast.Expr(nil) // base case: (call, nil) + again: + if call, ok := base.(*ast.CallExpr); ok { + if id, ok := call.Fun.(*ast.Ident); ok && + call.Ellipsis.IsValid() && + len(call.Args) == 2 && + info.Uses[id] == builtinAppend { + + // Have: append(base, s...) + base, slices = call.Args[0], append(slices, call.Args[1]) + skip[call] = true + goto again + } + } + + if len(slices) > 0 { + simplifyAppendEllipsis(file, call, base, slices) + } + } + } +} + +// isClippedSlice reports whether e denotes a slice that is definitely +// clipped, that is, its len(s)==cap(s). +// +// In addition, it reports whether the slice is definitely empty. +// +// Examples of clipped slices: +// +// x[:0:0] (empty) +// []T(nil) (empty) +// Slice{} (empty) +// x[:len(x):len(x)] (nonempty) +// x[:k:k] (nonempty) +// slices.Clip(x) (nonempty) +func isClippedSlice(info *types.Info, e ast.Expr) (clipped, empty bool) { + switch e := e.(type) { + case *ast.SliceExpr: + // x[:0:0], x[:len(x):len(x)], x[:k:k], x[:0] + clipped = e.Slice3 && e.High != nil && e.Max != nil && equalSyntax(e.High, e.Max) // x[:k:k] + empty = e.High != nil && isZeroLiteral(e.High) // x[:0:*] + return + + case *ast.CallExpr: + // []T(nil)? + if info.Types[e.Fun].IsType() && + is[*ast.Ident](e.Args[0]) && + info.Uses[e.Args[0].(*ast.Ident)] == builtinNil { + return true, true + } + + // slices.Clip(x)? + if id := isQualifiedIdent(info, e.Fun, "slices", "Clip"); id != nil { + return true, false + } + + case *ast.CompositeLit: + // Slice{}? + if len(e.Elts) == 0 { + return true, true + } + } + return false, false +} diff --git a/gopls/internal/analysis/modernize/sortslice.go b/gopls/internal/analysis/modernize/sortslice.go new file mode 100644 index 00000000000..98e501875d2 --- /dev/null +++ b/gopls/internal/analysis/modernize/sortslice.go @@ -0,0 +1,131 @@ +// 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 modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysisinternal" +) + +// The sortslice pass replaces sort.Slice(slice, less) with +// slices.Sort(slice) when slice is a []T and less is a FuncLit +// equivalent to cmp.Ordered[T]. +// +// sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) +// => slices.Sort(s) +// +// It also supports the SliceStable variant. +// +// TODO(adonovan): support +// +// - sort.Slice(s, func(i, j int) bool { return s[i] ... s[j] }) +// -> slices.SortFunc(s, func(x, y int) bool { return x ... y }) +// iff all uses of i, j can be replaced by s[i], s[j]. +// +// - sort.Sort(x) where x has a named slice type whose Less method is the natural order. +// -> sort.Slice(x) +func sortslice(pass *analysis.Pass) { + if !_imports(pass.Pkg, "sort") { + return + } + + info := pass.TypesInfo + + check := func(file *ast.File, call *ast.CallExpr) { + // call to sort.Slice{,Stable}? + var stable string + if isQualifiedIdent(info, call.Fun, "sort", "Slice") != nil { + } else if isQualifiedIdent(info, call.Fun, "sort", "SliceStable") != nil { + stable = "Stable" + } else { + return + } + + if lit, ok := call.Args[1].(*ast.FuncLit); ok && len(lit.Body.List) == 1 { + sig := info.Types[lit.Type].Type.(*types.Signature) + + // Have: sort.Slice(s, func(i, j int) bool { return ... }) + s := call.Args[0] + i := sig.Params().At(0) + j := sig.Params().At(1) + + ret := lit.Body.List[0].(*ast.ReturnStmt) + if compare, ok := ret.Results[0].(*ast.BinaryExpr); ok && compare.Op == token.LSS { + // isIndex reports whether e is s[v]. + isIndex := func(e ast.Expr, v *types.Var) bool { + index, ok := e.(*ast.IndexExpr) + return ok && + equalSyntax(index.X, s) && + is[*ast.Ident](index.Index) && + info.Uses[index.Index.(*ast.Ident)] == v + } + if isIndex(compare.X, i) && isIndex(compare.Y, j) { + // Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) + + slicesName, importEdits := analysisinternal.AddImport(info, file, call.Pos(), "slices", "slices") + + pass.Report(analysis.Diagnostic{ + // Highlight "sort.Slice". + Pos: call.Fun.Pos(), + End: call.Fun.End(), + Category: "sortslice", + Message: fmt.Sprintf("sort.Slice%[1]s can be modernized using slices.Sort%[1]s", stable), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace sort.Slice%[1]s call by slices.Sort%[1]s", stable), + TextEdits: append(importEdits, []analysis.TextEdit{ + { + // Replace sort.Slice with slices.Sort. + Pos: call.Fun.Pos(), + End: call.Fun.End(), + NewText: []byte(slicesName + ".Sort" + stable), + }, + { + // Eliminate FuncLit. + Pos: call.Args[0].End(), + End: call.Rparen, + }, + }...), + }}, + }) + } + } + } + } + + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + for curFile := range filesUsing(inspect, info, "go1.21") { + file := curFile.Node().(*ast.File) + + for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) { + call := curCall.Node().(*ast.CallExpr) + check(file, call) + } + } +} + +// isQualifiedIdent reports whether e is a reference to pkg.Name. If so, it returns the identifier. +func isQualifiedIdent(info *types.Info, e ast.Expr, pkgpath, name string) *ast.Ident { + var id *ast.Ident + switch e := e.(type) { + case *ast.Ident: + id = e // e.g. dot import + case *ast.SelectorExpr: + id = e.Sel + default: + return nil + } + obj, ok := info.Uses[id] + if ok && isPackageLevel(obj, pkgpath, name) { + return id + } + return nil +} diff --git a/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go b/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go new file mode 100644 index 00000000000..c4e98535a37 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go @@ -0,0 +1,26 @@ +package appendclipped + +import ( + "os" + "slices" +) + +type Bytes []byte + +func _(s, other []string) { + print(append([]string{}, s...)) // want "Replace append with slices.Clone" + print(append([]string(nil), s...)) // want "Replace append with slices.Clone" + print(append(Bytes(nil), Bytes{1, 2, 3}...)) // want "Replace append with slices.Clone" + print(append(other[:0:0], s...)) // want "Replace append with slices.Clone" + print(append(other[:0:0], os.Environ()...)) // want "Redundant clone of os.Environ()" + print(append(other[:0], s...)) // nope: intent may be to mutate other + + print(append(append(append([]string{}, s...), other...), other...)) // want "Replace append with slices.Concat" + print(append(append(append([]string(nil), s...), other...), other...)) // want "Replace append with slices.Concat" + print(append(append(Bytes(nil), Bytes{1, 2, 3}...), Bytes{4, 5, 6}...)) // want "Replace append with slices.Concat" + print(append(append(append(other[:0:0], s...), other...), other...)) // want "Replace append with slices.Concat" + print(append(append(append(other[:0:0], os.Environ()...), other...), other...)) // want "Replace append with slices.Concat" + print(append(append(other[:len(other):len(other)], s...), other...)) // want "Replace append with slices.Concat" + print(append(append(slices.Clip(other), s...), other...)) // want "Replace append with slices.Concat" + print(append(append(append(other[:0], s...), other...), other...)) // nope: intent may be to mutate other +} diff --git a/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go.golden b/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go.golden new file mode 100644 index 00000000000..5d6761b5371 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/appendclipped/appendclipped.go.golden @@ -0,0 +1,26 @@ +package appendclipped + +import ( + "os" + "slices" +) + +type Bytes []byte + +func _(s, other []string) { + print(slices.Clone(s)) // want "Replace append with slices.Clone" + print(slices.Clone(s)) // want "Replace append with slices.Clone" + print(slices.Clone(Bytes{1, 2, 3})) // want "Replace append with slices.Clone" + print(slices.Clone(s)) // want "Replace append with slices.Clone" + print(os.Environ()) // want "Redundant clone of os.Environ()" + print(append(other[:0], s...)) // nope: intent may be to mutate other + + print(slices.Concat(s, other, other)) // want "Replace append with slices.Concat" + print(slices.Concat(s, other, other)) // want "Replace append with slices.Concat" + print(slices.Concat(Bytes{1, 2, 3}, Bytes{4, 5, 6})) // want "Replace append with slices.Concat" + print(slices.Concat(s, other, other)) // want "Replace append with slices.Concat" + print(slices.Concat(os.Environ(), other, other)) // want "Replace append with slices.Concat" + print(slices.Concat(other[:len(other):len(other)], s, other)) // want "Replace append with slices.Concat" + print(slices.Concat(slices.Clip(other), s, other)) // want "Replace append with slices.Concat" + print(append(append(append(other[:0], s...), other...), other...)) // nope: intent may be to mutate other +} diff --git a/gopls/internal/analysis/modernize/testdata/src/bloop/bloop.go b/gopls/internal/analysis/modernize/testdata/src/bloop/bloop.go new file mode 100644 index 00000000000..f474dcebf69 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/bloop/bloop.go @@ -0,0 +1 @@ +package bloop diff --git a/gopls/internal/analysis/modernize/testdata/src/bloop/bloop_test.go b/gopls/internal/analysis/modernize/testdata/src/bloop/bloop_test.go new file mode 100644 index 00000000000..c7552f4223f --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/bloop/bloop_test.go @@ -0,0 +1,69 @@ +//go:build go1.24 + +package bloop + +import "testing" + +func BenchmarkA(b *testing.B) { + println("slow") + b.ResetTimer() + + for range b.N { // want "b.N can be modernized using b.Loop.." + } +} + +func BenchmarkB(b *testing.B) { + // setup + { + b.StopTimer() + println("slow") + b.StartTimer() + } + + for i := range b.N { // Nope. Should we change this to "for i := 0; b.Loop(); i++"? + print(i) + } + + b.StopTimer() + println("slow") +} + +func BenchmarkC(b *testing.B) { + // setup + { + b.StopTimer() + println("slow") + b.StartTimer() + } + + for i := 0; i < b.N; i++ { // want "b.N can be modernized using b.Loop.." + println("no uses of i") + } + + b.StopTimer() + println("slow") +} + +func BenchmarkD(b *testing.B) { + for i := 0; i < b.N; i++ { // want "b.N can be modernized using b.Loop.." + println(i) + } +} + +func BenchmarkE(b *testing.B) { + b.Run("sub", func(b *testing.B) { + b.StopTimer() // not deleted + println("slow") + b.StartTimer() // not deleted + + // ... + }) + b.ResetTimer() + + for i := 0; i < b.N; i++ { // want "b.N can be modernized using b.Loop.." + println("no uses of i") + } + + b.StopTimer() + println("slow") +} diff --git a/gopls/internal/analysis/modernize/testdata/src/bloop/bloop_test.go.golden b/gopls/internal/analysis/modernize/testdata/src/bloop/bloop_test.go.golden new file mode 100644 index 00000000000..4c0353c8687 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/bloop/bloop_test.go.golden @@ -0,0 +1,67 @@ +//go:build go1.24 + +package bloop + +import "testing" + +func BenchmarkA(b *testing.B) { + println("slow") + + for b.Loop() { // want "b.N can be modernized using b.Loop.." + } +} + +func BenchmarkB(b *testing.B) { + // setup + { + b.StopTimer() + println("slow") + b.StartTimer() + } + + for i := range b.N { // Nope. Should we change this to "for i := 0; b.Loop(); i++"? + print(i) + } + + b.StopTimer() + println("slow") +} + +func BenchmarkC(b *testing.B) { + // setup + { + + println("slow") + + } + + for b.Loop() { // want "b.N can be modernized using b.Loop.." + println("no uses of i") + } + + b.StopTimer() + println("slow") +} + +func BenchmarkD(b *testing.B) { + for i := 0; b.Loop(); i++ { // want "b.N can be modernized using b.Loop.." + println(i) + } +} + +func BenchmarkE(b *testing.B) { + b.Run("sub", func(b *testing.B) { + b.StopTimer() // not deleted + println("slow") + b.StartTimer() // not deleted + + // ... + }) + + for b.Loop() { // want "b.N can be modernized using b.Loop.." + println("no uses of i") + } + + b.StopTimer() + println("slow") +} diff --git a/gopls/internal/analysis/modernize/testdata/src/efaceany/efaceany.go b/gopls/internal/analysis/modernize/testdata/src/efaceany/efaceany.go new file mode 100644 index 00000000000..b3c8fd58603 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/efaceany/efaceany.go @@ -0,0 +1,10 @@ +package efaceany + +func _(x interface{}) {} // want "interface{} can be replaced by any" + +func _() { + var x interface{} // want "interface{} can be replaced by any" + const any = 1 + var y interface{} // nope: any is shadowed here + _, _ = x, y +} diff --git a/gopls/internal/analysis/modernize/testdata/src/efaceany/efaceany.go.golden b/gopls/internal/analysis/modernize/testdata/src/efaceany/efaceany.go.golden new file mode 100644 index 00000000000..4c2e37fd769 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/efaceany/efaceany.go.golden @@ -0,0 +1,10 @@ +package efaceany + +func _(x any) {} // want "interface{} can be replaced by any" + +func _() { + var x any // want "interface{} can be replaced by any" + const any = 1 + var y interface{} // nope: any is shadowed here + _, _ = x, y +} diff --git a/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go b/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go new file mode 100644 index 00000000000..a39a03ee786 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go @@ -0,0 +1,36 @@ +package fmtappendf + +import ( + "fmt" +) + +func two() string { + return "two" +} + +func bye() { + bye := []byte(fmt.Sprintf("bye %d", 1)) // want "Replace .*Sprintf.* with fmt.Appendf" + print(bye) +} + +func funcsandvars() { + one := "one" + bye := []byte(fmt.Sprintf("bye %d %s %s", 1, two(), one)) // want "Replace .*Sprintf.* with fmt.Appendf" + print(bye) +} + +func typealias() { + type b = byte + type bt = []byte + bye := []b(fmt.Sprintf("bye %d", 1)) // want "Replace .*Sprintf.* with fmt.Appendf" + print(bye) + bye = bt(fmt.Sprintf("bye %d", 1)) // want "Replace .*Sprintf.* with fmt.Appendf" + print(bye) +} + +func otherprints() { + sprint := []byte(fmt.Sprint("bye %d", 1)) // want "Replace .*Sprintf.* with fmt.Appendf" + print(sprint) + sprintln := []byte(fmt.Sprintln("bye %d", 1)) // want "Replace .*Sprintf.* with fmt.Appendf" + print(sprintln) +} diff --git a/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go.golden b/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go.golden new file mode 100644 index 00000000000..7c8aa7b9a5e --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/fmtappendf/fmtappendf.go.golden @@ -0,0 +1,36 @@ +package fmtappendf + +import ( + "fmt" +) + +func two() string { + return "two" +} + +func bye() { + bye := fmt.Appendf(nil, "bye %d", 1) // want "Replace .*Sprintf.* with fmt.Appendf" + print(bye) +} + +func funcsandvars() { + one := "one" + bye := fmt.Appendf(nil, "bye %d %s %s", 1, two(), one) // want "Replace .*Sprintf.* with fmt.Appendf" + print(bye) +} + +func typealias() { + type b = byte + type bt = []byte + bye := fmt.Appendf(nil, "bye %d", 1) // want "Replace .*Sprintf.* with fmt.Appendf" + print(bye) + bye = fmt.Appendf(nil, "bye %d", 1) // want "Replace .*Sprintf.* with fmt.Appendf" + print(bye) +} + +func otherprints() { + sprint := fmt.Append(nil, "bye %d", 1) // want "Replace .*Sprintf.* with fmt.Appendf" + print(sprint) + sprintln := fmt.Appendln(nil, "bye %d", 1) // want "Replace .*Sprintf.* with fmt.Appendf" + print(sprintln) +} \ No newline at end of file diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go new file mode 100644 index 00000000000..ab1305d3b81 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go @@ -0,0 +1,140 @@ +//go:build go1.23 + +package mapsloop + +import ( + "iter" + "maps" +) + +var _ = maps.Clone[M] // force "maps" import so that each diagnostic doesn't add one + +type M map[int]string + +// -- src is map -- + +func useCopy(dst, src map[int]string) { + // Replace loop by maps.Copy. + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" + } +} + +func useClone(src map[int]string) { + // Replace make(...) by maps.Clone. + dst := make(map[int]string, len(src)) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + } + println(dst) +} + +func useCopy_typesDiffer(src M) { + // Replace loop but not make(...) as maps.Copy(src) would return wrong type M. + dst := make(map[int]string, len(src)) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" + } + println(dst) +} + +func useCopy_typesDiffer2(src map[int]string) { + // Replace loop but not make(...) as maps.Copy(src) would return wrong type map[int]string. + dst := make(M, len(src)) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" + } + println(dst) +} + +func useClone_typesDiffer3(src map[int]string) { + // Replace loop and make(...) as maps.Clone(src) returns map[int]string + // which is assignable to M. + var dst M + dst = make(M, len(src)) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + } + println(dst) +} + +func useClone_typesDiffer4(src map[int]string) { + // Replace loop and make(...) as maps.Clone(src) returns map[int]string + // which is assignable to M. + var dst M + dst = make(M, len(src)) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + } + println(dst) +} + +func useClone_generic[Map ~map[K]V, K comparable, V any](src Map) { + // Replace loop and make(...) by maps.Clone + dst := make(Map, len(src)) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + } + println(dst) +} + +// -- src is iter.Seq2 -- + +func useInsert_assignableToSeq2(dst map[int]string, src func(yield func(int, string) bool)) { + // Replace loop by maps.Insert because src is assignable to iter.Seq2. + for k, v := range src { + dst[k] = v // want "Replace m\\[k\\]=v loop with maps.Insert" + } +} + +func useCollect(src iter.Seq2[int, string]) { + // Replace loop and make(...) by maps.Collect. + var dst map[int]string + dst = make(map[int]string) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Collect" + } +} + +func useInsert_typesDifferAssign(src iter.Seq2[int, string]) { + // Replace loop and make(...): maps.Collect returns an unnamed map type + // that is assignable to M. + var dst M + dst = make(M) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Collect" + } +} + +func useInsert_typesDifferDeclare(src iter.Seq2[int, string]) { + // Replace loop but not make(...) as maps.Collect would return an + // unnamed map type that would change the type of dst. + dst := make(M) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Insert" + } +} + +// -- non-matches -- + +type isomerOfSeq2 func(yield func(int, string) bool) + +func nopeInsertRequiresAssignableToSeq2(dst map[int]string, src isomerOfSeq2) { + for k, v := range src { // nope: src is not assignable to maps.Insert's iter.Seq2 parameter + dst[k] = v + } +} + +func nopeSingleVarRange(dst map[int]bool, src map[int]string) { + for key := range src { // nope: must be "for k, v" + dst[key] = true + } +} + +func nopeBodyNotASingleton(src map[int]string) { + var dst map[int]string + for key, value := range src { + dst[key] = value + println() // nope: other things in the loop body + } +} diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden new file mode 100644 index 00000000000..6d95cc023ee --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden @@ -0,0 +1,112 @@ +//go:build go1.23 + +package mapsloop + +import ( + "iter" + "maps" +) + +var _ = maps.Clone[M] // force "maps" import so that each diagnostic doesn't add one + +type M map[int]string + +// -- src is map -- + +func useCopy(dst, src map[int]string) { + // Replace loop by maps.Copy. + maps.Copy(dst, src) +} + +func useClone(src map[int]string) { + // Replace make(...) by maps.Clone. + dst := maps.Clone(src) + println(dst) +} + +func useCopy_typesDiffer(src M) { + // Replace loop but not make(...) as maps.Copy(src) would return wrong type M. + dst := make(map[int]string, len(src)) + maps.Copy(dst, src) + println(dst) +} + +func useCopy_typesDiffer2(src map[int]string) { + // Replace loop but not make(...) as maps.Copy(src) would return wrong type map[int]string. + dst := make(M, len(src)) + maps.Copy(dst, src) + println(dst) +} + +func useClone_typesDiffer3(src map[int]string) { + // Replace loop and make(...) as maps.Clone(src) returns map[int]string + // which is assignable to M. + var dst M + dst = maps.Clone(src) + println(dst) +} + +func useClone_typesDiffer4(src map[int]string) { + // Replace loop and make(...) as maps.Clone(src) returns map[int]string + // which is assignable to M. + var dst M + dst = maps.Clone(src) + println(dst) +} + +func useClone_generic[Map ~map[K]V, K comparable, V any](src Map) { + // Replace loop and make(...) by maps.Clone + dst := maps.Clone(src) + println(dst) +} + +// -- src is iter.Seq2 -- + +func useInsert_assignableToSeq2(dst map[int]string, src func(yield func(int, string) bool)) { + // Replace loop by maps.Insert because src is assignable to iter.Seq2. + maps.Insert(dst, src) +} + +func useCollect(src iter.Seq2[int, string]) { + // Replace loop and make(...) by maps.Collect. + var dst map[int]string + dst = maps.Collect(src) +} + +func useInsert_typesDifferAssign(src iter.Seq2[int, string]) { + // Replace loop and make(...): maps.Collect returns an unnamed map type + // that is assignable to M. + var dst M + dst = maps.Collect(src) +} + +func useInsert_typesDifferDeclare(src iter.Seq2[int, string]) { + // Replace loop but not make(...) as maps.Collect would return an + // unnamed map type that would change the type of dst. + dst := make(M) + maps.Insert(dst, src) +} + +// -- non-matches -- + +type isomerOfSeq2 func(yield func(int, string) bool) + +func nopeInsertRequiresAssignableToSeq2(dst map[int]string, src isomerOfSeq2) { + for k, v := range src { // nope: src is not assignable to maps.Insert's iter.Seq2 parameter + dst[k] = v + } +} + +func nopeSingleVarRange(dst map[int]bool, src map[int]string) { + for key := range src { // nope: must be "for k, v" + dst[key] = true + } +} + +func nopeBodyNotASingleton(src map[int]string) { + var dst map[int]string + for key, value := range src { + dst[key] = value + println() // nope: other things in the loop body + } +} diff --git a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go new file mode 100644 index 00000000000..393b3729e07 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go @@ -0,0 +1,73 @@ +package minmax + +func ifmin(a, b int) { + x := a + if a < b { // want "if statement can be modernized using max" + x = b + } + print(x) +} + +func ifmax(a, b int) { + x := a + if a > b { // want "if statement can be modernized using min" + x = b + } + print(x) +} + +func ifminvariant(a, b int) { + x := a + if x > b { // want "if statement can be modernized using min" + x = b + } + print(x) +} + +func ifmaxvariant(a, b int) { + x := b + if a < x { // want "if statement can be modernized using min" + x = a + } + print(x) +} + +func ifelsemin(a, b int) { + var x int + if a <= b { // want "if/else statement can be modernized using min" + x = a + } else { + x = b + } + print(x) +} + +func ifelsemax(a, b int) { + var x int + if a >= b { // want "if/else statement can be modernized using max" + x = a + } else { + x = b + } + print(x) +} + +func shadowed() int { + hour, min := 3600, 60 + + var time int + if hour < min { // silent: the built-in min function is shadowed here + time = hour + } else { + time = min + } + return time +} + +func nopeIfStmtHasInitStmt() { + x := 1 + if y := 2; y < x { // silent: IfStmt has an Init stmt + x = y + } + print(x) +} diff --git a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden new file mode 100644 index 00000000000..aacf84dd1c4 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden @@ -0,0 +1,53 @@ +package minmax + +func ifmin(a, b int) { + x := max(a, b) + print(x) +} + +func ifmax(a, b int) { + x := min(a, b) + print(x) +} + +func ifminvariant(a, b int) { + x := min(x, b) + print(x) +} + +func ifmaxvariant(a, b int) { + x := min(a, x) + print(x) +} + +func ifelsemin(a, b int) { + var x int + x = min(a, b) + print(x) +} + +func ifelsemax(a, b int) { + var x int + x = max(a, b) + print(x) +} + +func shadowed() int { + hour, min := 3600, 60 + + var time int + if hour < min { // silent: the built-in min function is shadowed here + time = hour + } else { + time = min + } + return time +} + +func nopeIfStmtHasInitStmt() { + x := 1 + if y := 2; y < x { // silent: IfStmt has an Init stmt + x = y + } + print(x) +} diff --git a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go new file mode 100644 index 00000000000..fce3e006328 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go @@ -0,0 +1,27 @@ +package sortslice + +import "sort" + +type myint int + +func _(s []myint) { + sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) // want "sort.Slice can be modernized using slices.Sort" + + sort.SliceStable(s, func(i, j int) bool { return s[i] < s[j] }) // want "sort.SliceStable can be modernized using slices.SortStable" +} + +func _(x *struct{ s []int }) { + sort.Slice(x.s, func(first, second int) bool { return x.s[first] < x.s[second] }) // want "sort.Slice can be modernized using slices.Sort" +} + +func _(s []int) { + sort.Slice(s, func(i, j int) bool { return s[i] > s[j] }) // nope: wrong comparison operator +} + +func _(s []int) { + sort.Slice(s, func(i, j int) bool { return s[j] < s[i] }) // nope: wrong index var +} + +func _(s2 []struct{ x int }) { + sort.Slice(s2, func(i, j int) bool { return s2[i].x < s2[j].x }) // nope: not a simple index operation +} diff --git a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden new file mode 100644 index 00000000000..176ae66d204 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden @@ -0,0 +1,33 @@ +package sortslice + +import "slices" + +import "slices" + +import "slices" + +import "sort" + +type myint int + +func _(s []myint) { + slices.Sort(s) // want "sort.Slice can be modernized using slices.Sort" + + slices.SortStable(s) // want "sort.SliceStable can be modernized using slices.SortStable" +} + +func _(x *struct{ s []int }) { + slices.Sort(x.s) // want "sort.Slice can be modernized using slices.Sort" +} + +func _(s []int) { + sort.Slice(s, func(i, j int) bool { return s[i] > s[j] }) // nope: wrong comparison operator +} + +func _(s []int) { + sort.Slice(s, func(i, j int) bool { return s[j] < s[i] }) // nope: wrong index var +} + +func _(s2 []struct{ x int }) { + sort.Slice(s2, func(i, j int) bool { return s2[i].x < s2[j].x }) // nope: not a simple index operation +} diff --git a/gopls/internal/analysis/unusedfunc/doc.go b/gopls/internal/analysis/unusedfunc/doc.go new file mode 100644 index 00000000000..5946ed897bb --- /dev/null +++ b/gopls/internal/analysis/unusedfunc/doc.go @@ -0,0 +1,34 @@ +// 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 unusedfunc defines an analyzer that checks for unused +// functions and methods +// +// # Analyzer unusedfunc +// +// unusedfunc: check for unused functions and methods +// +// The unusedfunc analyzer reports functions and methods that are +// never referenced outside of their own declaration. +// +// A function is considered unused if it is unexported and not +// referenced (except within its own declaration). +// +// A method is considered unused if it is unexported, not referenced +// (except within its own declaration), and its name does not match +// that of any method of an interface type declared within the same +// package. +// +// The tool may report a false positive for a declaration of an +// unexported function that is referenced from another package using +// the go:linkname mechanism, if the declaration's doc comment does +// not also have a go:linkname comment. (Such code is in any case +// strongly discouraged: linkname annotations, if they must be used at +// all, should be used on both the declaration and the alias.) +// +// The unusedfunc algorithm is not as precise as the +// golang.org/x/tools/cmd/deadcode tool, but it has the advantage that +// it runs within the modular analysis framework, enabling near +// real-time feedback within gopls. +package unusedfunc diff --git a/gopls/internal/analysis/unusedfunc/main.go b/gopls/internal/analysis/unusedfunc/main.go new file mode 100644 index 00000000000..0f42023b642 --- /dev/null +++ b/gopls/internal/analysis/unusedfunc/main.go @@ -0,0 +1,15 @@ +// 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 ignore + +// The unusedfunc command runs the unusedfunc analyzer. +package main + +import ( + "golang.org/x/tools/go/analysis/singlechecker" + "golang.org/x/tools/gopls/internal/analysis/unusedfunc" +) + +func main() { singlechecker.Main(unusedfunc.Analyzer) } diff --git a/gopls/internal/analysis/unusedfunc/testdata/src/a/a.go b/gopls/internal/analysis/unusedfunc/testdata/src/a/a.go new file mode 100644 index 00000000000..46ccde17d1d --- /dev/null +++ b/gopls/internal/analysis/unusedfunc/testdata/src/a/a.go @@ -0,0 +1,41 @@ +package a + +func main() { + _ = live +} + +// -- functions -- + +func Exported() {} + +func dead() { // want `function "dead" is unused` +} + +func deadRecursive() int { // want `function "deadRecursive" is unused` + return deadRecursive() +} + +func live() {} + +//go:linkname foo +func apparentlyDeadButHasPrecedingLinknameComment() {} + +// -- methods -- + +type ExportedType int +type unexportedType int + +func (ExportedType) Exported() {} +func (unexportedType) Exported() {} + +func (x ExportedType) dead() { // want `method "dead" is unused` + x.dead() +} + +func (u unexportedType) dead() { // want `method "dead" is unused` + u.dead() +} + +func (x ExportedType) dynamic() {} // matches name of interface method => live + +type _ interface{ dynamic() } diff --git a/gopls/internal/analysis/unusedfunc/testdata/src/a/a.go.golden b/gopls/internal/analysis/unusedfunc/testdata/src/a/a.go.golden new file mode 100644 index 00000000000..86da439bf3f --- /dev/null +++ b/gopls/internal/analysis/unusedfunc/testdata/src/a/a.go.golden @@ -0,0 +1,26 @@ +package a + +func main() { + _ = live +} + +// -- functions -- + +func Exported() {} + +func live() {} + +//go:linkname foo +func apparentlyDeadButHasPrecedingLinknameComment() {} + +// -- methods -- + +type ExportedType int +type unexportedType int + +func (ExportedType) Exported() {} +func (unexportedType) Exported() {} + +func (x ExportedType) dynamic() {} // matches name of interface method => live + +type _ interface{ dynamic() } diff --git a/gopls/internal/analysis/unusedfunc/unusedfunc.go b/gopls/internal/analysis/unusedfunc/unusedfunc.go new file mode 100644 index 00000000000..f13da635890 --- /dev/null +++ b/gopls/internal/analysis/unusedfunc/unusedfunc.go @@ -0,0 +1,183 @@ +// 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 unusedfunc + +import ( + _ "embed" + "fmt" + "go/ast" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/util/astutil" + "golang.org/x/tools/internal/analysisinternal" +) + +// Assumptions +// +// Like unusedparams, this analyzer depends on the invariant of the +// gopls analysis driver that only the "widest" package (the one with +// the most files) for a given file is analyzed. This invariant allows +// the algorithm to make "closed world" assumptions about the target +// package. (In general, analysis of Go test packages cannot make that +// assumption because in-package tests add new files to existing +// packages, potentially invalidating results.) Consequently, running +// this analyzer in, say, unitchecker or multichecker may produce +// incorrect results. +// +// A function is unreferenced if it is never referenced except within +// its own declaration, and it is unexported. (Exported functions must +// be assumed to be referenced from other packages.) +// +// For methods, we assume that the receiver type is "live" (variables +// of that type are created) and "address taken" (its rtype ends up in +// an at least one interface value). This means exported methods may +// be called via reflection or by interfaces defined in other +// packages, so again we are concerned only with unexported methods. +// +// To discount the possibility of a method being called via an +// interface, we must additionally ensure that no literal interface +// type within the package has a method of the same name. +// (Unexported methods cannot be called through interfaces declared +// in other packages because each package has a private namespace +// for unexported identifiers.) + +//go:embed doc.go +var doc string + +var Analyzer = &analysis.Analyzer{ + Name: "unusedfunc", + Doc: analysisinternal.MustExtractDoc(doc, "unusedfunc"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedfunc", +} + +func run(pass *analysis.Pass) (any, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + // Gather names of unexported interface methods declared in this package. + localIfaceMethods := make(map[string]bool) + nodeFilter := []ast.Node{(*ast.InterfaceType)(nil)} + inspect.Preorder(nodeFilter, func(n ast.Node) { + iface := n.(*ast.InterfaceType) + for _, field := range iface.Methods.List { + if len(field.Names) > 0 { + id := field.Names[0] + if !id.IsExported() { + // TODO(adonovan): check not just name but signature too. + localIfaceMethods[id.Name] = true + } + } + } + }) + + // Map each function/method symbol to its declaration. + decls := make(map[*types.Func]*ast.FuncDecl) + for _, file := range pass.Files { + if ast.IsGenerated(file) { + continue // skip generated files + } + + for _, decl := range file.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok { + id := decl.Name + // Exported functions may be called from other packages. + if id.IsExported() { + continue + } + + // Blank functions are exempt from diagnostics. + if id.Name == "_" { + continue + } + + // An (unexported) method whose name matches an + // interface method declared in the same package + // may be dynamically called via that interface. + if decl.Recv != nil && localIfaceMethods[id.Name] { + continue + } + + // main and init functions are implicitly always used + if decl.Recv == nil && (id.Name == "init" || id.Name == "main") { + continue + } + + fn := pass.TypesInfo.Defs[id].(*types.Func) + decls[fn] = decl + } + } + } + + // Scan for uses of each function symbol. + // (Ignore uses within the function's body.) + use := func(ref ast.Node, obj types.Object) { + if fn, ok := obj.(*types.Func); ok { + if fn := fn.Origin(); fn.Pkg() == pass.Pkg { + if decl, ok := decls[fn]; ok { + // Ignore uses within the function's body. + if decl.Body != nil && astutil.NodeContains(decl.Body, ref.Pos()) { + return + } + delete(decls, fn) // symbol is referenced + } + } + } + } + for id, obj := range pass.TypesInfo.Uses { + use(id, obj) + } + for sel, seln := range pass.TypesInfo.Selections { + use(sel, seln.Obj()) + } + + // Report the remaining unreferenced symbols. +nextDecl: + for fn, decl := range decls { + noun := "function" + if decl.Recv != nil { + noun = "method" + } + + pos := decl.Pos() // start of func decl or associated comment + if decl.Doc != nil { + pos = decl.Doc.Pos() + + // Skip if there's a preceding //go:linkname directive. + // + // (A program can link fine without such a directive, + // but it is bad style; and the directive may + // appear anywhere, not just on the preceding line, + // but again that is poor form.) + // + // TODO(adonovan): use ast.ParseDirective when #68021 lands. + for _, comment := range decl.Doc.List { + if strings.HasPrefix(comment.Text, "//go:linkname ") { + continue nextDecl + } + } + } + + pass.Report(analysis.Diagnostic{ + Pos: decl.Name.Pos(), + End: decl.Name.End(), + Message: fmt.Sprintf("%s %q is unused", noun, fn.Name()), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Delete %s %q", noun, fn.Name()), + TextEdits: []analysis.TextEdit{{ + // delete declaration + Pos: pos, + End: decl.End(), + }}, + }}, + }) + } + + return nil, nil +} diff --git a/gopls/internal/analysis/useany/useany_test.go b/gopls/internal/analysis/unusedfunc/unusedfunc_test.go similarity index 53% rename from gopls/internal/analysis/useany/useany_test.go rename to gopls/internal/analysis/unusedfunc/unusedfunc_test.go index a8cb692f359..1bf73da3653 100644 --- a/gopls/internal/analysis/useany/useany_test.go +++ b/gopls/internal/analysis/unusedfunc/unusedfunc_test.go @@ -1,17 +1,17 @@ -// Copyright 2021 The Go Authors. All rights reserved. +// 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 useany_test +package unusedfunc_test import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/gopls/internal/analysis/useany" + "golang.org/x/tools/gopls/internal/analysis/unusedfunc" ) func Test(t *testing.T) { testdata := analysistest.TestData() - analysistest.RunWithSuggestedFixes(t, testdata, useany.Analyzer, "a") + analysistest.RunWithSuggestedFixes(t, testdata, unusedfunc.Analyzer, "a") } diff --git a/gopls/internal/analysis/unusedparams/unusedparams.go b/gopls/internal/analysis/unusedparams/unusedparams.go index ca808a740d3..2b74328021d 100644 --- a/gopls/internal/analysis/unusedparams/unusedparams.go +++ b/gopls/internal/analysis/unusedparams/unusedparams.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/util/moreslices" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/astutil/cursor" ) //go:embed doc.go @@ -141,7 +142,7 @@ func run(pass *analysis.Pass) (any, error) { (*ast.FuncDecl)(nil), (*ast.FuncLit)(nil), } - inspect.WithStack(filter, func(n ast.Node, push bool, stack []ast.Node) bool { + cursor.Root(inspect).Inspect(filter, func(c cursor.Cursor, push bool) bool { // (We always return true so that we visit nested FuncLits.) if !push { @@ -153,7 +154,7 @@ func run(pass *analysis.Pass) (any, error) { ftype *ast.FuncType body *ast.BlockStmt ) - switch n := n.(type) { + switch n := c.Node().(type) { case *ast.FuncDecl: // We can't analyze non-Go functions. if n.Body == nil { @@ -182,7 +183,7 @@ func run(pass *analysis.Pass) (any, error) { // Find the symbol for the variable (if any) // to which the FuncLit is bound. // (We don't bother to allow ParenExprs.) - switch parent := stack[len(stack)-2].(type) { + switch parent := c.Parent().Node().(type) { case *ast.AssignStmt: // f = func() {...} // f := func() {...} diff --git a/gopls/internal/analysis/useany/testdata/src/a/a.go b/gopls/internal/analysis/useany/testdata/src/a/a.go deleted file mode 100644 index 22d69315070..00000000000 --- a/gopls/internal/analysis/useany/testdata/src/a/a.go +++ /dev/null @@ -1,25 +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. - -// This file contains tests for the useany checker. - -package a - -type Any interface{} - -func _[T interface{}]() {} // want "could use \"any\" for this empty interface" -func _[X any, T interface{}]() {} // want "could use \"any\" for this empty interface" -func _[any interface{}]() {} // want "could use \"any\" for this empty interface" -func _[T Any]() {} // want "could use \"any\" for this empty interface" -func _[T interface{ int | interface{} }]() {} // want "could use \"any\" for this empty interface" -func _[T interface{ int | Any }]() {} // want "could use \"any\" for this empty interface" -func _[T any]() {} - -type _[T interface{}] int // want "could use \"any\" for this empty interface" -type _[X any, T interface{}] int // want "could use \"any\" for this empty interface" -type _[any interface{}] int // want "could use \"any\" for this empty interface" -type _[T Any] int // want "could use \"any\" for this empty interface" -type _[T interface{ int | interface{} }] int // want "could use \"any\" for this empty interface" -type _[T interface{ int | Any }] int // want "could use \"any\" for this empty interface" -type _[T any] int diff --git a/gopls/internal/analysis/useany/testdata/src/a/a.go.golden b/gopls/internal/analysis/useany/testdata/src/a/a.go.golden deleted file mode 100644 index efd8fd640a4..00000000000 --- a/gopls/internal/analysis/useany/testdata/src/a/a.go.golden +++ /dev/null @@ -1,25 +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. - -// This file contains tests for the useany checker. - -package a - -type Any interface{} - -func _[T any]() {} // want "could use \"any\" for this empty interface" -func _[X any, T any]() {} // want "could use \"any\" for this empty interface" -func _[any interface{}]() {} // want "could use \"any\" for this empty interface" -func _[T any]() {} // want "could use \"any\" for this empty interface" -func _[T any]() {} // want "could use \"any\" for this empty interface" -func _[T any]() {} // want "could use \"any\" for this empty interface" -func _[T any]() {} - -type _[T any] int // want "could use \"any\" for this empty interface" -type _[X any, T any] int // want "could use \"any\" for this empty interface" -type _[any interface{}] int // want "could use \"any\" for this empty interface" -type _[T any] int // want "could use \"any\" for this empty interface" -type _[T any] int // want "could use \"any\" for this empty interface" -type _[T any] int // want "could use \"any\" for this empty interface" -type _[T any] int diff --git a/gopls/internal/analysis/useany/useany.go b/gopls/internal/analysis/useany/useany.go deleted file mode 100644 index ff25e5945d3..00000000000 --- a/gopls/internal/analysis/useany/useany.go +++ /dev/null @@ -1,98 +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 useany defines an Analyzer that checks for usage of interface{} in -// constraints, rather than the predeclared any. -package useany - -import ( - "fmt" - "go/ast" - "go/token" - "go/types" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" -) - -const Doc = `check for constraints that could be simplified to "any"` - -var Analyzer = &analysis.Analyzer{ - Name: "useany", - Doc: Doc, - Requires: []*analysis.Analyzer{inspect.Analyzer}, - Run: run, - URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany", -} - -func run(pass *analysis.Pass) (interface{}, error) { - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - - universeAny := types.Universe.Lookup("any") - - nodeFilter := []ast.Node{ - (*ast.TypeSpec)(nil), - (*ast.FuncType)(nil), - } - - inspect.Preorder(nodeFilter, func(node ast.Node) { - var tparams *ast.FieldList - switch node := node.(type) { - case *ast.TypeSpec: - tparams = node.TypeParams - case *ast.FuncType: - tparams = node.TypeParams - default: - panic(fmt.Sprintf("unexpected node type %T", node)) - } - if tparams.NumFields() == 0 { - return - } - - for _, field := range tparams.List { - typ := pass.TypesInfo.Types[field.Type].Type - if typ == nil { - continue // something is wrong, but not our concern - } - iface, ok := typ.Underlying().(*types.Interface) - if !ok { - continue // invalid constraint - } - - // If the constraint is the empty interface, offer a fix to use 'any' - // instead. - if iface.Empty() { - id, _ := field.Type.(*ast.Ident) - if id != nil && pass.TypesInfo.Uses[id] == universeAny { - continue - } - - diag := analysis.Diagnostic{ - Pos: field.Type.Pos(), - End: field.Type.End(), - Message: `could use "any" for this empty interface`, - } - - // Only suggest a fix to 'any' if we actually resolve the predeclared - // any in this scope. - if scope := pass.TypesInfo.Scopes[node]; scope != nil { - if _, any := scope.LookupParent("any", token.NoPos); any == universeAny { - diag.SuggestedFixes = []analysis.SuggestedFix{{ - Message: `use "any"`, - TextEdits: []analysis.TextEdit{{ - Pos: field.Type.Pos(), - End: field.Type.End(), - NewText: []byte("any"), - }}, - }} - } - } - - pass.Report(diag) - } - } - }) - return nil, nil -} diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index 0964078f0e5..4c5abbc23ce 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go @@ -490,14 +490,7 @@ func (an *analysisNode) summaryHash() file.Hash { fmt.Fprintf(hasher, "compiles: %t\n", an.summary.Compiles) // 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] + for name, summary := range moremaps.Sorted(an.summary.Actions) { fmt.Fprintf(hasher, "action %s\n", name) if summary.Err != "" { fmt.Fprintf(hasher, "error %s\n", summary.Err) diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index dbae63b8529..068fa70b4ed 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -33,6 +33,7 @@ import ( "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/event" @@ -1314,13 +1315,7 @@ func typerefsKey(id PackageID, imports map[ImportPath]*metadata.Package, compile fmt.Fprintf(hasher, "typerefs: %s\n", id) - importPaths := make([]string, 0, len(imports)) - for impPath := range imports { - importPaths = append(importPaths, string(impPath)) - } - sort.Strings(importPaths) - for _, importPath := range importPaths { - imp := imports[ImportPath(importPath)] + for importPath, imp := range moremaps.Sorted(imports) { // TODO(rfindley): strength reduce the typerefs.Export API to guarantee // that it only depends on these attributes of dependencies. fmt.Fprintf(hasher, "import %s %s %s", importPath, imp.ID, imp.Name) @@ -1431,13 +1426,8 @@ func localPackageKey(inputs *typeCheckInputs) file.Hash { fmt.Fprintf(hasher, "go %s\n", inputs.goVersion) // import map - importPaths := make([]string, 0, len(inputs.depsByImpPath)) - for impPath := range inputs.depsByImpPath { - importPaths = append(importPaths, string(impPath)) - } - sort.Strings(importPaths) - for _, impPath := range importPaths { - fmt.Fprintf(hasher, "import %s %s", impPath, string(inputs.depsByImpPath[ImportPath(impPath)])) + for impPath, depID := range moremaps.Sorted(inputs.depsByImpPath) { + fmt.Fprintf(hasher, "import %s %s", impPath, depID) } // file names and contents @@ -1720,7 +1710,7 @@ func depsErrors(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) ( } directImporter := depsError.ImportStack[directImporterIdx] - if snapshot.isWorkspacePackage(PackageID(directImporter)) { + if snapshot.IsWorkspacePackage(PackageID(directImporter)) { continue } relevantErrors = append(relevantErrors, depsError) @@ -1766,7 +1756,7 @@ func depsErrors(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) ( for _, depErr := range relevantErrors { for i := len(depErr.ImportStack) - 1; i >= 0; i-- { item := depErr.ImportStack[i] - if snapshot.isWorkspacePackage(PackageID(item)) { + if snapshot.IsWorkspacePackage(PackageID(item)) { break } diff --git a/gopls/internal/cache/diagnostics.go b/gopls/internal/cache/diagnostics.go index 0adbcb495db..68c1632594f 100644 --- a/gopls/internal/cache/diagnostics.go +++ b/gopls/internal/cache/diagnostics.go @@ -95,21 +95,24 @@ func (d *Diagnostic) Hash() file.Hash { return hash } +// A DiagnosticSource identifies the source of a diagnostic. +// +// Its value may be one of the distinguished string values below, or +// the Name of an [analysis.Analyzer]. type DiagnosticSource string const ( - UnknownError DiagnosticSource = "" - ListError DiagnosticSource = "go list" - ParseError DiagnosticSource = "syntax" - TypeError DiagnosticSource = "compiler" - ModTidyError DiagnosticSource = "go mod tidy" - OptimizationDetailsError DiagnosticSource = "optimizer details" - UpgradeNotification DiagnosticSource = "upgrade available" - Vulncheck DiagnosticSource = "vulncheck imports" - Govulncheck DiagnosticSource = "govulncheck" - TemplateError DiagnosticSource = "template" - WorkFileError DiagnosticSource = "go.work file" - ConsistencyInfo DiagnosticSource = "consistency" + UnknownError DiagnosticSource = "" + ListError DiagnosticSource = "go list" + ParseError DiagnosticSource = "syntax" + TypeError DiagnosticSource = "compiler" + ModTidyError DiagnosticSource = "go mod tidy" + CompilerOptDetailsInfo DiagnosticSource = "optimizer details" // cmd/compile -json=0,dir + UpgradeNotification DiagnosticSource = "upgrade available" + Vulncheck DiagnosticSource = "vulncheck imports" + Govulncheck DiagnosticSource = "govulncheck" + TemplateError DiagnosticSource = "template" + WorkFileError DiagnosticSource = "go.work file" ) // A SuggestedFix represents a suggested fix (for a diagnostic) diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go index 4868c0fa877..873cef56a2b 100644 --- a/gopls/internal/cache/load.go +++ b/gopls/internal/cache/load.go @@ -612,22 +612,6 @@ func computeLoadDiagnostics(ctx context.Context, snapshot *Snapshot, mp *metadat return diags } -// IsWorkspacePackage reports whether id points to a workspace package in s. -// -// Currently, the result depends on the current set of loaded packages, and so -// is not guaranteed to be stable. -func (s *Snapshot) IsWorkspacePackage(ctx context.Context, id PackageID) bool { - s.mu.Lock() - defer s.mu.Unlock() - - mg := s.meta - m := mg.Packages[id] - if m == nil { - return false - } - return isWorkspacePackageLocked(ctx, s, mg, m) -} - // isWorkspacePackageLocked reports whether p is a workspace package for the // snapshot s. // diff --git a/gopls/internal/cache/methodsets/fingerprint.go b/gopls/internal/cache/methodsets/fingerprint.go new file mode 100644 index 00000000000..05ccfe0911c --- /dev/null +++ b/gopls/internal/cache/methodsets/fingerprint.go @@ -0,0 +1,357 @@ +// 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 methodsets + +import ( + "fmt" + "go/types" + "reflect" + "strconv" + "strings" + "text/scanner" +) + +// Fingerprint syntax +// +// The lexical syntax is essentially Lisp S-expressions: +// +// expr = STRING | INTEGER | IDENT | '(' expr... ')' +// +// where the tokens are as defined by text/scanner. +// +// The grammar of expression forms is: +// +// τ = IDENT -- named or basic type +// | (qual STRING IDENT) -- qualified named type +// | (array INTEGER τ) +// | (slice τ) +// | (ptr τ) +// | (chan IDENT τ) +// | (func τ v? τ) -- signature params, results, variadic? +// | (map τ τ) +// | (struct field*) +// | (tuple τ*) +// | (interface) -- nonempty interface (lossy) +// | (typeparam INTEGER) +// | (inst τ τ...) -- instantiation of a named type +// +// field = IDENT IDENT STRING τ -- name, embedded?, tag, type + +// fingerprint returns an encoding of a [types.Type] such that, in +// most cases, fingerprint(x) == fingerprint(t) iff types.Identical(x, y). +// +// For a minority of types, mostly involving type parameters, identity +// cannot be reduced to string comparison; these types are called +// "tricky", and are indicated by the boolean result. +// +// In general, computing identity correctly for tricky types requires +// the type checker. However, the fingerprint encoding can be parsed +// by [parseFingerprint] into a tree form that permits simple matching +// sufficient to allow a type parameter to unify with any subtree. +// +// In the standard library, 99.8% of package-level types have a +// non-tricky method-set. The most common exceptions are due to type +// parameters. +// +// fingerprint is defined only for the signature types of methods. It +// must not be called for "untyped" basic types, nor the type of a +// generic function. +func fingerprint(t types.Type) (string, bool) { + var buf strings.Builder + tricky := false + var print func(t types.Type) + print = func(t types.Type) { + switch t := t.(type) { + case *types.Alias: + print(types.Unalias(t)) + + case *types.Named: + targs := t.TypeArgs() + if targs != nil { + buf.WriteString("(inst ") + } + tname := t.Obj() + if tname.Pkg() != nil { + fmt.Fprintf(&buf, "(qual %q %s)", tname.Pkg().Path(), tname.Name()) + } else if tname.Name() != "error" && tname.Name() != "comparable" { + panic(tname) // error and comparable the only named types with no package + } else { + buf.WriteString(tname.Name()) + } + if targs != nil { + for i := range targs.Len() { + buf.WriteByte(' ') + print(targs.At(i)) + } + buf.WriteString(")") + } + + case *types.Array: + fmt.Fprintf(&buf, "(array %d ", t.Len()) + print(t.Elem()) + buf.WriteByte(')') + + case *types.Slice: + buf.WriteString("(slice ") + print(t.Elem()) + buf.WriteByte(')') + + case *types.Pointer: + buf.WriteString("(ptr ") + print(t.Elem()) + buf.WriteByte(')') + + case *types.Map: + buf.WriteString("(map ") + print(t.Key()) + buf.WriteByte(' ') + print(t.Elem()) + buf.WriteByte(')') + + case *types.Chan: + fmt.Fprintf(&buf, "(chan %d ", t.Dir()) + print(t.Elem()) + buf.WriteByte(')') + + case *types.Tuple: + buf.WriteString("(tuple") + for i := range t.Len() { + buf.WriteByte(' ') + print(t.At(i).Type()) + } + buf.WriteByte(')') + + case *types.Basic: + // Print byte/uint8 as "byte" instead of calling + // BasicType.String, which prints the two distinctly + // (even though their Kinds are numerically equal). + // Ditto for rune/int32. + switch t.Kind() { + case types.Byte: + buf.WriteString("byte") + case types.Rune: + buf.WriteString("rune") + case types.UnsafePointer: + buf.WriteString(`(qual "unsafe" Pointer)`) + default: + if t.Info()&types.IsUntyped != 0 { + panic("fingerprint of untyped type") + } + buf.WriteString(t.String()) + } + + case *types.Signature: + buf.WriteString("(func ") + print(t.Params()) + if t.Variadic() { + buf.WriteString(" v") + } + buf.WriteByte(' ') + print(t.Results()) + buf.WriteByte(')') + + case *types.Struct: + // Non-empty unnamed struct types in method + // signatures are vanishingly rare. + buf.WriteString("(struct") + for i := range t.NumFields() { + f := t.Field(i) + name := f.Name() + if !f.Exported() { + name = fmt.Sprintf("(qual %q %s)", f.Pkg().Path(), name) + } + + // This isn't quite right for embedded type aliases. + // (See types.TypeString(StructType) and #44410 for context.) + // But this is vanishingly rare. + fmt.Fprintf(&buf, " %s %t %q ", name, f.Embedded(), t.Tag(i)) + print(f.Type()) + } + buf.WriteByte(')') + + case *types.Interface: + if t.NumMethods() == 0 { + buf.WriteString("any") // common case + } else { + // Interface assignability is particularly + // tricky due to the possibility of recursion. + // However, nontrivial interface type literals + // are exceedingly rare in function signatures. + // + // TODO(adonovan): add disambiguating precision + // (e.g. number of methods, their IDs and arities) + // as needs arise (i.e. collisions are observed). + tricky = true + buf.WriteString("(interface)") + } + + case *types.TypeParam: + // Matching of type parameters will require + // parsing fingerprints and unification. + tricky = true + fmt.Fprintf(&buf, "(%s %d)", symTypeparam, t.Index()) + + default: // incl. *types.Union + panic(t) + } + } + + print(t) + + return buf.String(), tricky +} + +const symTypeparam = "typeparam" + +// sexpr defines the representation of a fingerprint tree. +type ( + sexpr any // = string | int | symbol | *cons | nil + symbol string + cons struct{ car, cdr sexpr } +) + +// parseFingerprint returns the type encoded by fp in tree form. +// +// The input must have been produced by [fingerprint] at the same +// source version; parsing is thus infallible. +func parseFingerprint(fp string) sexpr { + var scan scanner.Scanner + scan.Error = func(scan *scanner.Scanner, msg string) { panic(msg) } + scan.Init(strings.NewReader(fp)) + + // next scans a token and updates tok. + var tok rune + next := func() { tok = scan.Scan() } + + next() + + // parse parses a fingerprint and returns its tree. + var parse func() sexpr + parse = func() sexpr { + if tok == '(' { + next() // consume '(' + var head sexpr // empty list + tailcdr := &head + for tok != ')' { + cell := &cons{car: parse()} + *tailcdr = cell + tailcdr = &cell.cdr + } + next() // consume ')' + return head + } + + s := scan.TokenText() + switch tok { + case scanner.Ident: + next() // consume IDENT + return symbol(s) + + case scanner.Int: + next() // consume INT + i, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return i + + case scanner.String: + next() // consume STRING + s, err := strconv.Unquote(s) + if err != nil { + panic(err) + } + return s + + default: + panic(tok) + } + } + + return parse() +} + +func sexprString(x sexpr) string { + var out strings.Builder + writeSexpr(&out, x) + return out.String() +} + +// writeSexpr formats an S-expression. +// It is provided for debugging. +func writeSexpr(out *strings.Builder, x sexpr) { + switch x := x.(type) { + case nil: + out.WriteString("()") + case string: + fmt.Fprintf(out, "%q", x) + case int: + fmt.Fprintf(out, "%d", x) + case symbol: + out.WriteString(string(x)) + case *cons: + out.WriteString("(") + for { + writeSexpr(out, x.car) + if x.cdr == nil { + break + } else if cdr, ok := x.cdr.(*cons); ok { + x = cdr + out.WriteByte(' ') + } else { + // Dotted list: should never happen, + // but support it for debugging. + out.WriteString(" . ") + print(x.cdr) + break + } + } + out.WriteString(")") + default: + panic(x) + } +} + +// unify reports whether the types of methods x and y match, in the +// presence of type parameters, each of which matches anything at all. +// (It's not true unification as we don't track substitutions.) +// +// TODO(adonovan): implement full unification. +func unify(x, y sexpr) bool { + if isTypeParam(x) >= 0 || isTypeParam(y) >= 0 { + return true // a type parameter matches anything + } + if reflect.TypeOf(x) != reflect.TypeOf(y) { + return false // type mismatch + } + switch x := x.(type) { + case nil, string, int, symbol: + return x == y + case *cons: + y := y.(*cons) + if !unify(x.car, y.car) { + return false + } + if x.cdr == nil { + return y.cdr == nil + } + if y.cdr == nil { + return false + } + return unify(x.cdr, y.cdr) + default: + panic(fmt.Sprintf("unify %T %T", x, y)) + } +} + +// isTypeParam returns the index of the type parameter, +// if x has the form "(typeparam INTEGER)", otherwise -1. +func isTypeParam(x sexpr) int { + if x, ok := x.(*cons); ok { + if sym, ok := x.car.(symbol); ok && sym == symTypeparam { + return 0 + } + } + return -1 +} diff --git a/gopls/internal/cache/methodsets/fingerprint_test.go b/gopls/internal/cache/methodsets/fingerprint_test.go new file mode 100644 index 00000000000..a9f47c1a2e6 --- /dev/null +++ b/gopls/internal/cache/methodsets/fingerprint_test.go @@ -0,0 +1,188 @@ +// 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 methodsets + +// This is an internal test of [fingerprint] and [unify]. +// +// TODO(adonovan): avoid internal tests. +// Break fingerprint.go off into its own package? + +import ( + "go/types" + "testing" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/txtar" +) + +// Test_fingerprint runs the fingerprint encoder, decoder, and printer +// on the types of all package-level symbols in gopls, and ensures +// that parse+print is lossless. +func Test_fingerprint(t *testing.T) { + if testing.Short() { + t.Skip("skipping slow test") + } + + cfg := &packages.Config{Mode: packages.NeedTypes} + pkgs, err := packages.Load(cfg, "std", "golang.org/x/tools/gopls/...") + if err != nil { + t.Fatal(err) + } + + // Record the fingerprint of each logical type (equivalence + // class of types.Types) and assert that they are all equal. + // (Non-tricky types only.) + var fingerprints typeutil.Map + + type eqclass struct { + class map[types.Type]bool + fp string + } + + for _, pkg := range pkgs { + switch pkg.Types.Path() { + case "unsafe", "builtin": + continue + } + scope := pkg.Types.Scope() + for _, name := range scope.Names() { + obj := scope.Lookup(name) + typ := obj.Type() + + if basic, ok := typ.(*types.Basic); ok && + basic.Info()&types.IsUntyped != 0 { + continue // untyped constant + } + + fp, tricky := fingerprint(typ) // check Type encoder doesn't panic + + // All equivalent (non-tricky) types have the same fingerprint. + if !tricky { + if prevfp, ok := fingerprints.At(typ).(string); !ok { + fingerprints.Set(typ, fp) + } else if fp != prevfp { + t.Errorf("inconsistent fingerprints for type %v:\n- old: %s\n- new: %s", + typ, fp, prevfp) + } + } + + tree := parseFingerprint(fp) // check parser doesn't panic + fp2 := sexprString(tree) // check formatter doesn't pannic + + // A parse+print round-trip should be lossless. + if fp != fp2 { + t.Errorf("%s: %v: parse+print changed fingerprint:\n"+ + "was: %s\ngot: %s\ntype: %v", + pkg.Fset.Position(obj.Pos()), obj, fp, fp2, typ) + } + } + } +} + +// Test_unify exercises the matching algorithm for generic types. +func Test_unify(t *testing.T) { + if testenv.Go1Point() < 24 { + testenv.NeedsGoExperiment(t, "aliastypeparams") // testenv.Go1Point() >= 24 implies aliastypeparams=1 + } + + const src = ` +-- go.mod -- +module example.com +go 1.24 + +-- a/a.go -- +package a + +type Int = int +type String = string + +// Eq.Equal matches casefold.Equal. +type Eq[T any] interface { Equal(T, T) bool } +type casefold struct{} +func (casefold) Equal(x, y string) bool + +// A matches AString. +type A[T any] = struct { x T } +type AString = struct { x string } + +// B matches anything! +type B[T any] = T + +func C1[T any](int, T, ...string) T { panic(0) } +func C2[U any](int, int, ...U) bool { panic(0) } +func C3(int, bool, ...string) rune +func C4(int, bool, ...string) +func C5(int, float64, bool, string) bool + +func DAny[T any](Named[T]) { panic(0) } +func DString(Named[string]) +func DInt(Named[int]) + +type Named[T any] struct { x T } + +func E1(byte) rune +func E2(uint8) int32 +func E3(int8) uint32 +` + pkg := testfiles.LoadPackages(t, txtar.Parse([]byte(src)), "./a")[0] + scope := pkg.Types.Scope() + for _, test := range []struct { + a, b string + method string // optional field or method + want bool + }{ + {"Eq", "casefold", "Equal", true}, + {"A", "AString", "", true}, + {"A", "Eq", "", false}, // completely unrelated + {"B", "String", "", true}, + {"B", "Int", "", true}, + {"B", "A", "", true}, + {"C1", "C2", "", true}, // matches despite inconsistent substitution + {"C1", "C3", "", true}, + {"C1", "C4", "", false}, + {"C1", "C5", "", false}, + {"C2", "C3", "", false}, // intransitive (C1≡C2 ^ C1≡C3) + {"C2", "C4", "", false}, + {"C3", "C4", "", false}, + {"DAny", "DString", "", true}, + {"DAny", "DInt", "", true}, + {"DString", "DInt", "", false}, // different instantiations of Named + {"E1", "E2", "", true}, // byte and rune are just aliases + {"E2", "E3", "", false}, + } { + lookup := func(name string) types.Type { + obj := scope.Lookup(name) + if obj == nil { + t.Fatalf("Lookup %s failed", name) + } + if test.method != "" { + obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, pkg.Types, test.method) + if obj == nil { + t.Fatalf("Lookup %s.%s failed", name, test.method) + } + } + return obj.Type() + } + + a := lookup(test.a) + b := lookup(test.b) + + afp, _ := fingerprint(a) + bfp, _ := fingerprint(b) + + atree := parseFingerprint(afp) + btree := parseFingerprint(bfp) + + got := unify(atree, btree) + if got != test.want { + t.Errorf("a=%s b=%s method=%s: unify returned %t for these inputs:\n- %s\n- %s", + test.a, test.b, test.method, + got, sexprString(atree), sexprString(btree)) + } + } +} diff --git a/gopls/internal/cache/methodsets/methodsets.go b/gopls/internal/cache/methodsets/methodsets.go index d9173b3b4c3..3026819ee81 100644 --- a/gopls/internal/cache/methodsets/methodsets.go +++ b/gopls/internal/cache/methodsets/methodsets.go @@ -44,12 +44,11 @@ package methodsets // single 64-bit mask is quite effective. See CL 452060 for details. import ( - "fmt" "go/token" "go/types" "hash/crc32" - "strconv" - "strings" + "slices" + "sync/atomic" "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/util/bug" @@ -95,7 +94,7 @@ type Location struct { // A Key represents the method set of a given type in a form suitable // to pass to the (*Index).Search method of many different Indexes. type Key struct { - mset gobMethodSet // note: lacks position information + mset *gobMethodSet // note: lacks position information } // KeyOf returns the search key for the method sets of a given type. @@ -123,7 +122,7 @@ type Result struct { // // The result does not include the error.Error method. // TODO(adonovan): give this special case a more systematic treatment. -func (index *Index) Search(key Key, methodID string) []Result { +func (index *Index) Search(key Key, method *types.Func) []Result { var results []Result for _, candidate := range index.pkg.MethodSets { // Traditionally this feature doesn't report @@ -133,30 +132,22 @@ func (index *Index) Search(key Key, methodID string) []Result { if candidate.IsInterface && key.mset.IsInterface { continue } - if !satisfies(candidate, key.mset) && !satisfies(key.mset, candidate) { - continue - } - if candidate.Tricky { - // If any interface method is tricky then extra - // checking may be needed to eliminate a false positive. - // TODO(adonovan): implement it. + if !implements(candidate, key.mset) && !implements(key.mset, candidate) { + continue } - if methodID == "" { + if method == nil { results = append(results, Result{Location: index.location(candidate.Posn)}) } else { for _, m := range candidate.Methods { - // Here we exploit knowledge of the shape of the fingerprint string. - if strings.HasPrefix(m.Fingerprint, methodID) && - m.Fingerprint[len(methodID)] == '(' { - + if m.ID == method.Id() { // Don't report error.Error among the results: // it has no true source location, no package, // and is excluded from the xrefs index. if m.PkgPath == 0 || m.ObjectPath == 0 { - if methodID != "Error" { - panic("missing info for" + methodID) + if m.ID != "Error" { + panic("missing info for" + m.ID) } continue } @@ -174,23 +165,48 @@ func (index *Index) Search(key Key, methodID string) []Result { return results } -// satisfies does a fast check for whether x satisfies y. -func satisfies(x, y gobMethodSet) bool { - return y.IsInterface && x.Mask&y.Mask == y.Mask && subset(y, x) -} +// implements reports whether x implements y. +func implements(x, y *gobMethodSet) bool { + if !y.IsInterface { + return false + } -// subset reports whether method set x is a subset of y. -func subset(x, y gobMethodSet) bool { -outer: - for _, mx := range x.Methods { - for _, my := range y.Methods { - if mx.Sum == my.Sum && mx.Fingerprint == my.Fingerprint { - continue outer // found; try next x method + // Fast path: neither method set is tricky, so all methods can + // be compared by equality of ID and Fingerprint, and the + // entire subset check can be done using the bit mask. + if !x.Tricky && !y.Tricky { + if x.Mask&y.Mask != y.Mask { + return false // x lacks a method of interface y + } + } + + // At least one operand is tricky (e.g. contains a type parameter), + // so we must used tree-based matching (unification). + + // nonmatching reports whether interface method 'my' lacks + // a matching method in set x. (The sense is inverted for use + // with slice.ContainsFunc below.) + nonmatching := func(my *gobMethod) bool { + for _, mx := range x.Methods { + if mx.ID == my.ID { + var match bool + if !mx.Tricky && !my.Tricky { + // Fast path: neither method is tricky, + // so a string match is sufficient. + match = mx.Sum&my.Sum == my.Sum && mx.Fingerprint == my.Fingerprint + } else { + match = unify(mx.parse(), my.parse()) + } + return !match } } - return false // method of x not found in y + return true // method of y not found in x } - return true // all methods of x found in y + + // Each interface method must have a match. + // (This would be more readable with a DeMorganized + // variant of ContainsFunc.) + return !slices.ContainsFunc(y.Methods, nonmatching) } func (index *Index) location(posn gobPosition) Location { @@ -296,7 +312,7 @@ func (b *indexBuilder) string(s string) int { // It calls the optional setIndexInfo function for each gobMethod. // This is used during index construction, but not search (KeyOf), // to store extra information. -func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) gobMethodSet { +func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) *gobMethodSet { // For non-interface types, use *T // (if T is not already a pointer) // since it may have more methods. @@ -305,21 +321,24 @@ func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) gob // Convert the method set into a compact summary. var mask uint64 tricky := false - methods := make([]gobMethod, mset.Len()) + var buf []byte + methods := make([]*gobMethod, mset.Len()) for i := 0; i < mset.Len(); i++ { m := mset.At(i).Obj().(*types.Func) - fp, isTricky := fingerprint(m) + id := m.Id() + fp, isTricky := fingerprint(m.Signature()) if isTricky { tricky = true } - sum := crc32.ChecksumIEEE([]byte(fp)) - methods[i] = gobMethod{Fingerprint: fp, Sum: sum} + buf = append(append(buf[:0], id...), fp...) + sum := crc32.ChecksumIEEE(buf) + methods[i] = &gobMethod{ID: id, Fingerprint: fp, Sum: sum, Tricky: isTricky} if setIndexInfo != nil { - setIndexInfo(&methods[i], m) // set Position, PkgPath, ObjectPath + setIndexInfo(methods[i], m) // set Position, PkgPath, ObjectPath } mask |= 1 << uint64(((sum>>24)^(sum>>16)^(sum>>8)^sum)&0x3f) } - return gobMethodSet{ + return &gobMethodSet{ IsInterface: types.IsInterface(t), Tricky: tricky, Mask: mask, @@ -337,149 +356,6 @@ func EnsurePointer(T types.Type) types.Type { return T } -// fingerprint returns an encoding of a method signature such that two -// methods with equal encodings have identical types, except for a few -// tricky types whose encodings may spuriously match and whose exact -// identity computation requires the type checker to eliminate false -// positives (which are rare). The boolean result indicates whether -// the result was one of these tricky types. -// -// In the standard library, 99.8% of package-level types have a -// non-tricky method-set. The most common exceptions are due to type -// parameters. -// -// The fingerprint string starts with method.Id() + "(". -func fingerprint(method *types.Func) (string, bool) { - var buf strings.Builder - tricky := false - var fprint func(t types.Type) - fprint = func(t types.Type) { - switch t := t.(type) { - case *types.Alias: - fprint(types.Unalias(t)) - - case *types.Named: - tname := t.Obj() - if tname.Pkg() != nil { - buf.WriteString(strconv.Quote(tname.Pkg().Path())) - buf.WriteByte('.') - } else if tname.Name() != "error" && tname.Name() != "comparable" { - panic(tname) // error and comparable the only named types with no package - } - buf.WriteString(tname.Name()) - - case *types.Array: - fmt.Fprintf(&buf, "[%d]", t.Len()) - fprint(t.Elem()) - - case *types.Slice: - buf.WriteString("[]") - fprint(t.Elem()) - - case *types.Pointer: - buf.WriteByte('*') - fprint(t.Elem()) - - case *types.Map: - buf.WriteString("map[") - fprint(t.Key()) - buf.WriteByte(']') - fprint(t.Elem()) - - case *types.Chan: - switch t.Dir() { - case types.SendRecv: - buf.WriteString("chan ") - case types.SendOnly: - buf.WriteString("<-chan ") - case types.RecvOnly: - buf.WriteString("chan<- ") - } - fprint(t.Elem()) - - case *types.Tuple: - buf.WriteByte('(') - for i := 0; i < t.Len(); i++ { - if i > 0 { - buf.WriteByte(',') - } - fprint(t.At(i).Type()) - } - buf.WriteByte(')') - - case *types.Basic: - // Use canonical names for uint8 and int32 aliases. - switch t.Kind() { - case types.Byte: - buf.WriteString("byte") - case types.Rune: - buf.WriteString("rune") - default: - buf.WriteString(t.String()) - } - - case *types.Signature: - buf.WriteString("func") - fprint(t.Params()) - if t.Variadic() { - buf.WriteString("...") // not quite Go syntax - } - fprint(t.Results()) - - case *types.Struct: - // Non-empty unnamed struct types in method - // signatures are vanishingly rare. - buf.WriteString("struct{") - for i := 0; i < t.NumFields(); i++ { - if i > 0 { - buf.WriteByte(';') - } - f := t.Field(i) - // This isn't quite right for embedded type aliases. - // (See types.TypeString(StructType) and #44410 for context.) - // But this is vanishingly rare. - if !f.Embedded() { - buf.WriteString(f.Id()) - buf.WriteByte(' ') - } - fprint(f.Type()) - if tag := t.Tag(i); tag != "" { - buf.WriteByte(' ') - buf.WriteString(strconv.Quote(tag)) - } - } - buf.WriteString("}") - - case *types.Interface: - if t.NumMethods() == 0 { - buf.WriteString("any") // common case - } else { - // Interface assignability is particularly - // tricky due to the possibility of recursion. - tricky = true - // We could still give more disambiguating precision - // than "..." if we wanted to. - buf.WriteString("interface{...}") - } - - case *types.TypeParam: - tricky = true - // TODO(adonovan): refine this by adding a numeric suffix - // indicating the index among the receiver type's parameters. - buf.WriteByte('?') - - default: // incl. *types.Union - panic(t) - } - } - - buf.WriteString(method.Id()) // e.g. "pkg.Type" - sig := method.Signature() - fprint(sig.Params()) - fprint(sig.Results()) - return buf.String(), tricky -} - // -- serial format of index -- // (The name says gob but in fact we use frob.) @@ -488,27 +364,32 @@ var packageCodec = frob.CodecFor[gobPackage]() // A gobPackage records the method set of each package-level type for a single package. type gobPackage struct { Strings []string // index of strings used by gobPosition.File, gobMethod.{Pkg,Object}Path - MethodSets []gobMethodSet + MethodSets []*gobMethodSet } // A gobMethodSet records the method set of a single type. type gobMethodSet struct { Posn gobPosition IsInterface bool - Tricky bool // at least one method is tricky; assignability requires go/types + Tricky bool // at least one method is tricky; fingerprint must be parsed + unified Mask uint64 // mask with 1 bit from each of methods[*].sum - Methods []gobMethod + Methods []*gobMethod } // A gobMethod records the name, type, and position of a single method. type gobMethod struct { - Fingerprint string // string of form "methodID(params...)(results)" - Sum uint32 // checksum of fingerprint + ID string // (*types.Func).Id() value of method + Fingerprint string // encoding of types as string of form "(params)(results)" + Sum uint32 // checksum of ID + fingerprint + Tricky bool // method type contains tricky features (type params, interface types) // index records only (zero in KeyOf; also for index of error.Error). Posn gobPosition // location of method declaration PkgPath int // path of package containing method declaration ObjectPath int // object path of method relative to PkgPath + + // internal fields (not serialized) + tree atomic.Pointer[sexpr] // fingerprint tree, parsed on demand } // A gobPosition records the file, offset, and length of an identifier. @@ -516,3 +397,15 @@ type gobPosition struct { File int // index into gobPackage.Strings Offset, Len int // in bytes } + +// parse returns the method's parsed fingerprint tree. +// It may return a new instance or a cached one. +func (m *gobMethod) parse() sexpr { + ptr := m.tree.Load() + if ptr == nil { + tree := parseFingerprint(m.Fingerprint) + ptr = &tree + m.tree.Store(ptr) // may race; that's ok + } + return *ptr +} diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index 52445a9fbbf..df167314b04 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -21,6 +21,7 @@ import ( "go/scanner" "go/token" "reflect" + "slices" "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" @@ -629,27 +630,17 @@ func readKeyword(pos token.Pos, tok *token.File, src []byte) string { func fixArrayType(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) bool { // Our expected input is a bad expression that looks like "[]someExpr". - from := bad.Pos() - to := bad.End() - - if !from.IsValid() || !to.IsValid() { - return false - } - - exprBytes := make([]byte, 0, int(to-from)+3) - // Avoid doing tok.Offset(to) since that panics if badExpr ends at EOF. - // It also panics if the position is not in the range of the file, and - // badExprs may not necessarily have good positions, so check first. - fromOffset, toOffset, err := safetoken.Offsets(tok, from, to-1) + from, to := bad.Pos(), bad.End() + fromOffset, toOffset, err := safetoken.Offsets(tok, from, to) if err != nil { return false } - exprBytes = append(exprBytes, src[fromOffset:toOffset+1]...) - exprBytes = bytes.TrimSpace(exprBytes) + + exprBytes := bytes.TrimSpace(slices.Clone(src[fromOffset:toOffset])) // If our expression ends in "]" (e.g. "[]"), add a phantom selector // so we can complete directly after the "[]". - if len(exprBytes) > 0 && exprBytes[len(exprBytes)-1] == ']' { + if bytes.HasSuffix(exprBytes, []byte("]")) { exprBytes = append(exprBytes, '_') } diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go index d8c01a17a01..a6f4118e23e 100644 --- a/gopls/internal/cache/session.go +++ b/gopls/internal/cache/session.go @@ -251,7 +251,6 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, * factyAnalysisKeys: new(persistent.Map[PackageID, file.Hash]), meta: new(metadata.Graph), files: newFileMap(), - symbolizeHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), shouldLoad: new(persistent.Map[PackageID, []PackagePath]), unloadableFiles: new(persistent.Set[protocol.DocumentURI]), parseModHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), @@ -349,7 +348,7 @@ func (s *Session) View(id string) (*View, error) { // SnapshotOf returns a Snapshot corresponding to the given URI. // // In the case where the file can be can be associated with a View by -// bestViewForURI (based on directory information alone, without package +// [RelevantViews] (based on directory information alone, without package // metadata), SnapshotOf returns the current Snapshot for that View. Otherwise, // it awaits loading package metadata and returns a Snapshot for the first View // containing a real (=not command-line-arguments) package for the file. @@ -552,13 +551,12 @@ checkFiles: } def, err = defineView(ctx, fs, folder, fh) if err != nil { - // We should never call selectViewDefs with a cancellable context, so - // this should never fail. - return nil, bug.Errorf("failed to define view for open file: %v", err) + // e.g. folder path is invalid? + return nil, fmt.Errorf("failed to define view for open file: %v", err) } // It need not strictly be the case that the best view for a file is // distinct from other views, as the logic of getViewDefinition and - // bestViewForURI does not align perfectly. This is not necessarily a bug: + // [RelevantViews] does not align perfectly. This is not necessarily a bug: // there may be files for which we can't construct a valid view. // // Nevertheless, we should not create redundant views. @@ -573,7 +571,7 @@ checkFiles: return defs, nil } -// The viewDefiner interface allows the bestView algorithm to operate on both +// The viewDefiner interface allows the [RelevantViews] algorithm to operate on both // Views and viewDefinitions. type viewDefiner interface{ definition() *viewDefinition } diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 46b0a6a1b5c..de4a52ff6cb 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -17,14 +17,12 @@ import ( "path" "path/filepath" "regexp" - "runtime" "slices" "sort" "strconv" "strings" "sync" - "golang.org/x/sync/errgroup" "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" @@ -127,10 +125,6 @@ type Snapshot struct { // It may invalidated when a file's content changes. files *fileMap - // symbolizeHandles maps each file URI to a handle for the future - // result of computing the symbols declared in that file. - symbolizeHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[symbolizeResult] - // packages maps a packageKey to a *packageHandle. // It may be invalidated when a file's content changes. // @@ -189,9 +183,9 @@ type Snapshot struct { // vulns maps each go.mod file's URI to its known vulnerabilities. vulns *persistent.Map[protocol.DocumentURI, *vulncheck.Result] - // gcOptimizationDetails describes the packages for which we want - // optimization details to be included in the diagnostics. - gcOptimizationDetails map[metadata.PackageID]unit + // compilerOptDetails describes the packages for which we want + // compiler optimization details to be included in the diagnostics. + compilerOptDetails map[metadata.PackageID]unit // Concurrent type checking: // typeCheckMu guards the ongoing type checking batch, and reference count of @@ -238,7 +232,6 @@ func (s *Snapshot) decref() { if s.refcount == 0 { s.packages.Destroy() s.files.destroy() - s.symbolizeHandles.Destroy() s.parseModHandles.Destroy() s.parseWorkHandles.Destroy() s.modTidyHandles.Destroy() @@ -527,6 +520,7 @@ const ( exportDataKind = "export" diagnosticsKind = "diagnostics" typerefsKind = "typerefs" + symbolsKind = "symbols" ) // PackageDiagnostics returns diagnostics for files contained in specified @@ -952,73 +946,24 @@ func (s *Snapshot) WorkspaceMetadata(ctx context.Context) ([]*metadata.Package, return meta, nil } -// isWorkspacePackage reports whether the given package ID refers to a -// workspace package for the snapshot. -func (s *Snapshot) isWorkspacePackage(id PackageID) bool { +// WorkspacePackages returns the map of workspace package to package path. +// +// The set of workspace packages is updated after every load. A package is a +// workspace package if and only if it is present in this map. +func (s *Snapshot) WorkspacePackages() immutable.Map[PackageID, PackagePath] { s.mu.Lock() defer s.mu.Unlock() - _, ok := s.workspacePackages.Value(id) - return ok + return s.workspacePackages } -// Symbols extracts and returns symbol information for every file contained in -// a loaded package. It awaits snapshot loading. -// -// If workspaceOnly is set, this only includes symbols from files in a -// workspace package. Otherwise, it returns symbols from all loaded packages. -// -// TODO(rfindley): move to symbols.go. -func (s *Snapshot) Symbols(ctx context.Context, workspaceOnly bool) (map[protocol.DocumentURI][]Symbol, error) { - var ( - meta []*metadata.Package - err error - ) - if workspaceOnly { - meta, err = s.WorkspaceMetadata(ctx) - } else { - meta, err = s.AllMetadata(ctx) - } - if err != nil { - return nil, fmt.Errorf("loading metadata: %v", err) - } - - goFiles := make(map[protocol.DocumentURI]struct{}) - for _, mp := range meta { - for _, uri := range mp.GoFiles { - goFiles[uri] = struct{}{} - } - for _, uri := range mp.CompiledGoFiles { - goFiles[uri] = struct{}{} - } - } - - // Symbolize them in parallel. - var ( - group errgroup.Group - nprocs = 2 * runtime.GOMAXPROCS(-1) // symbolize is a mix of I/O and CPU - resultMu sync.Mutex - result = make(map[protocol.DocumentURI][]Symbol) - ) - group.SetLimit(nprocs) - for uri := range goFiles { - uri := uri - group.Go(func() error { - symbols, err := s.symbolize(ctx, uri) - if err != nil { - return err - } - resultMu.Lock() - result[uri] = symbols - resultMu.Unlock() - return nil - }) - } - // Keep going on errors, but log the first failure. - // Partial results are better than no symbol results. - if err := group.Wait(); err != nil { - event.Error(ctx, "getting snapshot symbols", err) - } - return result, nil +// IsWorkspacePackage reports whether the given package ID refers to a +// workspace package for the Snapshot. It is equivalent to looking up the +// package in [Snapshot.WorkspacePackages]. +func (s *Snapshot) IsWorkspacePackage(id PackageID) bool { + s.mu.Lock() + defer s.mu.Unlock() + _, ok := s.workspacePackages.Value(id) + return ok } // AllMetadata returns a new unordered array of metadata for @@ -1547,7 +1492,7 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f // TODO(rfindley): reorganize this function to make the derivation of // needsDiagnosis clearer. - needsDiagnosis := len(changed.GCDetails) > 0 || len(changed.ModuleUpgrades) > 0 || len(changed.Vulns) > 0 + needsDiagnosis := len(changed.CompilerOptDetails) > 0 || len(changed.ModuleUpgrades) > 0 || len(changed.Vulns) > 0 bgCtx, cancel := context.WithCancel(bgCtx) result := &Snapshot{ @@ -1565,7 +1510,6 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f fullAnalysisKeys: s.fullAnalysisKeys.Clone(), factyAnalysisKeys: s.factyAnalysisKeys.Clone(), files: s.files.clone(changedFiles), - symbolizeHandles: cloneWithout(s.symbolizeHandles, changedFiles, nil), workspacePackages: s.workspacePackages, shouldLoad: s.shouldLoad.Clone(), // not cloneWithout: shouldLoad is cleared on loads unloadableFiles: s.unloadableFiles.Clone(), // not cloneWithout: typing in a file doesn't necessarily make it loadable @@ -1578,22 +1522,22 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f vulns: cloneWith(s.vulns, changed.Vulns), } - // Compute the new set of packages for which we want gc details, after - // applying changed.GCDetails. - if len(s.gcOptimizationDetails) > 0 || len(changed.GCDetails) > 0 { - newGCDetails := make(map[metadata.PackageID]unit) - for id := range s.gcOptimizationDetails { - if _, ok := changed.GCDetails[id]; !ok { - newGCDetails[id] = unit{} // no change + // Compute the new set of packages for which we want compiler + // optimization details, after applying changed.CompilerOptDetails. + if len(s.compilerOptDetails) > 0 || len(changed.CompilerOptDetails) > 0 { + newCompilerOptDetails := make(map[metadata.PackageID]unit) + for id := range s.compilerOptDetails { + if _, ok := changed.CompilerOptDetails[id]; !ok { + newCompilerOptDetails[id] = unit{} // no change } } - for id, want := range changed.GCDetails { + for id, want := range changed.CompilerOptDetails { if want { - newGCDetails[id] = unit{} + newCompilerOptDetails[id] = unit{} } } - if len(newGCDetails) > 0 { - result.gcOptimizationDetails = newGCDetails + if len(newCompilerOptDetails) > 0 { + result.compilerOptDetails = newCompilerOptDetails } } @@ -2217,10 +2161,10 @@ func (s *Snapshot) setBuiltin(path string) { s.builtin = protocol.URIFromPath(path) } -// WantGCDetails reports whether to compute GC optimization details for the -// specified package. -func (s *Snapshot) WantGCDetails(id metadata.PackageID) bool { - _, ok := s.gcOptimizationDetails[id] +// WantCompilerOptDetails reports whether to compute compiler +// optimization details for the specified package. +func (s *Snapshot) WantCompilerOptDetails(id metadata.PackageID) bool { + _, ok := s.compilerOptDetails[id] return ok } diff --git a/gopls/internal/cache/symbols.go b/gopls/internal/cache/symbols.go index 9954c747798..4ec88a08a84 100644 --- a/gopls/internal/cache/symbols.go +++ b/gopls/internal/cache/symbols.go @@ -6,196 +6,98 @@ package cache import ( "context" - "go/ast" + "crypto/sha256" + "fmt" + "go/parser" "go/token" - "go/types" - "strings" + "runtime" + "golang.org/x/sync/errgroup" + "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/symbols" "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/filecache" "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" ) -// Symbol holds a precomputed symbol value. Note: we avoid using the -// protocol.SymbolInformation struct here in order to reduce the size of each -// symbol. -type Symbol struct { - Name string - Kind protocol.SymbolKind - Range protocol.Range -} - -// symbolize returns the result of symbolizing the file identified by uri, using a cache. -func (s *Snapshot) symbolize(ctx context.Context, uri protocol.DocumentURI) ([]Symbol, error) { - - s.mu.Lock() - entry, hit := s.symbolizeHandles.Get(uri) - s.mu.Unlock() - - type symbolizeResult struct { - symbols []Symbol - err error - } - - // Cache miss? - if !hit { - fh, err := s.ReadFile(ctx, uri) - if err != nil { - return nil, err - } - type symbolHandleKey file.Hash - key := symbolHandleKey(fh.Identity().Hash) - promise, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} { - symbols, err := symbolizeImpl(ctx, arg.(*Snapshot), fh) - return symbolizeResult{symbols, err} - }) - - entry = promise - - s.mu.Lock() - s.symbolizeHandles.Set(uri, entry, func(_, _ interface{}) { release() }) - s.mu.Unlock() - } - - // Await result. - v, err := s.awaitPromise(ctx, entry) - if err != nil { - return nil, err - } - res := v.(symbolizeResult) - return res.symbols, res.err -} - -// symbolizeImpl reads and parses a file and extracts symbols from it. -func symbolizeImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) ([]Symbol, error) { - pgfs, err := snapshot.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full, false, fh) - if err != nil { - return nil, err - } - - w := &symbolWalker{ - tokFile: pgfs[0].Tok, - mapper: pgfs[0].Mapper, - } - w.fileDecls(pgfs[0].File.Decls) +// Symbols extracts and returns symbol information for every file contained in +// a loaded package. It awaits snapshot loading. +// +// If workspaceOnly is set, this only includes symbols from files in a +// workspace package. Otherwise, it returns symbols from all loaded packages. +func (s *Snapshot) Symbols(ctx context.Context, ids ...PackageID) ([]*symbols.Package, error) { + meta := s.MetadataGraph() + + res := make([]*symbols.Package, len(ids)) + var g errgroup.Group + g.SetLimit(runtime.GOMAXPROCS(-1)) // symbolizing is cpu bound + for i, id := range ids { + g.Go(func() error { + mp := meta.Packages[id] + if mp == nil { + return bug.Errorf("missing metadata for %q", id) + } - return w.symbols, w.firstError -} + key, fhs, err := symbolKey(ctx, mp, s) + if err != nil { + return err + } -type symbolWalker struct { - // for computing positions - tokFile *token.File - mapper *protocol.Mapper + if data, err := filecache.Get(symbolsKind, key); err == nil { + res[i] = symbols.Decode(data) + return nil + } else if err != filecache.ErrNotFound { + bug.Reportf("internal error reading symbol data: %v", err) + } - symbols []Symbol - firstError error -} + pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full&^parser.ParseComments, false, fhs...) + if err != nil { + return err + } + pkg := symbols.New(pgfs) -func (w *symbolWalker) atNode(node ast.Node, name string, kind protocol.SymbolKind, path ...*ast.Ident) { - var b strings.Builder - for _, ident := range path { - if ident != nil { - b.WriteString(ident.Name) - b.WriteString(".") - } - } - b.WriteString(name) + // Store the resulting data in the cache. + go func() { + data := pkg.Encode() + if err := filecache.Set(symbolsKind, key, data); err != nil { + event.Error(ctx, fmt.Sprintf("storing symbol data for %s", id), err) + } + }() - rng, err := w.mapper.NodeRange(w.tokFile, node) - if err != nil { - w.error(err) - return - } - sym := Symbol{ - Name: b.String(), - Kind: kind, - Range: rng, + res[i] = pkg + return nil + }) } - w.symbols = append(w.symbols, sym) -} -func (w *symbolWalker) error(err error) { - if err != nil && w.firstError == nil { - w.firstError = err - } + return res, g.Wait() } -func (w *symbolWalker) fileDecls(decls []ast.Decl) { - for _, decl := range decls { - switch decl := decl.(type) { - case *ast.FuncDecl: - kind := protocol.Function - var recv *ast.Ident - if decl.Recv.NumFields() > 0 { - kind = protocol.Method - _, recv, _ = astutil.UnpackRecv(decl.Recv.List[0].Type) - } - w.atNode(decl.Name, decl.Name.Name, kind, recv) - case *ast.GenDecl: - for _, spec := range decl.Specs { - switch spec := spec.(type) { - case *ast.TypeSpec: - kind := guessKind(spec) - w.atNode(spec.Name, spec.Name.Name, kind) - w.walkType(spec.Type, spec.Name) - case *ast.ValueSpec: - for _, name := range spec.Names { - kind := protocol.Variable - if decl.Tok == token.CONST { - kind = protocol.Constant - } - w.atNode(name, name.Name, kind) - } +func symbolKey(ctx context.Context, mp *metadata.Package, fs file.Source) (file.Hash, []file.Handle, error) { + seen := make(map[protocol.DocumentURI]bool) + var fhs []file.Handle + for _, list := range [][]protocol.DocumentURI{mp.GoFiles, mp.CompiledGoFiles} { + for _, uri := range list { + if !seen[uri] { + seen[uri] = true + fh, err := fs.ReadFile(ctx, uri) + if err != nil { + return file.Hash{}, nil, err // context cancelled } + fhs = append(fhs, fh) } } } -} - -func guessKind(spec *ast.TypeSpec) protocol.SymbolKind { - switch spec.Type.(type) { - case *ast.InterfaceType: - return protocol.Interface - case *ast.StructType: - return protocol.Struct - case *ast.FuncType: - return protocol.Function - } - return protocol.Class -} - -// walkType processes symbols related to a type expression. path is path of -// nested type identifiers to the type expression. -func (w *symbolWalker) walkType(typ ast.Expr, path ...*ast.Ident) { - switch st := typ.(type) { - case *ast.StructType: - for _, field := range st.Fields.List { - w.walkField(field, protocol.Field, protocol.Field, path...) - } - case *ast.InterfaceType: - for _, field := range st.Methods.List { - w.walkField(field, protocol.Interface, protocol.Method, path...) - } - } -} -// walkField processes symbols related to the struct field or interface method. -// -// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed -// or named. path is the path of nested identifiers containing the field. -func (w *symbolWalker) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) { - if len(field.Names) == 0 { - switch typ := field.Type.(type) { - case *ast.SelectorExpr: - // embedded qualified type - w.atNode(field, typ.Sel.Name, unnamedKind, path...) - default: - w.atNode(field, types.ExprString(field.Type), unnamedKind, path...) - } - } - for _, name := range field.Names { - w.atNode(name, name.Name, namedKind, path...) - w.walkType(field.Type, append(path, name)...) + hasher := sha256.New() + fmt.Fprintf(hasher, "symbols: %s\n", mp.PkgPath) + fmt.Fprintf(hasher, "files: %d\n", len(fhs)) + for _, fh := range fhs { + fmt.Fprintln(hasher, fh.Identity()) } + var hash file.Hash + hasher.Sum(hash[:0]) + return hash, fhs, nil } diff --git a/gopls/internal/cache/symbols/symbols.go b/gopls/internal/cache/symbols/symbols.go new file mode 100644 index 00000000000..28605368337 --- /dev/null +++ b/gopls/internal/cache/symbols/symbols.go @@ -0,0 +1,186 @@ +// 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 symbols defines the serializable index of package symbols extracted +// from parsed package files. +package symbols + +import ( + "go/ast" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/astutil" + "golang.org/x/tools/gopls/internal/util/frob" +) + +// Symbol holds a precomputed symbol value. This is a subset of the information +// in the full protocol.SymbolInformation struct to reduce the size of each +// symbol. +type Symbol struct { + Name string + Kind protocol.SymbolKind + Range protocol.Range +} + +// A Package holds information about symbols declared by each file of a +// package. +// +// The symbols included are: package-level declarations, and fields and methods +// of type declarations. +type Package struct { + Files []protocol.DocumentURI // package files + Symbols [][]Symbol // symbols in each file +} + +var codec = frob.CodecFor[Package]() + +// Decode decodes data from [Package.Encode]. +func Decode(data []byte) *Package { + var pkg Package + codec.Decode(data, &pkg) + return &pkg +} + +// Encode encodes the package. +func (pkg *Package) Encode() []byte { + return codec.Encode(*pkg) +} + +// New returns a new [Package] summarizing symbols in the given files. +func New(files []*parsego.File) *Package { + var ( + uris []protocol.DocumentURI + symbols [][]Symbol + ) + for _, pgf := range files { + uris = append(uris, pgf.URI) + syms := symbolizeFile(pgf) + symbols = append(symbols, syms) + } + return &Package{ + Files: uris, + Symbols: symbols, + } +} + +// symbolizeFile reads and parses a file and extracts symbols from it. +func symbolizeFile(pgf *parsego.File) []Symbol { + w := &symbolWalker{ + nodeRange: pgf.NodeRange, + } + + for _, decl := range pgf.File.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + kind := protocol.Function + var recv *ast.Ident + if decl.Recv.NumFields() > 0 { + kind = protocol.Method + _, recv, _ = astutil.UnpackRecv(decl.Recv.List[0].Type) + } + w.declare(decl.Name.Name, kind, decl.Name, recv) + + case *ast.GenDecl: + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + kind := protocol.Class + switch spec.Type.(type) { + case *ast.InterfaceType: + kind = protocol.Interface + case *ast.StructType: + kind = protocol.Struct + case *ast.FuncType: + kind = protocol.Function + } + w.declare(spec.Name.Name, kind, spec.Name) + w.walkType(spec.Type, spec.Name) + case *ast.ValueSpec: + for _, name := range spec.Names { + kind := protocol.Variable + if decl.Tok == token.CONST { + kind = protocol.Constant + } + w.declare(name.Name, kind, name) + } + } + } + } + } + + return w.symbols +} + +type symbolWalker struct { + nodeRange func(node ast.Node) (protocol.Range, error) // for computing positions + + symbols []Symbol +} + +// declare declares a symbol of the specified name, kind, node location, and enclosing dotted path of identifiers. +func (w *symbolWalker) declare(name string, kind protocol.SymbolKind, node ast.Node, path ...*ast.Ident) { + var b strings.Builder + for _, ident := range path { + if ident != nil { + b.WriteString(ident.Name) + b.WriteString(".") + } + } + b.WriteString(name) + + rng, err := w.nodeRange(node) + if err != nil { + // TODO(rfindley): establish an invariant that node positions cannot exceed + // the file. This is not currently the case--for example see + // golang/go#48300 (this can also happen due to phantom selectors). + // + // For now, we have nothing to do with this error. + return + } + sym := Symbol{ + Name: b.String(), + Kind: kind, + Range: rng, + } + w.symbols = append(w.symbols, sym) +} + +// walkType processes symbols related to a type expression. path is path of +// nested type identifiers to the type expression. +func (w *symbolWalker) walkType(typ ast.Expr, path ...*ast.Ident) { + switch st := typ.(type) { + case *ast.StructType: + for _, field := range st.Fields.List { + w.walkField(field, protocol.Field, protocol.Field, path...) + } + case *ast.InterfaceType: + for _, field := range st.Methods.List { + w.walkField(field, protocol.Interface, protocol.Method, path...) + } + } +} + +// walkField processes symbols related to the struct field or interface method. +// +// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed +// or named. path is the path of nested identifiers containing the field. +func (w *symbolWalker) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) { + if len(field.Names) == 0 { + switch typ := field.Type.(type) { + case *ast.SelectorExpr: + // embedded qualified type + w.declare(typ.Sel.Name, unnamedKind, field, path...) + default: + w.declare(types.ExprString(field.Type), unnamedKind, field, path...) + } + } + for _, name := range field.Names { + w.declare(name.Name, namedKind, name, path...) + w.walkType(field.Type, append(path, name)...) + } +} diff --git a/gopls/internal/cache/testfuncs/tests.go b/gopls/internal/cache/testfuncs/tests.go index cfc7daab15c..fca25e5db19 100644 --- a/gopls/internal/cache/testfuncs/tests.go +++ b/gopls/internal/cache/testfuncs/tests.go @@ -281,7 +281,7 @@ func testKind(sig *types.Signature) (*types.TypeName, bool) { } named, ok := ptr.Elem().(*types.Named) - if !ok || named.Obj().Pkg().Path() != "testing" { + if !ok || named.Obj().Pkg() == nil || named.Obj().Pkg().Path() != "testing" { return nil, false } diff --git a/gopls/internal/cache/typerefs/packageset.go b/gopls/internal/cache/typerefs/packageset.go index 29c37cd1c4c..f4f7c94f712 100644 --- a/gopls/internal/cache/typerefs/packageset.go +++ b/gopls/internal/cache/typerefs/packageset.go @@ -7,11 +7,11 @@ package typerefs import ( "fmt" "math/bits" - "sort" "strings" "sync" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/util/moremaps" ) // PackageIndex stores common data to enable efficient representation of @@ -123,13 +123,7 @@ func (s *PackageSet) Contains(id metadata.PackageID) bool { // Elems calls f for each element of the set in ascending order. func (s *PackageSet) Elems(f func(IndexID)) { - blockIndexes := make([]int, 0, len(s.sparse)) - for k := range s.sparse { - blockIndexes = append(blockIndexes, k) - } - sort.Ints(blockIndexes) - for _, i := range blockIndexes { - v := s.sparse[i] + for i, v := range moremaps.Sorted(s.sparse) { for b := 0; b < blockSize; b++ { if (v & (1 << b)) != 0 { f(IndexID(i*blockSize + b)) diff --git a/gopls/internal/cache/typerefs/pkggraph_test.go b/gopls/internal/cache/typerefs/pkggraph_test.go index 01cd1a86f0f..20e34ce1aa9 100644 --- a/gopls/internal/cache/typerefs/pkggraph_test.go +++ b/gopls/internal/cache/typerefs/pkggraph_test.go @@ -40,7 +40,7 @@ type Package struct { // transitively reachable through references, starting with the given decl. transitiveRefs map[string]*typerefs.PackageSet - // ReachesViaDeps records the set of packages in the containing graph whose + // ReachesByDeps records the set of packages in the containing graph whose // syntax may affect the current package's types. See the package // documentation for more details of what this means. ReachesByDeps *typerefs.PackageSet diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go index d2adc5de019..5fb03cb1152 100644 --- a/gopls/internal/cache/view.go +++ b/gopls/internal/cache/view.go @@ -736,11 +736,11 @@ func (s *Snapshot) initialize(ctx context.Context, firstAttempt bool) { // By far the most common of these is a change to file state, but a query of // module upgrade information or vulnerabilities also affects gopls' behavior. type StateChange struct { - Modifications []file.Modification // if set, the raw modifications originating this change - Files map[protocol.DocumentURI]file.Handle - ModuleUpgrades map[protocol.DocumentURI]map[string]string - Vulns map[protocol.DocumentURI]*vulncheck.Result - GCDetails map[metadata.PackageID]bool // package -> whether or not we want details + Modifications []file.Modification // if set, the raw modifications originating this change + Files map[protocol.DocumentURI]file.Handle + ModuleUpgrades map[protocol.DocumentURI]map[string]string + Vulns map[protocol.DocumentURI]*vulncheck.Result + CompilerOptDetails map[metadata.PackageID]bool // package -> whether or not we want details } // InvalidateView processes the provided state change, invalidating any derived @@ -809,9 +809,10 @@ func (s *Session) invalidateViewLocked(ctx context.Context, v *View, changed Sta // If forURI is non-empty, this view should be the best view including forURI. // Otherwise, it is the default view for the folder. // -// defineView only returns an error in the event of context cancellation. +// defineView may return an error if the context is cancelled, or the +// workspace folder path is invalid. // -// Note: keep this function in sync with bestView. +// Note: keep this function in sync with [RelevantViews]. // // TODO(rfindley): we should be able to remove the error return, as // findModules is going away, and all other I/O is memoized. @@ -838,11 +839,11 @@ func defineView(ctx context.Context, fs file.Source, folder *Folder, forFile fil // add those constraints to the viewDefinition's environment. // Content trimming is nontrivial, so do this outside of the loop below. - // Keep this in sync with bestView. + // Keep this in sync with [RelevantViews]. path := forFile.URI().Path() if content, err := forFile.Content(); err == nil { // Note the err == nil condition above: by convention a non-existent file - // does not have any constraints. See the related note in bestView: this + // does not have any constraints. See the related note in [RelevantViews]: this // choice of behavior shouldn't actually matter. In this case, we should // only call defineView with Overlays, which always have content. content = trimContentForPortMatch(content) diff --git a/gopls/internal/cmd/codelens.go b/gopls/internal/cmd/codelens.go index 452e978094f..75db4d04843 100644 --- a/gopls/internal/cmd/codelens.go +++ b/gopls/internal/cmd/codelens.go @@ -42,10 +42,10 @@ is executed, and its output is printed to stdout. Example: - $ gopls codelens a_test.go # list code lenses in a file - $ gopls codelens a_test.go:10 # list code lenses on line 10 - $ gopls codelens a_test.go gopls.test # list gopls.test commands - $ gopls codelens -run a_test.go:10 gopls.test # run a specific test + $ gopls codelens a_test.go # list code lenses in a file + $ gopls codelens a_test.go:10 # list code lenses on line 10 + $ gopls codelens a_test.go gopls.test # list gopls.test commands + $ gopls codelens -exec a_test.go:10 gopls.test # run a specific test codelens-flags: `) diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index ad08119d397..d819279d699 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -994,6 +994,8 @@ type C struct{} res.checkExit(true) got := res.stdout want := `command "Browse documentation for package a" [source.doc]` + + "\n" + + `command "Toggle compiler optimization details" [source.toggleCompilerOptDetails]` + "\n" if got != want { t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) diff --git a/gopls/internal/cmd/usage/codelens.hlp b/gopls/internal/cmd/usage/codelens.hlp index 5766d7fd189..59afe0d3a27 100644 --- a/gopls/internal/cmd/usage/codelens.hlp +++ b/gopls/internal/cmd/usage/codelens.hlp @@ -17,10 +17,10 @@ is executed, and its output is printed to stdout. Example: - $ gopls codelens a_test.go # list code lenses in a file - $ gopls codelens a_test.go:10 # list code lenses on line 10 - $ gopls codelens a_test.go gopls.test # list gopls.test commands - $ gopls codelens -run a_test.go:10 gopls.test # run a specific test + $ gopls codelens a_test.go # list code lenses in a file + $ gopls codelens a_test.go:10 # list code lenses on line 10 + $ gopls codelens a_test.go gopls.test # list gopls.test commands + $ gopls codelens -exec a_test.go:10 gopls.test # run a specific test codelens-flags: -d,-diff diff --git a/gopls/internal/debug/template_test.go b/gopls/internal/debug/template_test.go index db940efc602..d4d9071c140 100644 --- a/gopls/internal/debug/template_test.go +++ b/gopls/internal/debug/template_test.go @@ -15,7 +15,6 @@ import ( "html/template" "os" "runtime" - "sort" "strings" "testing" @@ -24,6 +23,7 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/testenv" ) @@ -110,13 +110,7 @@ func TestTemplates(t *testing.T) { } } // now check all the known templates, in alphabetic order, for determinacy - keys := []string{} - for k := range templates { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - v := templates[k] + for k, v := range moremaps.Sorted(templates) { // the FuncMap is an annoyance; should not be necessary if err := templatecheck.CheckHTML(v.tmpl, v.data); err != nil { t.Errorf("%s: %v", k, err) diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index b64965ab863..982ec34909b 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -95,7 +95,7 @@ { "Name": "hoverKind", "Type": "enum", - "Doc": "hoverKind controls the information that appears in the hover text.\nSingleLine and Structured are intended for use only by authors of editor plugins.\n", + "Doc": "hoverKind controls the information that appears in the hover text.\nSingleLine is intended for use only by authors of editor plugins.\n", "EnumKeys": { "ValueType": "", "Keys": null @@ -113,10 +113,6 @@ "Value": "\"SingleLine\"", "Doc": "" }, - { - "Value": "\"Structured\"", - "Doc": "`\"Structured\"` is an experimental setting that returns a structured hover format.\nThis format separates the signature from the documentation, so that the client\ncan do more manipulation of these fields.\n\nThis should only be used by clients that support this behavior.\n" - }, { "Value": "\"SynopsisDocumentation\"", "Doc": "" @@ -469,6 +465,11 @@ "Doc": "check cancel func returned by context.WithCancel is called\n\nThe cancellation function returned by context.WithCancel, WithTimeout,\nWithDeadline and variants such as WithCancelCause must be called,\nor the new context will remain live until its parent context is cancelled.\n(The background context is never cancelled.)", "Default": "true" }, + { + "Name": "\"modernize\"", + "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;", + "Default": "true" + }, { "Name": "\"nilfunc\"", "Doc": "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.", @@ -584,6 +585,11 @@ "Doc": "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.", "Default": "true" }, + { + "Name": "\"unusedfunc\"", + "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report a false positive for a declaration of an\nunexported function that is referenced from another package using\nthe go:linkname mechanism, if the declaration's doc comment does\nnot also have a go:linkname comment. (Such code is in any case\nstrongly discouraged: linkname annotations, if they must be used at\nall, should be used on both the declaration and the alias.)\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.", + "Default": "true" + }, { "Name": "\"unusedparams\"", "Doc": "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.", @@ -604,11 +610,6 @@ "Doc": "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}", "Default": "true" }, - { - "Name": "\"useany\"", - "Doc": "check for constraints that could be simplified to \"any\"", - "Default": "false" - }, { "Name": "\"waitgroup\"", "Doc": "check for misuses of sync.WaitGroup\n\nThis analyzer detects mistaken calls to the (*sync.WaitGroup).Add\nmethod from inside a new goroutine, causing Add to race with Wait:\n\n\t// WRONG\n\tvar wg sync.WaitGroup\n\tgo func() {\n\t wg.Add(1) // \"WaitGroup.Add called from inside new goroutine\"\n\t defer wg.Done()\n\t ...\n\t}()\n\twg.Wait() // (may return prematurely before new goroutine starts)\n\nThe correct code calls Add before starting the goroutine:\n\n\t// RIGHT\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t...\n\t}()\n\twg.Wait()", @@ -639,40 +640,6 @@ "Status": "experimental", "Hierarchy": "ui.diagnostic" }, - { - "Name": "annotations", - "Type": "map[enum]bool", - "Doc": "annotations specifies the various kinds of optimization diagnostics\nthat should be reported by the gc_details command.\n", - "EnumKeys": { - "ValueType": "bool", - "Keys": [ - { - "Name": "\"bounds\"", - "Doc": "`\"bounds\"` controls bounds checking diagnostics.\n", - "Default": "true" - }, - { - "Name": "\"escape\"", - "Doc": "`\"escape\"` controls diagnostics about escape choices.\n", - "Default": "true" - }, - { - "Name": "\"inline\"", - "Doc": "`\"inline\"` controls diagnostics about inlining choices.\n", - "Default": "true" - }, - { - "Name": "\"nil\"", - "Doc": "`\"nil\"` controls nil checks.\n", - "Default": "true" - } - ] - }, - "EnumValues": null, - "Default": "{\"bounds\":true,\"escape\":true,\"inline\":true,\"nil\":true}", - "Status": "experimental", - "Hierarchy": "ui.diagnostic" - }, { "Name": "vulncheck", "Type": "enum", @@ -795,15 +762,10 @@ { "Name": "codelenses", "Type": "map[enum]bool", - "Doc": "codelenses overrides the enabled/disabled state of each of gopls'\nsources of [Code Lenses](codelenses.md).\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"codelenses\": {\n \"generate\": false, // Don't show the `go generate` lens.\n \"gc_details\": true // Show a code lens toggling the display of gc's choices.\n }\n...\n}\n```\n", + "Doc": "codelenses overrides the enabled/disabled state of each of gopls'\nsources of [Code Lenses](codelenses.md).\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"codelenses\": {\n \"generate\": false, // Don't show the `go generate` lens.\n }\n...\n}\n```\n", "EnumKeys": { "ValueType": "bool", "Keys": [ - { - "Name": "\"gc_details\"", - "Doc": "`\"gc_details\"`: Toggle display of Go compiler optimization decisions\n\nThis codelens source causes the `package` declaration of\neach file to be annotated with a command to toggle the\nstate of the per-session variable that controls whether\noptimization decisions from the Go compiler (formerly known\nas \"gc\") should be displayed as diagnostics.\n\nOptimization decisions include:\n- whether a variable escapes, and how escape is inferred;\n- whether a nil-pointer check is implied or eliminated;\n- whether a function can be inlined.\n\nTODO(adonovan): this source is off by default because the\nannotation is annoying and because VS Code has a separate\n\"Toggle gc details\" command. Replace it with a Code Action\n(\"Source action...\").\n", - "Default": "false" - }, { "Name": "\"generate\"", "Doc": "`\"generate\"`: Run `go generate`\n\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n", @@ -847,7 +809,7 @@ ] }, "EnumValues": null, - "Default": "{\"gc_details\":false,\"generate\":true,\"regenerate_cgo\":true,\"run_govulncheck\":false,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}", + "Default": "{\"generate\":true,\"regenerate_cgo\":true,\"run_govulncheck\":false,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}", "Status": "", "Hierarchy": "ui" }, @@ -932,13 +894,6 @@ ] }, "Lenses": [ - { - "FileType": "Go", - "Lens": "gc_details", - "Title": "Toggle display of Go compiler optimization decisions", - "Doc": "\nThis codelens source causes the `package` declaration of\neach file to be annotated with a command to toggle the\nstate of the per-session variable that controls whether\noptimization decisions from the Go compiler (formerly known\nas \"gc\") should be displayed as diagnostics.\n\nOptimization decisions include:\n- whether a variable escapes, and how escape is inferred;\n- whether a nil-pointer check is implied or eliminated;\n- whether a function can be inlined.\n\nTODO(adonovan): this source is off by default because the\nannotation is annoying and because VS Code has a separate\n\"Toggle gc details\" command. Replace it with a Code Action\n(\"Source action...\").\n", - "Default": false - }, { "FileType": "Go", "Lens": "generate", @@ -1135,6 +1090,12 @@ "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/lostcancel", "Default": true }, + { + "Name": "modernize", + "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;", + "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize", + "Default": true + }, { "Name": "nilfunc", "Doc": "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.", @@ -1273,6 +1234,12 @@ "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr", "Default": true }, + { + "Name": "unusedfunc", + "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report a false positive for a declaration of an\nunexported function that is referenced from another package using\nthe go:linkname mechanism, if the declaration's doc comment does\nnot also have a go:linkname comment. (Such code is in any case\nstrongly discouraged: linkname annotations, if they must be used at\nall, should be used on both the declaration and the alias.)\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.", + "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedfunc", + "Default": true + }, { "Name": "unusedparams", "Doc": "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.", @@ -1297,12 +1264,6 @@ "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedwrite", "Default": true }, - { - "Name": "useany", - "Doc": "check for constraints that could be simplified to \"any\"", - "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany", - "Default": false - }, { "Name": "waitgroup", "Doc": "check for misuses of sync.WaitGroup\n\nThis analyzer detects mistaken calls to the (*sync.WaitGroup).Add\nmethod from inside a new goroutine, causing Add to race with Wait:\n\n\t// WRONG\n\tvar wg sync.WaitGroup\n\tgo func() {\n\t wg.Add(1) // \"WaitGroup.Add called from inside new goroutine\"\n\t defer wg.Done()\n\t ...\n\t}()\n\twg.Wait() // (may return prematurely before new goroutine starts)\n\nThe correct code calls Add before starting the goroutine:\n\n\t// RIGHT\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t...\n\t}()\n\twg.Wait()", diff --git a/gopls/internal/filecache/filecache.go b/gopls/internal/filecache/filecache.go index 243e9547128..c5edf340bc5 100644 --- a/gopls/internal/filecache/filecache.go +++ b/gopls/internal/filecache/filecache.go @@ -62,7 +62,12 @@ type memKey struct { // Get retrieves from the cache and returns the value most recently // supplied to Set(kind, key), possibly by another process. -// Get returns ErrNotFound if the value was not found. +// +// Get returns ErrNotFound if the value was not found. The first call +// to Get may fail due to ENOSPC or deletion of the process's +// executable. Other causes of failure include deletion or corruption +// of the cache (by external meddling) while gopls is running, or +// faulty hardware; see issue #67433. // // Callers should not modify the returned array. func Get(kind string, key [32]byte) ([]byte, error) { @@ -79,6 +84,8 @@ func Get(kind string, key [32]byte) ([]byte, error) { // Read the index file, which provides the name of the CAS file. indexName, err := filename(kind, key) if err != nil { + // e.g. ENOSPC, deletion of executable (first time only); + // deletion of cache (at any time). return nil, err } indexData, err := os.ReadFile(indexName) @@ -100,7 +107,7 @@ func Get(kind string, key [32]byte) ([]byte, error) { // engineered hash collision, which is infeasible. casName, err := filename(casKind, valueHash) if err != nil { - return nil, err + return nil, err // see above for possible causes } value, _ := os.ReadFile(casName) // ignore error if sha256.Sum256(value) != valueHash { @@ -138,6 +145,13 @@ func Get(kind string, key [32]byte) ([]byte, error) { var ErrNotFound = fmt.Errorf("not found") // Set updates the value in the cache. +// +// Set may fail due to: +// - failure to access/create the cache (first call only); +// - out of space (ENOSPC); +// - deletion of the cache concurrent with a call to Set; +// - faulty hardware. +// See issue #67433. func Set(kind string, key [32]byte, value []byte) error { memCache.Set(memKey{kind, key}, value, len(value)) @@ -353,13 +367,13 @@ func getCacheDir() (string, error) { // Compute the hash of this executable (~20ms) and create a subdirectory. hash, err := hashExecutable() if err != nil { - cacheDirErr = fmt.Errorf("can't hash gopls executable: %v", err) + cacheDirErr = fmt.Errorf("can't hash gopls executable: %w", err) } // Use only 32 bits of the digest to avoid unwieldy filenames. // It's not an adversarial situation. cacheDir = filepath.Join(goplsDir, fmt.Sprintf("%x", hash[:4])) if err := os.MkdirAll(cacheDir, 0700); err != nil { - cacheDirErr = fmt.Errorf("can't create cache: %v", err) + cacheDirErr = fmt.Errorf("can't create cache: %w", err) } }) return cacheDir, cacheDirErr diff --git a/gopls/internal/fuzzy/input_test.go b/gopls/internal/fuzzy/input_test.go index 4f3239372aa..ffe147241b6 100644 --- a/gopls/internal/fuzzy/input_test.go +++ b/gopls/internal/fuzzy/input_test.go @@ -6,6 +6,7 @@ package fuzzy_test import ( "bytes" + "slices" "sort" "testing" @@ -83,17 +84,9 @@ func TestWordSplit(t *testing.T) { } func diffStringLists(a, b []string) bool { - if len(a) != len(b) { - return false - } sort.Strings(a) sort.Strings(b) - for i := range a { - if a[i] != b[i] { - return false - } - } - return true + return slices.Equal(a, b) } var lastSegmentSplitTests = []struct { diff --git a/gopls/internal/golang/addtest.go b/gopls/internal/golang/addtest.go index 8228faf0fc8..4a43a82ffee 100644 --- a/gopls/internal/golang/addtest.go +++ b/gopls/internal/golang/addtest.go @@ -17,7 +17,6 @@ import ( "go/types" "os" "path/filepath" - "sort" "strconv" "strings" "text/template" @@ -29,6 +28,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typesinternal" ) @@ -418,13 +418,13 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. } } - // qf qualifier determines the correct package name to use for a type in + // qual qualifier determines the correct package name to use for a type in // foo_test.go. It does this by: // - Consult imports map from test file foo_test.go. // - If not found, consult imports map from original file foo.go. // If the package is not imported in test file foo_test.go, it is added to // extraImports map. - qf := func(p *types.Package) string { + qual := func(p *types.Package) string { // References from an in-package test should not be qualified. if !xtest && p == pkg.Types() { return "" @@ -472,8 +472,8 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. } data := testInfo{ - TestingPackageName: qf(types.NewPackage("testing", "testing")), - PackageName: qf(pkg.Types()), + TestingPackageName: qual(types.NewPackage("testing", "testing")), + PackageName: qual(pkg.Types()), TestFuncName: testName, Func: function{ Name: fn.Name(), @@ -493,11 +493,11 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. for i := range sig.Params().Len() { param := sig.Params().At(i) name, typ := param.Name(), param.Type() - f := field{Type: types.TypeString(typ, qf)} + f := field{Type: types.TypeString(typ, qual)} if i == 0 && isContextType(typ) { - f.Value = qf(types.NewPackage("context", "context")) + ".Background()" + f.Value = qual(types.NewPackage("context", "context")) + ".Background()" } else if name == "" || name == "_" { - f.Value = typesinternal.ZeroString(typ, qf) + f.Value, _ = typesinternal.ZeroString(typ, qual) } else { f.Name = name } @@ -516,7 +516,7 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. } data.Func.Results = append(data.Func.Results, field{ Name: name, - Type: types.TypeString(typ, qf), + Type: types.TypeString(typ, qual), }) } @@ -575,7 +575,7 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. data.Receiver = &receiver{ Var: field{ Name: varName, - Type: types.TypeString(recvType, qf), + Type: types.TypeString(recvType, qual), }, } @@ -627,11 +627,11 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. for i := range constructor.Signature().Params().Len() { param := constructor.Signature().Params().At(i) name, typ := param.Name(), param.Type() - f := field{Type: types.TypeString(typ, qf)} + f := field{Type: types.TypeString(typ, qual)} if i == 0 && isContextType(typ) { - f.Value = qf(types.NewPackage("context", "context")) + ".Background()" + f.Value = qual(types.NewPackage("context", "context")) + ".Background()" } else if name == "" || name == "_" { - f.Value = typesinternal.ZeroString(typ, qf) + f.Value, _ = typesinternal.ZeroString(typ, qual) } else { f.Name = name } @@ -653,7 +653,7 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. } data.Receiver.Constructor.Results = append(data.Receiver.Constructor.Results, field{ Name: name, - Type: types.TypeString(typ, qf), + Type: types.TypeString(typ, qual), }) } } @@ -727,15 +727,10 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. } } else { importsBuffer.WriteString("\nimport(") - // Loop over the map in sorted order ensures deterministic outcome. - paths := make([]string, 0, len(extraImports)) - for key := range extraImports { - paths = append(paths, key) - } - sort.Strings(paths) - for _, path := range paths { + // Sort for determinism. + for path, name := range moremaps.Sorted(extraImports) { importsBuffer.WriteString("\n\t") - if name := extraImports[path]; name != "" { + if name != "" { importsBuffer.WriteString(name + " ") } importsBuffer.WriteString(fmt.Sprintf("\"%s\"", path)) diff --git a/gopls/internal/golang/change_signature.go b/gopls/internal/golang/change_signature.go index 8157c6d03fb..e9fc099399d 100644 --- a/gopls/internal/golang/change_signature.go +++ b/gopls/internal/golang/change_signature.go @@ -755,10 +755,6 @@ func reTypeCheck(logf func(string, ...any), orig *cache.Package, fileMask map[pr // TODO(golang/go#63472): this looks wrong with the new Go version syntax. var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) -func remove[T any](s []T, i int) []T { - return append(s[:i], s[i+1:]...) -} - // selectElements returns a new array of elements of s indicated by the // provided list of indices. It returns false if any index was out of bounds. // diff --git a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go index f0a5500b57f..1359d0d0148 100644 --- a/gopls/internal/golang/code_lens.go +++ b/gopls/internal/golang/code_lens.go @@ -23,10 +23,9 @@ import ( // CodeLensSources returns the supported sources of code lenses for Go files. func CodeLensSources() map[settings.CodeLensSource]cache.CodeLensSourceFunc { return map[settings.CodeLensSource]cache.CodeLensSourceFunc{ - settings.CodeLensGenerate: goGenerateCodeLens, // commands: Generate - settings.CodeLensTest: runTestCodeLens, // commands: Test - settings.CodeLensRegenerateCgo: regenerateCgoLens, // commands: RegenerateCgo - settings.CodeLensGCDetails: toggleDetailsCodeLens, // commands: GCDetails + settings.CodeLensGenerate: goGenerateCodeLens, // commands: Generate + settings.CodeLensTest: runTestCodeLens, // commands: Test + settings.CodeLensRegenerateCgo: regenerateCgoLens, // commands: RegenerateCgo } } @@ -196,21 +195,3 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha 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) { - pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) - if err != nil { - return nil, err - } - if !pgf.File.Package.IsValid() { - // Without a package name we have nowhere to put the codelens, so give up. - return nil, nil - } - rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package) - if err != nil { - return nil, err - } - puri := fh.URI() - 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 0a778ba758b..627ba1a60d6 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -27,7 +27,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/typesutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typesinternal" @@ -173,7 +172,7 @@ func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command, allowReso req.addAction(act) } -// addCommandAction adds an edit-based CodeAction to the result. +// addEditAction 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, @@ -232,12 +231,15 @@ var codeActionProducers = [...]codeActionProducer{ {kind: settings.GoDoc, fn: goDoc, needPkg: true}, {kind: settings.GoFreeSymbols, fn: goFreeSymbols}, {kind: settings.GoTest, fn: goTest}, + {kind: settings.GoToggleCompilerOptDetails, fn: toggleCompilerOptDetails}, {kind: settings.GoplsDocFeatures, fn: goplsDocFeatures}, {kind: settings.RefactorExtractFunction, fn: refactorExtractFunction}, {kind: settings.RefactorExtractMethod, fn: refactorExtractMethod}, {kind: settings.RefactorExtractToNewFile, fn: refactorExtractToNewFile}, {kind: settings.RefactorExtractConstant, fn: refactorExtractVariable, needPkg: true}, {kind: settings.RefactorExtractVariable, fn: refactorExtractVariable, needPkg: true}, + {kind: settings.RefactorExtractConstantAll, fn: refactorExtractVariableAll, needPkg: true}, + {kind: settings.RefactorExtractVariableAll, fn: refactorExtractVariableAll, needPkg: true}, {kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true}, {kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote}, {kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true}, @@ -316,8 +318,8 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error { path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end) si := stubmethods.GetIfaceStubInfo(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) + qual := typesinternal.FileQualifier(req.pgf.File, si.Concrete.Obj().Pkg()) + iface := types.TypeString(si.Interface.Type(), qual) msg := fmt.Sprintf("Declare missing methods of %s", iface) req.addApplyFixAction(msg, fixMissingInterfaceMethods, req.loc) } @@ -440,8 +442,10 @@ func goplsDocFeatures(ctx context.Context, req *codeActionsRequest) error { // 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) + if title != "" { + cmd := command.NewDocCommand(title, command.DocArgs{Location: req.loc, ShowDocument: true}) + req.addCommandAction(cmd, false) + } return nil } @@ -467,14 +471,15 @@ func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error { // See [extractVariable] for command implementation. func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error { info := req.pkg.TypesInfo() - if expr, _, err := canExtractVariable(info, req.pgf.File, req.start, req.end); err == nil { + if exprs, err := canExtractVariable(info, req.pgf.File, req.start, req.end, false); err == nil { // Offer one of refactor.extract.{constant,variable} // based on the constness of the expression; this is a // limitation of the codeActionProducers mechanism. // Beware that future evolutions of the refactorings // may make them diverge to become non-complementary, // for example because "if const x = ...; y {" is illegal. - constant := info.Types[expr].Value != nil + // Same as [refactorExtractVariableAll]. + constant := info.Types[exprs[0]].Value != nil if (req.kind == settings.RefactorExtractConstant) == constant { title := "Extract variable" if constant { @@ -486,6 +491,35 @@ func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error return nil } +// refactorExtractVariableAll produces "Extract N occurrences of EXPR" code action. +// See [extractAllOccursOfExpr] for command implementation. +func refactorExtractVariableAll(ctx context.Context, req *codeActionsRequest) error { + info := req.pkg.TypesInfo() + // Don't suggest if only one expr is found, + // otherwise it will duplicate with [refactorExtractVariable] + if exprs, err := canExtractVariable(info, req.pgf.File, req.start, req.end, true); err == nil && len(exprs) > 1 { + start, end, err := req.pgf.NodeOffsets(exprs[0]) + if err != nil { + return err + } + desc := string(req.pgf.Src[start:end]) + if len(desc) >= 40 || strings.Contains(desc, "\n") { + desc = astutil.NodeDescription(exprs[0]) + } + constant := info.Types[exprs[0]].Value != nil + if (req.kind == settings.RefactorExtractConstantAll) == constant { + var title string + if constant { + title = fmt.Sprintf("Extract %d occurrences of const expression: %s", len(exprs), desc) + } else { + title = fmt.Sprintf("Extract %d occurrences of %s", len(exprs), desc) + } + req.addApplyFixAction(title, fixExtractVariableAll, req.loc) + } + } + return nil +} + // refactorExtractToNewFile produces "Extract declarations to new file" code actions. // See [server.commandHandler.ExtractToNewFile] for command implementation. func refactorExtractToNewFile(ctx context.Context, req *codeActionsRequest) error { @@ -617,7 +651,7 @@ func refactorRewriteChangeQuote(ctx context.Context, req *codeActionsRequest) er return nil } -// refactorRewriteChangeQuote produces "Invert 'if' condition" code actions. +// refactorRewriteInvertIf produces "Invert 'if' condition" code actions. // See [invertIfCondition] for command implementation. func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error { if _, ok, _ := canInvertIfCondition(req.pgf.File, req.start, req.end); ok { @@ -815,12 +849,12 @@ func goAssembly(ctx context.Context, req *codeActionsRequest) error { sym.WriteString(".") if sig.Recv() != nil { if isPtr, named := typesinternal.ReceiverNamed(sig.Recv()); named != nil { - sym.WriteString("(") if isPtr { - sym.WriteString("*") + fmt.Fprintf(&sym, "(*%s)", named.Obj().Name()) + } else { + sym.WriteString(named.Obj().Name()) } - sym.WriteString(named.Obj().Name()) - sym.WriteString(").") + sym.WriteByte('.') } } sym.WriteString(fn.Name()) @@ -840,3 +874,11 @@ func goAssembly(ctx context.Context, req *codeActionsRequest) error { } return nil } + +// toggleCompilerOptDetails produces "Toggle compiler optimization details" code action. +// See [server.commandHandler.ToggleCompilerOptDetails] for command implementation. +func toggleCompilerOptDetails(ctx context.Context, req *codeActionsRequest) error { + cmd := command.NewGCDetailsCommand("Toggle compiler optimization details", req.fh.URI()) + req.addCommandAction(cmd, false) + return nil +} diff --git a/gopls/internal/golang/comment.go b/gopls/internal/golang/comment.go index b7ff45037d8..9a360ce2e2b 100644 --- a/gopls/internal/golang/comment.go +++ b/gopls/internal/golang/comment.go @@ -12,6 +12,7 @@ import ( "go/doc/comment" "go/token" "go/types" + pathpkg "path" "slices" "strings" @@ -230,15 +231,15 @@ func lookupDocLinkSymbol(pkg *cache.Package, pgf *parsego.File, name string) typ // // [doc comment]: https://go.dev/doc/comment func newDocCommentParser(pkg *cache.Package) func(fileNode ast.Node, text string) *comment.Doc { - var currentFileNode ast.Node // node whose enclosing file's import mapping should be used + var currentFilePos token.Pos // pos whose enclosing file's import mapping should be used parser := &comment.Parser{ LookupPackage: func(name string) (importPath string, ok bool) { for _, f := range pkg.Syntax() { // Different files in the same package have // different import mappings. Use the provided // syntax node to find the correct file. - if astutil.NodeContains(f, currentFileNode.Pos()) { - // First try the actual imported package name. + if astutil.NodeContains(f, currentFilePos) { + // First try each actual imported package name. for _, imp := range f.Imports { pkgName := pkg.TypesInfo().PkgNameOf(imp) if pkgName != nil && pkgName.Name() == name { @@ -246,8 +247,8 @@ func newDocCommentParser(pkg *cache.Package) func(fileNode ast.Node, text string } } - // Then try the imported package name, as some - // packages are typically imported under a + // Then try each imported package's declared name, + // as some packages are typically imported under a // non-default name (e.g. pathpkg "path") but // may be referred to in doc links using their // canonical name. @@ -258,6 +259,21 @@ func newDocCommentParser(pkg *cache.Package) func(fileNode ast.Node, text string } } + // Finally try matching the last segment of each import + // path imported by any file in the package, as the + // doc comment may appear in a different file from the + // import. + // + // Ideally we would look up the DepsByPkgPath value + // (a PackageID) in the metadata graph and use the + // package's declared name instead of this heuristic, + // but we don't have access to the graph here. + for path := range pkg.Metadata().DepsByPkgPath { + if pathpkg.Base(trimVersionSuffix(string(path))) == name { + return string(path), true + } + } + break } } @@ -279,7 +295,7 @@ func newDocCommentParser(pkg *cache.Package) func(fileNode ast.Node, text string }, } return func(fileNode ast.Node, text string) *comment.Doc { - currentFileNode = fileNode + currentFilePos = fileNode.Pos() return parser.Parse(text) } } diff --git a/gopls/internal/golang/gc_annotations.go b/gopls/internal/golang/compileropt.go similarity index 59% rename from gopls/internal/golang/gc_annotations.go rename to gopls/internal/golang/compileropt.go index 618216f6306..2a39a5b5ee1 100644 --- a/gopls/internal/golang/gc_annotations.go +++ b/gopls/internal/golang/compileropt.go @@ -16,19 +16,13 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/internal/event" ) -// GCOptimizationDetails invokes the Go compiler on the specified -// package and reports its log of optimizations decisions as a set of -// diagnostics. -// -// TODO(adonovan): this feature needs more consistent and informative naming. -// Now that the compiler is cmd/compile, "GC" now means only "garbage collection". -// I propose "(Toggle|Display) Go compiler optimization details" in the UI, -// and CompilerOptimizationDetails for this function and compileropts.go for the file. -func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.Package) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +// CompilerOptDetails invokes the Go compiler with the "-json=0,dir" +// flag on the specified package, parses its log of optimization +// decisions, and returns them as a set of diagnostics. +func CompilerOptDetails(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.Package) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { if len(mp.CompiledGoFiles) == 0 { return nil, nil } @@ -39,7 +33,7 @@ func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *me } defer func() { if err := os.RemoveAll(outDir); err != nil { - event.Error(ctx, "cleaning gcdetails dir", err) + event.Error(ctx, "cleaning details dir", err) } }() @@ -51,13 +45,13 @@ func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *me defer os.Remove(tmpFile.Name()) outDirURI := protocol.URIFromPath(outDir) - // GC details doesn't handle Windows URIs in the form of "file:///C:/...", + // details doesn't handle Windows URIs in the form of "file:///C:/...", // so rewrite them to "file://C:/...". See golang/go#41614. if !strings.HasPrefix(outDir, "/") { outDirURI = protocol.DocumentURI(strings.Replace(string(outDirURI), "file:///", "file://", 1)) } inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NoNetwork, pkgDir, "build", []string{ - fmt.Sprintf("-gcflags=-json=0,%s", outDirURI), + fmt.Sprintf("-gcflags=-json=0,%s", outDirURI), // JSON schema version 0 fmt.Sprintf("-o=%s", tmpFile.Name()), ".", }) @@ -74,10 +68,9 @@ func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *me return nil, err } reports := make(map[protocol.DocumentURI][]*cache.Diagnostic) - opts := snapshot.Options() var parseError error for _, fn := range files { - uri, diagnostics, err := parseDetailsFile(fn, opts) + uri, diagnostics, err := parseDetailsFile(fn) if err != nil { // expect errors for all the files, save 1 parseError = err @@ -97,7 +90,8 @@ func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *me return reports, parseError } -func parseDetailsFile(filename string, options *settings.Options) (protocol.DocumentURI, []*cache.Diagnostic, error) { +// parseDetailsFile parses the file written by the Go compiler which contains a JSON-encoded protocol.Diagnostic. +func parseDetailsFile(filename string) (protocol.DocumentURI, []*cache.Diagnostic, error) { buf, err := os.ReadFile(filename) if err != nil { return "", nil, err @@ -128,21 +122,51 @@ func parseDetailsFile(filename string, options *settings.Options) (protocol.Docu if err := dec.Decode(d); err != nil { return "", nil, err } + if d.Source != "go compiler" { + continue + } d.Tags = []protocol.DiagnosticTag{} // must be an actual slice msg := d.Code.(string) if msg != "" { + // Typical message prefixes gathered by grepping the source of + // cmd/compile for literal arguments in calls to logopt.LogOpt. + // (It is not a well defined set.) + // + // - canInlineFunction + // - cannotInlineCall + // - cannotInlineFunction + // - copy + // - escape + // - escapes + // - isInBounds + // - isSliceInBounds + // - iteration-variable-to-{heap,stack} + // - leak + // - loop-modified-{range,for} + // - nilcheck msg = fmt.Sprintf("%s(%s)", msg, d.Message) } - if !showDiagnostic(msg, d.Source, options) { - continue + + // zeroIndexedRange subtracts 1 from the line and + // range, because the compiler output neglects to + // convert from 1-based UTF-8 coordinates to 0-based UTF-16. + // (See GOROOT/src/cmd/compile/internal/logopt/log_opts.go.) + // TODO(rfindley): also translate UTF-8 to UTF-16. + zeroIndexedRange := func(rng protocol.Range) protocol.Range { + return protocol.Range{ + Start: protocol.Position{ + Line: rng.Start.Line - 1, + Character: rng.Start.Character - 1, + }, + End: protocol.Position{ + Line: rng.End.Line - 1, + Character: rng.End.Character - 1, + }, + } } + var related []protocol.DiagnosticRelatedInformation for _, ri := range d.RelatedInformation { - // TODO(rfindley): The compiler uses LSP-like JSON to encode gc details, - // however the positions it uses are 1-based UTF-8: - // https://github.com/golang/go/blob/master/src/cmd/compile/internal/logopt/log_opts.go - // - // Here, we adjust for 0-based positions, but do not translate UTF-8 to UTF-16. related = append(related, protocol.DiagnosticRelatedInformation{ Location: protocol.Location{ URI: ri.Location.URI, @@ -156,7 +180,7 @@ func parseDetailsFile(filename string, options *settings.Options) (protocol.Docu Range: zeroIndexedRange(d.Range), Message: msg, Severity: d.Severity, - Source: cache.OptimizationDetailsError, // d.Source is always "go compiler" as of 1.16, use our own + Source: cache.CompilerOptDetailsInfo, // d.Source is always "go compiler" as of 1.16, use our own Tags: d.Tags, Related: related, } @@ -166,45 +190,6 @@ func parseDetailsFile(filename string, options *settings.Options) (protocol.Docu return uri, diagnostics, nil } -// showDiagnostic reports whether a given diagnostic should be shown to the end -// user, given the current options. -func showDiagnostic(msg, source string, o *settings.Options) bool { - if source != "go compiler" { - return false - } - if o.Annotations == nil { - return true - } - switch { - case strings.HasPrefix(msg, "canInline") || - strings.HasPrefix(msg, "cannotInline") || - strings.HasPrefix(msg, "inlineCall"): - return o.Annotations[settings.Inline] - case strings.HasPrefix(msg, "escape") || msg == "leak": - return o.Annotations[settings.Escape] - case strings.HasPrefix(msg, "nilcheck"): - return o.Annotations[settings.Nil] - case strings.HasPrefix(msg, "isInBounds") || - strings.HasPrefix(msg, "isSliceInBounds"): - return o.Annotations[settings.Bounds] - } - return false -} - -// The range produced by the compiler is 1-indexed, so subtract range by 1. -func zeroIndexedRange(rng protocol.Range) protocol.Range { - return protocol.Range{ - Start: protocol.Position{ - Line: rng.Start.Line - 1, - Character: rng.Start.Character - 1, - }, - End: protocol.Position{ - Line: rng.End.Line - 1, - Character: rng.End.Character - 1, - }, - } -} - func findJSONFiles(dir string) ([]string, error) { ans := []string{} f := func(path string, fi os.FileInfo, _ error) error { diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 7b4abe774a4..f438a220000 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -40,7 +40,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/typesutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/stdlib" @@ -205,7 +204,7 @@ func (ipm insensitivePrefixMatcher) Score(candidateLabel string) float32 { type completer struct { snapshot *cache.Snapshot pkg *cache.Package - qf types.Qualifier // for qualifying typed expressions + qual types.Qualifier // for qualifying typed expressions mq golang.MetadataQualifier // for syntactic qualifying opts *completionOptions @@ -266,9 +265,10 @@ type completer struct { // matcher matches the candidates against the surrounding prefix. matcher matcher - // methodSetCache caches the types.NewMethodSet call, which is relatively + // methodSetCache caches the [types.NewMethodSet] call, which is relatively // expensive and can be called many times for the same type while searching // for deep completions. + // TODO(adonovan): use [typeutil.MethodSetCache], which exists for this purpose. methodSetCache map[methodSetKey]*types.MethodSet // tooNewSymbolsCache is a cache of @@ -294,6 +294,11 @@ type completer struct { // including nil values for nodes that don't defined a scope. It // also includes our package scope and the universal scope at the // end. + // + // (It is tempting to replace this with fileScope.Innermost(pos) + // and simply follow the Scope.Parent chain, but we need to + // preserve the pairwise association of scopes[i] and path[i] + // because there is no way to get from the Scope to the Node.) scopes []*types.Scope } @@ -530,6 +535,8 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p return nil, nil, fmt.Errorf("cannot find node enclosing position") } + info := pkg.TypesInfo() + // Check if completion at this position is valid. If not, return early. switch n := path[0].(type) { case *ast.BasicLit: @@ -548,7 +555,7 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p } case *ast.Ident: // Don't offer completions for (most) defining identifiers. - if obj, ok := pkg.TypesInfo().Defs[n]; ok { + if obj, ok := info.Defs[n]; ok { if v, ok := obj.(*types.Var); ok && v.IsField() && v.Embedded() { // Allow completion of anonymous fields, since they may reference type // names. @@ -570,21 +577,31 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p } } - // Collect all surrounding scopes, innermost first. - scopes := golang.CollectScopes(pkg.TypesInfo(), path, pos) + // Collect all surrounding scopes, innermost first, inserting + // nils as needed to preserve the correspondence with path[i]. + var scopes []*types.Scope + for _, n := range path { + switch node := n.(type) { + case *ast.FuncDecl: + n = node.Type + case *ast.FuncLit: + n = node.Type + } + scopes = append(scopes, info.Scopes[n]) + } scopes = append(scopes, pkg.Types().Scope(), types.Universe) var goversion string // "" => no version check // Prior go1.22, the behavior of FileVersion is not useful to us. if slices.Contains(build.Default.ReleaseTags, "go1.22") { - goversion = versions.FileVersion(pkg.TypesInfo(), pgf.File) // may be "" + goversion = versions.FileVersion(info, pgf.File) // may be "" } opts := snapshot.Options() c := &completer{ pkg: pkg, snapshot: snapshot, - qf: typesutil.FileQualifier(pgf.File, pkg.Types(), pkg.TypesInfo()), + qual: typesinternal.FileQualifier(pgf.File, pkg.Types()), mq: golang.MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()), completionContext: completionContext{ triggerCharacter: protoContext.TriggerCharacter, @@ -598,8 +615,8 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p path: path, pos: pos, seen: make(map[types.Object]bool), - enclosingFunc: enclosingFunction(path, pkg.TypesInfo()), - enclosingCompositeLiteral: enclosingCompositeLiteral(path, pos, pkg.TypesInfo()), + enclosingFunc: enclosingFunction(path, info), + enclosingCompositeLiteral: enclosingCompositeLiteral(path, pos, info), deepState: deepCompletionState{ enabled: opts.DeepCompletion, }, @@ -1612,7 +1629,7 @@ func (c *completer) lexical(ctx context.Context) error { for _, name := range scope.Names() { declScope, obj := scope.LookupParent(name, c.pos) if declScope != scope { - continue // Name was declared in some enclosing scope, or not at all. + continue // scope of name starts after c.pos } // If obj's type is invalid, find the AST node that defines the lexical block @@ -1750,7 +1767,7 @@ func (c *completer) injectType(ctx context.Context, t types.Type) { // a named type whose name is literally "[]int". This allows // us to reuse our object based completion machinery. fakeNamedType := candidate{ - obj: types.NewTypeName(token.NoPos, nil, types.TypeString(t, c.qf), t), + obj: types.NewTypeName(token.NoPos, nil, types.TypeString(t, c.qual), t), score: stdScore, } // Make sure the type name matches before considering @@ -2690,7 +2707,7 @@ func reverseInferTypeArgs(sig *types.Signature, typeArgs []types.Type, expectedR return substs } -// inferExpectedTypeArg gives a type param candidateInference based on the surroundings of it's call site. +// inferExpectedTypeArg gives a type param candidateInference based on the surroundings of its call site. // If successful, the inf parameter is returned with only it's objType field updated. // // callNodeIdx is the index within the completion path of the type parameter's parent call expression. @@ -2704,9 +2721,12 @@ func (c *completer) inferExpectedTypeArg(callNodeIdx int, typeParamIdx int) type if !ok { return nil } + sig, ok := c.pkg.TypesInfo().Types[callNode.Fun].Type.(*types.Signature) + if !ok { + return nil + } - // Infer the type parameters in a function call based on it's context - sig := c.pkg.TypesInfo().Types[callNode.Fun].Type.(*types.Signature) + // Infer the type parameters in a function call based on context expectedResults := inferExpectedResultTypes(c, callNodeIdx) if typeParamIdx < 0 || typeParamIdx >= sig.TypeParams().Len() { return nil diff --git a/gopls/internal/golang/completion/format.go b/gopls/internal/golang/completion/format.go index 872025949fb..f4fc7339b95 100644 --- a/gopls/internal/golang/completion/format.go +++ b/gopls/internal/golang/completion/format.go @@ -51,7 +51,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e var ( label = cand.name - detail = types.TypeString(obj.Type(), c.qf) + detail = types.TypeString(obj.Type(), c.qual) insert = label kind = protocol.TextCompletion snip snippet.Builder @@ -71,7 +71,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e switch obj := obj.(type) { case *types.TypeName: - detail, kind = golang.FormatType(obj.Type(), c.qf) + detail, kind = golang.FormatType(obj.Type(), c.qual) case *types.Const: kind = protocol.ConstantCompletion case *types.Var: @@ -79,7 +79,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e detail = "struct{...}" // for anonymous unaliased struct types } else if obj.IsField() { var err error - detail, err = golang.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf, c.mq) + detail, err = golang.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qual, c.mq) if err != nil { return CompletionItem{}, err } @@ -128,7 +128,7 @@ Suffixes: switch mod { case invoke: if sig, ok := funcType.Underlying().(*types.Signature); ok { - s, err := golang.NewSignature(ctx, c.snapshot, c.pkg, sig, nil, c.qf, c.mq) + s, err := golang.NewSignature(ctx, c.snapshot, c.pkg, sig, nil, c.qual, c.mq) if err != nil { return CompletionItem{}, err } @@ -288,7 +288,7 @@ func (c *completer) formatConversion(convertTo types.Type) conversionEdits { return conversionEdits{} } - typeName := types.TypeString(convertTo, c.qf) + typeName := types.TypeString(convertTo, c.qual) switch t := convertTo.(type) { // We need extra parens when casting to these types. For example, // we need "(*int)(foo)", not "*int(foo)". @@ -299,7 +299,7 @@ func (c *completer) formatConversion(convertTo types.Type) conversionEdits { // must need a conversion here. However, if the target type is untyped, // don't suggest converting to e.g. "untyped float" (golang/go#62141). if t.Info()&types.IsUntyped != 0 { - typeName = types.TypeString(types.Default(convertTo), c.qf) + typeName = types.TypeString(types.Default(convertTo), c.qual) } } return conversionEdits{prefix: typeName + "(", suffix: ")"} diff --git a/gopls/internal/golang/completion/literal.go b/gopls/internal/golang/completion/literal.go index 50ddb1fc26e..ef077ab7e20 100644 --- a/gopls/internal/golang/completion/literal.go +++ b/gopls/internal/golang/completion/literal.go @@ -79,7 +79,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im } var ( - qf = c.qf + qual = c.qual sel = enclosingSelector(c.path, c.pos) conversion conversionEdits ) @@ -91,10 +91,10 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im // Don't qualify the type name if we are in a selector expression // since the package name is already present. if sel != nil { - qf = func(_ *types.Package) string { return "" } + qual = func(_ *types.Package) string { return "" } } - snip, typeName := c.typeNameSnippet(literalType, qf) + snip, typeName := c.typeNameSnippet(literalType, qual) // A type name of "[]int" doesn't work very will with the matcher // since "[" isn't a valid identifier prefix. Here we strip off the @@ -102,9 +102,9 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im matchName := typeName switch t := literalType.(type) { case *types.Slice: - matchName = types.TypeString(t.Elem(), qf) + matchName = types.TypeString(t.Elem(), qual) case *types.Array: - matchName = types.TypeString(t.Elem(), qf) + matchName = types.TypeString(t.Elem(), qual) } addlEdits, err := c.importEdits(imp) @@ -298,7 +298,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m // of "i int, j int". if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) { snip.WriteText(" ") - typeStr, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf, c.mq) + typeStr, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qual, c.mq) if err != nil { // In general, the only error we should encounter while formatting is // context cancellation. @@ -356,7 +356,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m snip.WriteText(name + " ") } - text, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf, c.mq) + text, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qual, c.mq) if err != nil { // In general, the only error we should encounter while formatting is // context cancellation. @@ -513,7 +513,7 @@ func (c *completer) makeCall(snip *snippet.Builder, typeName string, secondArg s } // Create a snippet for a type name where type params become placeholders. -func (c *completer) typeNameSnippet(literalType types.Type, qf types.Qualifier) (*snippet.Builder, string) { +func (c *completer) typeNameSnippet(literalType types.Type, qual types.Qualifier) (*snippet.Builder, string) { var ( snip snippet.Builder typeName string @@ -526,7 +526,7 @@ func (c *completer) typeNameSnippet(literalType types.Type, qf types.Qualifier) // Inv: pnt is not "error" or "unsafe.Pointer", so pnt.Obj() != nil and has a Pkg(). // We are not "fully instantiated" meaning we have type params that must be specified. - if pkg := qf(pnt.Obj().Pkg()); pkg != "" { + if pkg := qual(pnt.Obj().Pkg()); pkg != "" { typeName = pkg + "." } @@ -540,7 +540,7 @@ func (c *completer) typeNameSnippet(literalType types.Type, qf types.Qualifier) snip.WriteText(", ") } snip.WritePlaceholder(func(snip *snippet.Builder) { - snip.WriteText(types.TypeString(tparams.At(i), qf)) + snip.WriteText(types.TypeString(tparams.At(i), qual)) }) } } else { @@ -550,7 +550,7 @@ func (c *completer) typeNameSnippet(literalType types.Type, qf types.Qualifier) typeName += "[...]" } else { // We don't have unspecified type params so use default type formatting. - typeName = types.TypeString(literalType, qf) + typeName = types.TypeString(literalType, qual) snip.WriteText(typeName) } diff --git a/gopls/internal/golang/completion/postfix_snippets.go b/gopls/internal/golang/completion/postfix_snippets.go index e0fc12cc9b5..4ffd14225fa 100644 --- a/gopls/internal/golang/completion/postfix_snippets.go +++ b/gopls/internal/golang/completion/postfix_snippets.go @@ -69,7 +69,7 @@ type postfixTmplArgs struct { // Type is the type of "foo.bar" in "foo.bar.print!". Type types.Type - // FuncResult are results of the enclosed function + // FuncResults are results of the enclosed function FuncResults []*types.Var sel *ast.SelectorExpr @@ -77,7 +77,7 @@ type postfixTmplArgs struct { snip snippet.Builder importIfNeeded func(pkgPath string, scope *types.Scope) (name string, edits []protocol.TextEdit, err error) edits []protocol.TextEdit - qf types.Qualifier + qual types.Qualifier varNames map[string]bool placeholders bool currentTabStop int @@ -437,12 +437,13 @@ func (a *postfixTmplArgs) TypeName(t types.Type) (string, error) { if t == nil || t == types.Typ[types.Invalid] { return "", fmt.Errorf("invalid type: %v", t) } - return types.TypeString(t, a.qf), nil + return types.TypeString(t, a.qual), nil } // Zero return the zero value representation of type t func (a *postfixTmplArgs) Zero(t types.Type) string { - return typesinternal.ZeroString(t, a.qf) + zero, _ := typesinternal.ZeroString(t, a.qual) + return zero } func (a *postfixTmplArgs) IsIdent() bool { @@ -587,7 +588,7 @@ func (c *completer) addPostfixSnippetCandidates(ctx context.Context, sel *ast.Se Type: selType, FuncResults: funcResults, sel: sel, - qf: c.qf, + qual: c.qual, importIfNeeded: c.importIfNeeded, scope: scope, varNames: make(map[string]bool), diff --git a/gopls/internal/golang/completion/statements.go b/gopls/internal/golang/completion/statements.go index e187bf2bee0..3791211d6a6 100644 --- a/gopls/internal/golang/completion/statements.go +++ b/gopls/internal/golang/completion/statements.go @@ -295,7 +295,9 @@ func (c *completer) addErrCheck() { } else { snip.WriteText("return ") for i := 0; i < result.Len()-1; i++ { - snip.WriteText(typesinternal.ZeroString(result.At(i).Type(), c.qf)) + if zero, isValid := typesinternal.ZeroString(result.At(i).Type(), c.qual); isValid { + snip.WriteText(zero) + } snip.WriteText(", ") } snip.WritePlaceholder(func(b *snippet.Builder) { @@ -405,7 +407,10 @@ func (c *completer) addReturnZeroValues() { fmt.Fprintf(&label, ", ") } - zero := typesinternal.ZeroString(result.At(i).Type(), c.qf) + zero, isValid := typesinternal.ZeroString(result.At(i).Type(), c.qual) + if !isValid { + zero = "" + } snip.WritePlaceholder(func(b *snippet.Builder) { b.WriteText(zero) }) diff --git a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go index f20fe85f541..d64a53a5114 100644 --- a/gopls/internal/golang/definition.go +++ b/gopls/internal/golang/definition.go @@ -15,12 +15,13 @@ import ( "regexp" "strings" + "golang.org/x/tools/go/ast/astutil" "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" + goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/event" ) @@ -84,6 +85,89 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p return locations, err // may be success or failure } + // Handle definition requests for various special kinds of syntax node. + path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) + switch node := path[0].(type) { + // Handle the case where the cursor is on a return statement by jumping to the result variables. + case *ast.ReturnStmt: + var funcType *ast.FuncType + for _, n := range path[1:] { + switch n := n.(type) { + case *ast.FuncLit: + funcType = n.Type + case *ast.FuncDecl: + funcType = n.Type + } + if funcType != nil { + break + } + } + // Inv: funcType != nil, as a return stmt cannot appear outside a function. + if funcType.Results == nil { + return nil, nil // no result variables + } + loc, err := pgf.NodeLocation(funcType.Results) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil + + case *ast.BranchStmt: + // Handle the case where the cursor is on a goto, break or continue statement by returning the + // location of the label, the closing brace of the relevant block statement, or the + // start of the relevant loop, respectively. + label, isLabeled := pkg.TypesInfo().Uses[node.Label].(*types.Label) + switch node.Tok { + case token.GOTO: + if isLabeled { + loc, err := pgf.PosLocation(label.Pos(), label.Pos()+token.Pos(len(label.Name()))) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil + } else { + // Workaround for #70957. + // TODO(madelinekalil): delete when go1.25 fixes it. + return nil, nil + } + case token.BREAK, token.CONTINUE: + // Find innermost relevant ancestor for break/continue. + for i, n := range path[1:] { + if isLabeled { + l, ok := path[1:][i+1].(*ast.LabeledStmt) + if !(ok && l.Label.Name == label.Name()) { + continue + } + } + switch n.(type) { + case *ast.ForStmt, *ast.RangeStmt: + var start, end token.Pos + if node.Tok == token.BREAK { + start, end = n.End()-token.Pos(len("}")), n.End() + } else { // CONTINUE + start, end = n.Pos(), n.Pos()+token.Pos(len("for")) + } + loc, err := pgf.PosLocation(start, end) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil + case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: + if node.Tok == token.BREAK { + loc, err := pgf.PosLocation(n.End()-1, n.End()) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil + } + case *ast.FuncDecl, *ast.FuncLit: + // bad syntax; avoid jumping outside the current function + return nil, nil + } + } + } + } + // The general case: the cursor is on an identifier. _, obj, _ := referencedObject(pkg, pgf, pos) if obj == nil { @@ -102,7 +186,7 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p for _, decl := range pgf.File.Decls { if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body == nil && - astutil.NodeContains(decl.Name, pos) { + goplsastutil.NodeContains(decl.Name, pos) { return nonGoDefinition(ctx, snapshot, pkg, decl.Name.Name) } } diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go index 72d718c2faf..2ce89795a06 100644 --- a/gopls/internal/golang/extract.go +++ b/gopls/internal/golang/extract.go @@ -20,6 +20,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/analysisinternal" @@ -27,24 +28,54 @@ import ( ) // extractVariable implements the refactor.extract.{variable,constant} CodeAction command. -func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractExprs(fset, start, end, src, file, info, false) +} + +// extractVariableAll implements the refactor.extract.{variable,constant}-all CodeAction command. +func extractVariableAll(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractExprs(fset, start, end, src, file, info, true) +} + +// extractExprs replaces occurrence(s) of a specified expression within the same function +// with newVar. If 'all' is true, it replaces all occurrences of the same expression; +// otherwise, it only replaces the selected expression. +// +// The new variable/constant is declared as close as possible to the first found expression +// within the deepest common scope accessible to all candidate occurrences. +func extractExprs(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, info *types.Info, all bool) (*token.FileSet, *analysis.SuggestedFix, error) { tokFile := fset.File(file.FileStart) - expr, path, err := canExtractVariable(info, file, start, end) + exprs, err := canExtractVariable(info, file, start, end, all) if err != nil { - return nil, nil, fmt.Errorf("cannot extract %s: %v", safetoken.StartPosition(fset, start), err) + return nil, nil, fmt.Errorf("cannot extract: %v", err) + } + + // innermost scope enclosing ith expression + exprScopes := make([]*types.Scope, len(exprs)) + for i, e := range exprs { + exprScopes[i] = info.Scopes[file].Innermost(e.Pos()) } - constant := info.Types[expr].Value != nil + + hasCollision := func(name string) bool { + for _, scope := range exprScopes { + if s, _ := scope.LookupParent(name, token.NoPos); s != nil { + return true + } + } + return false + } + constant := info.Types[exprs[0]].Value != nil // Generate name(s) for new declaration. - baseName := cond(constant, "k", "x") + baseName := cond(constant, "newConst", "newVar") var lhsNames []string - switch expr := expr.(type) { + switch expr := exprs[0].(type) { case *ast.CallExpr: tup, ok := info.TypeOf(expr).(*types.Tuple) if !ok { // conversion or single-valued call: // treat it the same as our standard extract variable case. - name, _ := freshName(info, file, expr.Pos(), baseName, 0) + name, _ := generateName(0, baseName, hasCollision) lhsNames = append(lhsNames, name) } else { @@ -53,17 +84,55 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file for range tup.Len() { // Generate a unique variable for each result. var name string - name, idx = freshName(info, file, expr.Pos(), baseName, idx) + name, idx = generateName(idx, baseName, hasCollision) lhsNames = append(lhsNames, name) } } default: // TODO: stricter rules for selectorExpr. - name, _ := freshName(info, file, expr.Pos(), baseName, 0) + name, _ := generateName(0, baseName, hasCollision) lhsNames = append(lhsNames, name) } + // Where all the extractable positions can see variable being declared. + var commonScope *types.Scope + counter := make(map[*types.Scope]int) +Outer: + for _, scope := range exprScopes { + for s := scope; s != nil; s = s.Parent() { + counter[s]++ + if counter[s] == len(exprScopes) { + // A scope whose count is len(scopes) is common to all ancestor paths. + // Stop at the first (innermost) one. + commonScope = s + break Outer + } + } + } + + var visiblePath []ast.Node + if commonScope != exprScopes[0] { + // This means the first expr within function body is not the largest scope, + // we need to find the scope immediately follow the common + // scope where we will insert the statement before. + child := exprScopes[0] + for p := child; p != nil; p = p.Parent() { + if p == commonScope { + break + } + child = p + } + visiblePath, _ = astutil.PathEnclosingInterval(file, child.Pos(), child.End()) + } else { + // Insert newVar inside commonScope before the first occurrence of the expression. + visiblePath, _ = astutil.PathEnclosingInterval(file, exprs[0].Pos(), exprs[0].End()) + } + variables, err := collectFreeVars(info, file, exprs[0].Pos(), exprs[0].End(), exprs[0]) + if err != nil { + return nil, nil, err + } + // TODO: There is a bug here: for a variable declared in a labeled // switch/for statement it returns the for/switch statement itself // which produces the below code which is a compiler error. e.g. @@ -74,26 +143,16 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file // x := r() // switch r1 := x { ... break label ... } // compiler error // - // TODO(golang/go#70563): Another bug: extracting the - // expression to the recommended place may cause it to migrate - // across one or more declarations that it references. - // - // Before: - // if x := 1; cond { - // } else if y := «x + 2»; cond { - // } - // - // After: - // x1 := x + 2 // error: undefined x - // if x := 1; cond { - // } else if y := x1; cond { - // } var ( insertPos token.Pos indentation string stmtOK bool // ok to use ":=" instead of var/const decl? ) - if before := analysisinternal.StmtToInsertVarBefore(path); before != nil { + if funcDecl, ok := visiblePath[len(visiblePath)-2].(*ast.FuncDecl); ok && goplsastutil.NodeContains(funcDecl.Body, start) { + before, err := stmtToInsertVarBefore(visiblePath, variables) + if err != nil { + return nil, nil, fmt.Errorf("cannot find location to insert extraction: %v", err) + } // Within function: compute appropriate statement indentation. indent, err := calculateIndentation(src, tokFile, before) if err != nil { @@ -116,7 +175,7 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file } else { // Outside any statement: insert before the current // declaration, without indentation. - currentDecl := path[len(path)-2] + currentDecl := visiblePath[len(visiblePath)-2] insertPos = currentDecl.Pos() indentation = "\n" } @@ -152,7 +211,7 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file Specs: []ast.Spec{ &ast.ValueSpec{ Names: names, - Values: []ast.Expr{expr}, + Values: []ast.Expr{exprs[0]}, }, }, } @@ -166,7 +225,7 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file newNode = &ast.AssignStmt{ Tok: token.DEFINE, Lhs: lhs, - Rhs: []ast.Expr{expr}, + Rhs: []ast.Expr{exprs[0]}, } } @@ -177,50 +236,264 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file } // TODO(adonovan): not sound for `...` string literals containing newlines. assignment := strings.ReplaceAll(buf.String(), "\n", indentation) + indentation - + textEdits := []analysis.TextEdit{{ + Pos: insertPos, + End: insertPos, + NewText: []byte(assignment), + }} + for _, e := range exprs { + textEdits = append(textEdits, analysis.TextEdit{ + Pos: e.Pos(), + End: e.End(), + NewText: []byte(strings.Join(lhsNames, ", ")), + }) + } return fset, &analysis.SuggestedFix{ - TextEdits: []analysis.TextEdit{ - { - Pos: insertPos, - End: insertPos, - NewText: []byte(assignment), - }, - { - Pos: start, - End: end, - NewText: []byte(strings.Join(lhsNames, ", ")), - }, - }, + TextEdits: textEdits, }, nil } +// stmtToInsertVarBefore returns the ast.Stmt before which we can safely insert a new variable, +// and ensures that the new declaration is inserted at a point where all free variables are declared before. +// Some examples: +// +// Basic Example: +// +// z := 1 +// y := z + x +// +// If x is undeclared, then this function would return `y := z + x`, so that we +// can insert `x := ` on the line before `y := z + x`. +// +// valid IfStmt example: +// +// if z == 1 { +// } else if z == y {} +// +// If y is undeclared, then this function would return `if z == 1 {`, because we cannot +// insert a statement between an if and an else if statement. As a result, we need to find +// the top of the if chain to insert `y := ` before. +// +// invalid IfStmt example: +// +// if x := 1; true { +// } else if y := x + 1; true { //apply refactor.extract.variable to x +// } +// +// `x` is a free variable defined in the IfStmt, we should not insert +// the extracted expression outside the IfStmt scope, instead, return an error. +func stmtToInsertVarBefore(path []ast.Node, variables []*variable) (ast.Stmt, error) { + enclosingIndex := -1 // index in path of enclosing stmt + for i, p := range path { + if _, ok := p.(ast.Stmt); ok { + enclosingIndex = i + break + } + } + if enclosingIndex == -1 { + return nil, fmt.Errorf("no enclosing statement") + } + enclosingStmt := path[enclosingIndex].(ast.Stmt) + + // hasFreeVar reports if any free variables is defined inside stmt (which may be nil). + // If true, indicates that the insertion point will sit before the variable declaration. + hasFreeVar := func(stmt ast.Stmt) bool { + if stmt == nil { + return false + } + for _, v := range variables { + if goplsastutil.NodeContains(stmt, v.obj.Pos()) { + return true + } + } + return false + } + + // baseIfStmt walks up the if/else-if chain until we get to + // the top of the current if chain. + baseIfStmt := func(index int) (ast.Stmt, error) { + stmt := path[index] + for _, node := range path[index+1:] { + ifStmt, ok := node.(*ast.IfStmt) + if !ok || ifStmt.Else != stmt { + break + } + if hasFreeVar(ifStmt.Init) { + return nil, fmt.Errorf("Else's init statement has free variable declaration") + } + stmt = ifStmt + } + return stmt.(ast.Stmt), nil + } + + switch enclosingStmt := enclosingStmt.(type) { + case *ast.IfStmt: + if hasFreeVar(enclosingStmt.Init) { + return nil, fmt.Errorf("IfStmt's init statement has free variable declaration") + } + // The enclosingStmt is inside of the if declaration, + // We need to check if we are in an else-if stmt and + // get the base if statement. + return baseIfStmt(enclosingIndex) + case *ast.CaseClause: + // Get the enclosing switch stmt if the enclosingStmt is + // inside of the case statement. + for _, node := range path[enclosingIndex+1:] { + switch stmt := node.(type) { + case *ast.SwitchStmt: + if hasFreeVar(stmt.Init) { + return nil, fmt.Errorf("SwitchStmt's init statement has free variable declaration") + } + return stmt, nil + case *ast.TypeSwitchStmt: + if hasFreeVar(stmt.Init) { + return nil, fmt.Errorf("TypeSwitchStmt's init statement has free variable declaration") + } + return stmt, nil + } + } + } + // Check if the enclosing statement is inside another node. + switch parent := path[enclosingIndex+1].(type) { + case *ast.IfStmt: + if hasFreeVar(parent.Init) { + return nil, fmt.Errorf("IfStmt's init statement has free variable declaration") + } + return baseIfStmt(enclosingIndex + 1) + case *ast.ForStmt: + if parent.Init == enclosingStmt || parent.Post == enclosingStmt { + return parent, nil + } + case *ast.SwitchStmt: + if hasFreeVar(parent.Init) { + return nil, fmt.Errorf("SwitchStmt's init statement has free variable declaration") + } + return parent, nil + case *ast.TypeSwitchStmt: + if hasFreeVar(parent.Init) { + return nil, fmt.Errorf("TypeSwitchStmt's init statement has free variable declaration") + } + return parent, nil + } + return enclosingStmt.(ast.Stmt), nil +} + // canExtractVariable reports whether the code in the given range can be -// extracted to a variable (or constant). -func canExtractVariable(info *types.Info, file *ast.File, start, end token.Pos) (ast.Expr, []ast.Node, error) { +// extracted to a variable (or constant). It returns the selected expression or, if 'all', +// all structurally equivalent expressions within the same function body, in lexical order. +func canExtractVariable(info *types.Info, file *ast.File, start, end token.Pos, all bool) ([]ast.Expr, error) { if start == end { - return nil, nil, fmt.Errorf("empty selection") + return nil, fmt.Errorf("empty selection") } path, exact := astutil.PathEnclosingInterval(file, start, end) if !exact { - return nil, nil, fmt.Errorf("selection is not an expression") + return nil, fmt.Errorf("selection is not an expression") } if len(path) == 0 { - return nil, nil, bug.Errorf("no path enclosing interval") + return nil, bug.Errorf("no path enclosing interval") } for _, n := range path { if _, ok := n.(*ast.ImportSpec); ok { - return nil, nil, fmt.Errorf("cannot extract variable or constant in an import block") + return nil, fmt.Errorf("cannot extract variable or constant in an import block") } } expr, ok := path[0].(ast.Expr) if !ok { - return nil, nil, fmt.Errorf("selection is not an expression") // e.g. statement + return nil, fmt.Errorf("selection is not an expression") // e.g. statement } if tv, ok := info.Types[expr]; !ok || !tv.IsValue() || tv.Type == nil || tv.HasOk() { // e.g. type, builtin, x.(type), 2-valued m[k], or ill-typed - return nil, nil, fmt.Errorf("selection is not a single-valued expression") + return nil, fmt.Errorf("selection is not a single-valued expression") + } + + var exprs []ast.Expr + if !all { + exprs = append(exprs, expr) + } else if funcDecl, ok := path[len(path)-2].(*ast.FuncDecl); ok { + // Find all expressions in the same function body that + // are equal to the selected expression. + ast.Inspect(funcDecl.Body, func(n ast.Node) bool { + if e, ok := n.(ast.Expr); ok { + if goplsastutil.Equal(e, expr, func(x, y *ast.Ident) bool { + xobj, yobj := info.ObjectOf(x), info.ObjectOf(y) + // The two identifiers must resolve to the same object, + // or to a declaration within the candidate expression. + // (This allows two copies of "func (x int) { print(x) }" + // to match.) + if xobj != nil && goplsastutil.NodeContains(e, xobj.Pos()) && + yobj != nil && goplsastutil.NodeContains(expr, yobj.Pos()) { + return x.Name == y.Name + } + // Use info.Uses to avoid including declaration, for example, + // when extractnig x: + // + // x := 1 // should not include x + // y := x // include x + // z := x // include x + xuse := info.Uses[x] + return xuse != nil && xuse == info.Uses[y] + }) { + exprs = append(exprs, e) + } + } + return true + }) + } else { + return nil, fmt.Errorf("node %T is not inside a function", expr) } - return expr, path, nil + + // Disallow any expr that sits in lhs of an AssignStmt or ValueSpec for now. + // + // TODO(golang/go#70784): In such cases, exprs are operated in "variable" mode (L-value mode in C). + // In contrast, exprs in the RHS operate in "value" mode (R-value mode in C). + // L-value mode refers to exprs that represent storage locations, + // while R-value mode refers to exprs that represent values. + // There are a number of expressions that may have L-value mode, given by: + // + // lvalue = ident -- Ident such that info.Uses[id] is a *Var + // | '(' lvalue ') ' -- ParenExpr + // | lvalue '[' expr ']' -- IndexExpr + // | lvalue '.' ident -- SelectorExpr. + // + // For example: + // + // type foo struct { + // bar int + // } + // f := foo{bar: 1} + // x := f.bar + 1 // f.bar operates in "value" mode. + // f.bar = 2 // f.bar operates in "variable" mode. + // + // When extracting exprs in variable mode, we must be cautious. Any such extraction + // may require capturing the address of the expression and replacing its uses with dereferenced access. + // The type checker records this information in info.Types[id].{IsValue,Addressable}(). + // The correct result should be: + // + // newVar := &f.bar + // x := *newVar + 1 + // *newVar = 2 + for _, e := range exprs { + path, _ := astutil.PathEnclosingInterval(file, e.Pos(), e.End()) + for _, n := range path { + if assignment, ok := n.(*ast.AssignStmt); ok { + for _, lhs := range assignment.Lhs { + if lhs == e { + return nil, fmt.Errorf("node %T is in LHS of an AssignStmt", expr) + } + } + break + } + if value, ok := n.(*ast.ValueSpec); ok { + for _, name := range value.Names { + if name == e { + return nil, fmt.Errorf("node %T is in LHS of a ValueSpec", expr) + } + } + break + } + } + } + return exprs, nil } // Calculate indentation for insertion. @@ -331,14 +604,6 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte safetoken.StartPosition(fset, start), err) } tok, path, start, end, outer, node := p.tok, p.path, p.start, p.end, p.outer, p.node - fileScope := info.Scopes[file] - if fileScope == nil { - return nil, nil, fmt.Errorf("%s: file scope is empty", errorPrefix) - } - pkgScope := fileScope.Parent() - if pkgScope == nil { - return nil, nil, fmt.Errorf("%s: package scope is empty", errorPrefix) - } // A return statement is non-nested if its parent node is equal to the parent node // of the first node in the selection. These cases must be handled separately because @@ -373,7 +638,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte // we must determine the signature of the extracted function. We will then replace // the block with an assignment statement that calls the extracted function with // the appropriate parameters and return values. - variables, err := collectFreeVars(info, file, fileScope, pkgScope, start, end, path[0]) + variables, err := collectFreeVars(info, file, start, end, path[0]) if err != nil { return nil, nil, err } @@ -429,6 +694,8 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte // of using "x, y, z := fn()" style assignment statements. var canRedefineCount int + qual := typesinternal.FileQualifier(file, pkg) + // Each identifier in the selected block must become (1) a parameter to the // extracted function, (2) a return value of the extracted function, or (3) a local // variable in the extracted function. Determine the outcome(s) for each variable @@ -442,10 +709,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte // The blank identifier is always a local variable continue } - typ := typesinternal.TypeExpr(file, pkg, v.obj.Type()) - if typ == nil { - return nil, nil, fmt.Errorf("nil AST expression for type: %v", v.obj.Name()) - } + typ := typesinternal.TypeExpr(v.obj.Type(), qual) seenVars[v.obj] = typ identifier := ast.NewIdent(v.obj.Name()) // An identifier must meet three conditions to become a return value of the @@ -616,7 +880,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte // signature of the extracted function as described above. Adjust all of // the return statements in the extracted function to reflect this change in // signature. - if err := adjustReturnStatements(returnTypes, seenVars, file, pkg, extractedBlock); err != nil { + if err := adjustReturnStatements(returnTypes, seenVars, extractedBlock, qual); err != nil { return nil, nil, err } } @@ -922,7 +1186,15 @@ type variable struct { // variables will be used as arguments in the extracted function. It also returns a // list of identifiers that may need to be returned by the extracted function. // Some of the code in this function has been adapted from tools/cmd/guru/freevars.go. -func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *types.Scope, start, end token.Pos, node ast.Node) ([]*variable, error) { +func collectFreeVars(info *types.Info, file *ast.File, start, end token.Pos, node ast.Node) ([]*variable, error) { + fileScope := info.Scopes[file] + if fileScope == nil { + return nil, bug.Errorf("file scope is empty") + } + pkgScope := fileScope.Parent() + if pkgScope == nil { + return nil, bug.Errorf("package scope is empty") + } // id returns non-nil if n denotes an object that is referenced by the span // and defined either within the span or in the lexical environment. The bool // return value acts as an indicator for where it was defined. @@ -1318,16 +1590,13 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast. // Generate information for the values in the return signature of the enclosing function. if enclosing.Results != nil { nameIdx := make(map[string]int) // last integral suffixes of generated names + qual := typesinternal.FileQualifier(file, pkg) for _, field := range enclosing.Results.List { typ := info.TypeOf(field.Type) if typ == nil { return nil, nil, fmt.Errorf( "failed type conversion, AST expression: %T", field.Type) } - expr := typesinternal.TypeExpr(file, pkg, typ) - if expr == nil { - return nil, nil, fmt.Errorf("nil AST expression") - } names := []string{""} if len(field.Names) > 0 { names = nil @@ -1344,13 +1613,13 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast. } retName, idx := freshNameOutsideRange(info, file, path[0].Pos(), start, end, bestName, nameIdx[bestName]) nameIdx[bestName] = idx - z := typesinternal.ZeroExpr(file, pkg, typ) - if z == nil { + z, isValid := typesinternal.ZeroExpr(typ, qual) + if !isValid { return nil, nil, fmt.Errorf("can't generate zero value for %T", typ) } retVars = append(retVars, &returnVariable{ name: ast.NewIdent(retName), - decl: &ast.Field{Type: expr}, + decl: &ast.Field{Type: typesinternal.TypeExpr(typ, qual)}, zeroVal: z, }) } @@ -1385,7 +1654,7 @@ var conventionalVarNames = map[objKey]string{ {"http", "ResponseWriter"}: "rw", // Note: same as [AbbreviateVarName]. } -// varNameForTypeName chooses a "good" name for a variable with the given type, +// varNameForType chooses a "good" name for a variable with the given type, // if possible. Otherwise, it returns "", false. // // For special types, it uses known conventional names. @@ -1414,19 +1683,20 @@ func varNameForType(t types.Type) (string, bool) { // adjustReturnStatements adds "zero values" of the given types to each return statement // in the given AST node. -func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]ast.Expr, file *ast.File, pkg *types.Package, extractedBlock *ast.BlockStmt) error { +func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]ast.Expr, extractedBlock *ast.BlockStmt, qual types.Qualifier) error { var zeroVals []ast.Expr // Create "zero values" for each type. for _, returnType := range returnTypes { var val ast.Expr + var isValid bool for obj, typ := range seenVars { if typ != returnType.Type { continue } - val = typesinternal.ZeroExpr(file, pkg, obj.Type()) + val, isValid = typesinternal.ZeroExpr(obj.Type(), qual) break } - if val == nil { + if !isValid { return fmt.Errorf("could not find matching AST expression for %T", returnType.Type) } zeroVals = append(zeroVals, val) diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go index f88343f029c..7e83c1d6700 100644 --- a/gopls/internal/golang/fix.go +++ b/gopls/internal/golang/fix.go @@ -59,6 +59,7 @@ func singleFile(fixer1 singleFileFixer) fixer { // Names of ApplyFix.Fix created directly by the CodeAction handler. const ( fixExtractVariable = "extract_variable" // (or constant) + fixExtractVariableAll = "extract_variable_all" fixExtractFunction = "extract_function" fixExtractMethod = "extract_method" fixInlineCall = "inline_call" @@ -106,6 +107,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file fixExtractFunction: singleFile(extractFunction), fixExtractMethod: singleFile(extractMethod), fixExtractVariable: singleFile(extractVariable), + fixExtractVariableAll: singleFile(extractVariableAll), fixInlineCall: inlineCall, fixInvertIfCondition: singleFile(invertIfCondition), fixSplitLines: singleFile(splitLines), diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 3356a7db43a..80c47470215 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -7,12 +7,12 @@ package golang import ( "bytes" "context" - "encoding/json" "fmt" "go/ast" "go/constant" "go/doc" "go/format" + "go/printer" "go/token" "go/types" "go/version" @@ -37,7 +37,6 @@ 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/typesutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/tokeninternal" @@ -45,50 +44,41 @@ import ( "golang.org/x/tools/internal/typesinternal" ) -// hoverJSON contains the structured result of a hover query. It is -// formatted in one of several formats as determined by the HoverKind -// setting, one of which is JSON. -// -// We believe this is used only by govim. -// TODO(adonovan): see if we can wean all clients of this interface. -type hoverJSON struct { - // Synopsis is a single sentence synopsis of the symbol's documentation. +// hoverResult contains the (internal) result of a hover query. +// It is formatted in one of several formats as determined by the +// HoverKind setting. +type hoverResult struct { + // synopsis is a single sentence synopsis of the symbol's documentation. // - // TODO(adonovan): in what syntax? It (usually) comes from doc.Synopsis, + // TODO(adonovan): in what syntax? It (usually) comes from doc.synopsis, // which produces "Text" form, but it may be fed to // DocCommentToMarkdown, which expects doc comment syntax. - Synopsis string `json:"synopsis"` + synopsis string - // FullDocumentation is the symbol's full documentation. - FullDocumentation string `json:"fullDocumentation"` + // fullDocumentation is the symbol's full documentation. + fullDocumentation string - // Signature is the symbol's signature. - Signature string `json:"signature"` + // signature is the symbol's signature. + signature string - // SingleLine is a single line describing the symbol. + // singleLine is a single line describing the symbol. // This is recommended only for use in clients that show a single line for hover. - SingleLine string `json:"singleLine"` + singleLine string - // SymbolName is the human-readable name to use for the symbol in links. - SymbolName string `json:"symbolName"` + // symbolName is the human-readable name to use for the symbol in links. + symbolName string - // LinkPath is the path of the package enclosing the given symbol, + // linkPath is the path of the package enclosing the given symbol, // with the module portion (if any) replaced by "module@version". // // For example: "github.com/google/go-github/v48@v48.1.0/github". // - // Use LinkTarget + "/" + LinkPath + "#" + LinkAnchor to form a pkgsite URL. - LinkPath string `json:"linkPath"` + // Use LinkTarget + "/" + linkPath + "#" + LinkAnchor to form a pkgsite URL. + linkPath string - // LinkAnchor is the pkg.go.dev link anchor for the given symbol. + // linkAnchor is the pkg.go.dev link anchor for the given symbol. // For example, the "Node" part of "pkg.go.dev/go/ast#Node". - LinkAnchor string `json:"linkAnchor"` - - // New fields go below, and are unexported. The existing - // exported fields are underspecified and have already - // constrained our movements too much. A detailed JSON - // interface might be nice, but it needs a design and a - // precise specification. + linkAnchor string // typeDecl is the declaration syntax for a type, // or "" for a non-type. @@ -142,7 +132,7 @@ func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, positi // if the position is valid but we fail to compute hover information. // // TODO(adonovan): strength-reduce file.Handle to protocol.DocumentURI. -func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) (protocol.Range, *hoverJSON, error) { +func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) (protocol.Range, *hoverResult, error) { // Check for hover inside the builtin file before attempting type checking // below. NarrowestPackageForFile may or may not succeed, depending on // whether this is a GOROOT view, but even if it does succeed the resulting @@ -241,20 +231,25 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro // identifier. for _, spec := range pgf.File.Imports { if gastutil.NodeContains(spec, pos) { - rng, hoverJSON, err := hoverImport(ctx, snapshot, pkg, pgf, spec) + rng, hoverRes, err := hoverImport(ctx, snapshot, pkg, pgf, spec) if err != nil { return protocol.Range{}, nil, err } if hoverRange == nil { hoverRange = &rng } - return *hoverRange, hoverJSON, nil // (hoverJSON may be nil) + return *hoverRange, hoverRes, nil // (hoverRes may be nil) } } - // Handle hovering over (non-import-path) literals. + + // Handle hovering over various special kinds of syntax node. if path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos); len(path) > 0 { - if lit, _ := path[0].(*ast.BasicLit); lit != nil { - return hoverLit(pgf, lit, pos) + switch node := path[0].(type) { + // Handle hovering over (non-import-path) literals. + case *ast.BasicLit: + return hoverLit(pgf, node, pos) + case *ast.ReturnStmt: + return hoverReturnStatement(pgf, path, node) } } @@ -278,7 +273,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro // By convention, we qualify hover information relative to the package // from which the request originated. - qf := typesutil.FileQualifier(pgf.File, pkg.Types(), pkg.TypesInfo()) + qual := typesinternal.FileQualifier(pgf.File, pkg.Types()) // Handle type switch identifiers as a special case, since they don't have an // object. @@ -286,11 +281,11 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro // There's not much useful information to provide. if selectedType != nil { fakeObj := types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), selectedType) - signature := types.ObjectString(fakeObj, qf) - return *hoverRange, &hoverJSON{ - Signature: signature, - SingleLine: signature, - SymbolName: fakeObj.Name(), + signature := types.ObjectString(fakeObj, qual) + return *hoverRange, &hoverResult{ + signature: signature, + singleLine: signature, + symbolName: fakeObj.Name(), }, nil } @@ -313,7 +308,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro docText := comment.Text() // By default, types.ObjectString provides a reasonable signature. - signature := objectString(obj, qf, declPos, declPGF.Tok, spec) + signature := objectString(obj, qual, declPos, declPGF.Tok, spec) singleLineSignature := signature // Display struct tag for struct fields at the end of the signature. @@ -324,7 +319,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro // TODO(rfindley): we could do much better for inferred signatures. // TODO(adonovan): fuse the two calls below. if inferred := inferredSignature(pkg.TypesInfo(), ident); inferred != nil { - if s := inferredSignatureString(obj, qf, inferred); s != "" { + if s := inferredSignatureString(obj, qual, inferred); s != "" { signature = s } } @@ -457,7 +452,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro for _, f := range prom { fmt.Fprintf(w, "%s\t%s\t// through %s\t\n", f.field.Name(), - types.TypeString(f.field.Type(), qf), + types.TypeString(f.field.Type(), qual), f.path) } w.Flush() @@ -501,7 +496,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro } // Use objectString for its prettier rendering of method receivers. - b.WriteString(objectString(m.Obj(), qf, token.NoPos, nil, nil)) + b.WriteString(objectString(m.Obj(), qual, token.NoPos, nil, nil)) } methods = b.String() @@ -618,14 +613,14 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro footer = fmt.Sprintf("Added in %v", sym.Version) } - return *hoverRange, &hoverJSON{ - Synopsis: doc.Synopsis(docText), - FullDocumentation: docText, - SingleLine: singleLineSignature, - SymbolName: linkName, - Signature: signature, - LinkPath: linkPath, - LinkAnchor: anchor, + return *hoverRange, &hoverResult{ + synopsis: doc.Synopsis(docText), + fullDocumentation: docText, + singleLine: singleLineSignature, + symbolName: linkName, + signature: signature, + linkPath: linkPath, + linkAnchor: anchor, typeDecl: typeDecl, methods: methods, promotedFields: fields, @@ -635,15 +630,15 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro // hoverBuiltin computes hover information when hovering over a builtin // identifier. -func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*hoverJSON, error) { +func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*hoverResult, error) { // Special handling for error.Error, which is the only builtin method. // // TODO(rfindley): can this be unified with the handling below? if obj.Name() == "Error" { signature := obj.String() - return &hoverJSON{ - Signature: signature, - SingleLine: signature, + return &hoverResult{ + signature: signature, + singleLine: signature, // TODO(rfindley): these are better than the current behavior. // SymbolName: "(error).Error", // LinkPath: "builtin", @@ -685,14 +680,14 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec signature = replacer.Replace(signature) docText := comment.Text() - return &hoverJSON{ - Synopsis: doc.Synopsis(docText), - FullDocumentation: docText, - Signature: signature, - SingleLine: obj.String(), - SymbolName: obj.Name(), - LinkPath: "builtin", - LinkAnchor: obj.Name(), + return &hoverResult{ + synopsis: doc.Synopsis(docText), + fullDocumentation: docText, + signature: signature, + singleLine: obj.String(), + symbolName: obj.Name(), + linkPath: "builtin", + linkAnchor: obj.Name(), }, nil } @@ -700,7 +695,7 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec // imp in the file pgf of pkg. // // If we do not have metadata for the hovered import, it returns _ -func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, imp *ast.ImportSpec) (protocol.Range, *hoverJSON, error) { +func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, imp *ast.ImportSpec) (protocol.Range, *hoverResult, error) { rng, err := pgf.NodeRange(imp.Path) if err != nil { return protocol.Range{}, nil, err @@ -743,16 +738,16 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa } docText := comment.Text() - return rng, &hoverJSON{ - Signature: "package " + string(impMetadata.Name), - Synopsis: doc.Synopsis(docText), - FullDocumentation: docText, + return rng, &hoverResult{ + signature: "package " + string(impMetadata.Name), + synopsis: doc.Synopsis(docText), + fullDocumentation: docText, }, nil } // hoverPackageName computes hover information for the package name of the file // pgf in pkg. -func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *hoverJSON, error) { +func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *hoverResult, error) { var comment *ast.CommentGroup for _, pgf := range pkg.CompiledGoFiles() { if pgf.File.Doc != nil { @@ -801,10 +796,10 @@ func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *h footer += fmt.Sprintf(" - %s: %s", attr.title, attr.value) } - return rng, &hoverJSON{ - Signature: "package " + string(pkg.Metadata().Name), - Synopsis: doc.Synopsis(docText), - FullDocumentation: docText, + return rng, &hoverResult{ + signature: "package " + string(pkg.Metadata().Name), + synopsis: doc.Synopsis(docText), + fullDocumentation: docText, footer: footer, }, nil } @@ -816,7 +811,7 @@ func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *h // For example, hovering over "\u2211" in "foo \u2211 bar" yields: // // '∑', U+2211, N-ARY SUMMATION -func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverJSON, error) { +func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverResult, error) { var ( value string // if non-empty, a constant value to format in hover r rune // if non-zero, format a description of this rune in hover @@ -929,15 +924,54 @@ func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Ran fmt.Fprintf(&b, "U+%04X, %s", r, runeName) } hover := b.String() - return rng, &hoverJSON{ - Synopsis: hover, - FullDocumentation: hover, + return rng, &hoverResult{ + synopsis: hover, + fullDocumentation: hover, + }, nil +} + +func hoverReturnStatement(pgf *parsego.File, path []ast.Node, ret *ast.ReturnStmt) (protocol.Range, *hoverResult, error) { + var funcType *ast.FuncType + // Find innermost enclosing function. + for _, n := range path { + switch n := n.(type) { + case *ast.FuncLit: + funcType = n.Type + case *ast.FuncDecl: + funcType = n.Type + } + if funcType != nil { + break + } + } + // Inv: funcType != nil because a ReturnStmt is always enclosed by a function. + if funcType.Results == nil { + return protocol.Range{}, nil, nil // no result variables + } + rng, err := pgf.PosRange(ret.Pos(), ret.End()) + if err != nil { + return protocol.Range{}, nil, err + } + // Format the function's result type. + var buf strings.Builder + var cfg printer.Config + fset := token.NewFileSet() + buf.WriteString("returns (") + for i, field := range funcType.Results.List { + if i > 0 { + buf.WriteString(", ") + } + cfg.Fprint(&buf, fset, field.Type) + } + buf.WriteByte(')') + return rng, &hoverResult{ + signature: buf.String(), }, nil } // hoverEmbed computes hover information for a filepath.Match pattern. // Assumes that the pattern is relative to the location of fh. -func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Range, *hoverJSON, error) { +func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Range, *hoverResult, error) { s := &strings.Builder{} dir := fh.URI().DirPath() @@ -969,30 +1003,30 @@ func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Ra fmt.Fprintf(s, "%s\n\n", m) } - json := &hoverJSON{ - Signature: fmt.Sprintf("Embedding %q", pattern), - Synopsis: s.String(), - FullDocumentation: s.String(), + res := &hoverResult{ + signature: fmt.Sprintf("Embedding %q", pattern), + synopsis: s.String(), + fullDocumentation: s.String(), } - return rng, json, nil + return rng, res, nil } // inferredSignatureString is a wrapper around the types.ObjectString function // that adds more information to inferred signatures. It will return an empty string // if the passed types.Object is not a signature. -func inferredSignatureString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string { +func inferredSignatureString(obj types.Object, qual types.Qualifier, inferred *types.Signature) string { // If the signature type was inferred, prefer the inferred signature with a // comment showing the generic signature. if sig, _ := obj.Type().Underlying().(*types.Signature); sig != nil && sig.TypeParams().Len() > 0 && inferred != nil { obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred) - str := types.ObjectString(obj2, qf) + str := types.ObjectString(obj2, qual) // Try to avoid overly long lines. if len(str) > 60 { str += "\n" } else { str += " " } - str += "// " + types.TypeString(sig, qf) + str += "// " + types.TypeString(sig, qual) return str } return "" @@ -1004,8 +1038,8 @@ func inferredSignatureString(obj types.Object, qf types.Qualifier, inferred *typ // syntax, and file must be the token.File describing its positions. // // Precondition: obj is not a built-in function or method. -func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file *token.File, spec ast.Spec) string { - str := types.ObjectString(obj, qf) +func objectString(obj types.Object, qual types.Qualifier, declPos token.Pos, file *token.File, spec ast.Spec) string { + str := types.ObjectString(obj, qual) switch obj := obj.(type) { case *types.Func: @@ -1032,16 +1066,16 @@ func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file buf.WriteString(name) buf.WriteString(" ") } - types.WriteType(&buf, recv.Type(), qf) + types.WriteType(&buf, recv.Type(), qual) } buf.WriteByte(')') buf.WriteByte(' ') // space (go/types uses a period) - } else if s := qf(obj.Pkg()); s != "" { + } else if s := qual(obj.Pkg()); s != "" { buf.WriteString(s) buf.WriteString(".") } buf.WriteString(obj.Name()) - types.WriteSignature(&buf, sig, qf) + types.WriteSignature(&buf, sig, qual) str = buf.String() case *types.Const: @@ -1196,7 +1230,7 @@ func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSe } // If pkgURL is non-nil, it should be used to generate doc links. -func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) (string, error) { +func formatHover(h *hoverResult, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) (string, error) { markdown := options.PreferredContentFormat == protocol.Markdown maybeFenced := func(s string) string { if s != "" && markdown { @@ -1207,17 +1241,10 @@ func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path Packa switch options.HoverKind { case settings.SingleLine: - return h.SingleLine, nil + return h.singleLine, nil case settings.NoDocumentation: - return maybeFenced(h.Signature), nil - - case settings.Structured: - b, err := json.Marshal(h) - if err != nil { - return "", err - } - return string(b), nil + return maybeFenced(h.signature), nil case settings.SynopsisDocumentation, settings.FullDocumentation: var sections [][]string // assembled below @@ -1228,20 +1255,20 @@ func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path Packa // but not Signature, which is redundant (= TypeDecl + "\n" + Methods). // For all other symbols, we display Signature; // TypeDecl and Methods are empty. - // (This awkwardness is to preserve JSON compatibility.) + // (Now that JSON is no more, we could rationalize this.) if h.typeDecl != "" { sections = append(sections, []string{maybeFenced(h.typeDecl)}) } else { - sections = append(sections, []string{maybeFenced(h.Signature)}) + sections = append(sections, []string{maybeFenced(h.signature)}) } // Doc section. var doc string switch options.HoverKind { case settings.SynopsisDocumentation: - doc = h.Synopsis + doc = h.synopsis case settings.FullDocumentation: - doc = h.FullDocumentation + doc = h.fullDocumentation } if options.PreferredContentFormat == protocol.Markdown { doc = DocCommentToMarkdown(doc, options) @@ -1321,7 +1348,7 @@ func StdSymbolOf(obj types.Object) *stdlib.Symbol { // Handle Method. if fn, _ := obj.(*types.Func); fn != nil { isPtr, named := typesinternal.ReceiverNamed(fn.Signature().Recv()) - if isPackageLevel(named.Obj()) { + if named != nil && isPackageLevel(named.Obj()) { for _, s := range symbols { if s.Kind != stdlib.Method { continue @@ -1363,35 +1390,35 @@ func StdSymbolOf(obj types.Object) *stdlib.Symbol { } // If pkgURL is non-nil, it should be used to generate doc links. -func formatLink(h *hoverJSON, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) string { - if options.LinksInHover == settings.LinksInHover_None || h.LinkPath == "" { +func formatLink(h *hoverResult, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) string { + if options.LinksInHover == settings.LinksInHover_None || h.linkPath == "" { return "" } var url protocol.URI var caption string if pkgURL != nil { // LinksInHover == "gopls" // Discard optional module version portion. - // (Ideally the hoverJSON would retain the structure...) - path := h.LinkPath - if module, versionDir, ok := strings.Cut(h.LinkPath, "@"); ok { + // (Ideally the hoverResult would retain the structure...) + path := h.linkPath + if module, versionDir, ok := strings.Cut(h.linkPath, "@"); ok { // "module@version/dir" path = module if _, dir, ok := strings.Cut(versionDir, "/"); ok { path += "/" + dir } } - url = pkgURL(PackagePath(path), h.LinkAnchor) + url = pkgURL(PackagePath(path), h.linkAnchor) caption = "in gopls doc viewer" } else { if options.LinkTarget == "" { return "" } - url = cache.BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor) + url = cache.BuildLink(options.LinkTarget, h.linkPath, h.linkAnchor) caption = "on " + options.LinkTarget } switch options.PreferredContentFormat { case protocol.Markdown: - return fmt.Sprintf("[`%s` %s](%s)", h.SymbolName, caption, url) + return fmt.Sprintf("[`%s` %s](%s)", h.symbolName, caption, url) case protocol.PlainText: return "" default: diff --git a/gopls/internal/golang/implementation.go b/gopls/internal/golang/implementation.go index b3accff452f..fe0a34a1c80 100644 --- a/gopls/internal/golang/implementation.go +++ b/gopls/internal/golang/implementation.go @@ -17,6 +17,7 @@ import ( "sync" "golang.org/x/sync/errgroup" + "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" @@ -120,19 +121,19 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand // (For methods, report the corresponding method names.) // // This logic is reused for local queries. - typeOrMethod := func(obj types.Object) (types.Type, string) { + typeOrMethod := func(obj types.Object) (types.Type, *types.Func) { switch obj := obj.(type) { case *types.TypeName: - return obj.Type(), "" + return obj.Type(), nil case *types.Func: // For methods, use the receiver type, which may be anonymous. if recv := obj.Signature().Recv(); recv != nil { - return recv.Type(), obj.Id() + return recv.Type(), obj } } - return nil, "" + return nil, nil } - queryType, queryMethodID := typeOrMethod(obj) + queryType, queryMethod := typeOrMethod(obj) if queryType == nil { return nil, bug.Errorf("%s is not a type or method", obj.Name()) // should have been handled by implementsObj } @@ -211,13 +212,13 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand if !ok { return ErrNoIdentFound // checked earlier } - // Shadow obj, queryType, and queryMethodID in this package. + // Shadow obj, queryType, and queryMethod in this package. obj := declPkg.TypesInfo().ObjectOf(id) // may be nil - queryType, queryMethodID := typeOrMethod(obj) + queryType, queryMethod := typeOrMethod(obj) if queryType == nil { return fmt.Errorf("querying method sets in package %q: %v", pkgID, err) } - localLocs, err := localImplementations(ctx, snapshot, declPkg, queryType, queryMethodID) + localLocs, err := localImplementations(ctx, snapshot, declPkg, queryType, queryMethod) if err != nil { return fmt.Errorf("querying local implementations %q: %v", pkgID, err) } @@ -231,7 +232,7 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand for _, index := range indexes { index := index group.Go(func() error { - for _, res := range index.Search(key, queryMethodID) { + for _, res := range index.Search(key, queryMethod) { loc := res.Location // Map offsets to protocol.Locations in parallel (may involve I/O). group.Go(func() error { @@ -335,16 +336,18 @@ func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.D // types that are assignable to/from the query type, and returns a new // unordered array of their locations. // -// If methodID is non-empty, the function instead returns the location +// If method is non-nil, the function instead returns the location // of each type's method (if any) of that ID. // // ("Local" refers to the search within the same package, but this // function's results may include type declarations that are local to // a function body. The global search index excludes such types // because reliably naming such types is hard.) -func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, queryType types.Type, methodID string) ([]protocol.Location, error) { +func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, queryType types.Type, method *types.Func) ([]protocol.Location, error) { queryType = methodsets.EnsurePointer(queryType) + var msets typeutil.MethodSetCache + // Scan through all type declarations in the syntax. var locs []protocol.Location var methodLocs []methodsets.Location @@ -366,21 +369,21 @@ func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *ca // The historical behavior enshrined by this // function rejects cases where both are // (nontrivial) interface types? - // That seems like useful information. + // That seems like useful information; see #68641. // TODO(adonovan): UX: report I/I pairs too? // The same question appears in the global algorithm (methodsets). - if !concreteImplementsIntf(candidateType, queryType) { + if !concreteImplementsIntf(&msets, candidateType, queryType) { return true // not assignable } // Ignore types with empty method sets. // (No point reporting that every type satisfies 'any'.) - mset := types.NewMethodSet(candidateType) + mset := msets.MethodSet(candidateType) if mset.Len() == 0 { return true } - if methodID == "" { + if method == nil { // Found matching type. locs = append(locs, mustLocation(pgf, spec.Name)) return true @@ -393,13 +396,13 @@ func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *ca // We could recursively search pkg.Imports for it, // but it's easier to walk the method set. for i := 0; i < mset.Len(); i++ { - method := mset.At(i).Obj() - if method.Id() == methodID { - posn := safetoken.StartPosition(pkg.FileSet(), method.Pos()) + m := mset.At(i).Obj() + if m.Id() == method.Id() { + posn := safetoken.StartPosition(pkg.FileSet(), m.Pos()) methodLocs = append(methodLocs, methodsets.Location{ Filename: posn.Filename, Start: posn.Offset, - End: posn.Offset + len(method.Name()), + End: posn.Offset + len(m.Name()), }) break } @@ -449,28 +452,173 @@ func errorLocation(ctx context.Context, snapshot *cache.Snapshot) (protocol.Loca return protocol.Location{}, fmt.Errorf("built-in error type not found") } -// concreteImplementsIntf returns true if a is an interface type implemented by -// concrete type b, or vice versa. -func concreteImplementsIntf(a, b types.Type) bool { - aIsIntf, bIsIntf := types.IsInterface(a), types.IsInterface(b) +// concreteImplementsIntf reports whether x is an interface type +// implemented by concrete type y, or vice versa. +// +// If one or both types are generic, the result indicates whether the +// interface may be implemented under some instantiation. +func concreteImplementsIntf(msets *typeutil.MethodSetCache, x, y types.Type) bool { + xiface := types.IsInterface(x) + yiface := types.IsInterface(y) // Make sure exactly one is an interface type. - if aIsIntf == bIsIntf { + // TODO(adonovan): rescind this policy choice and report + // I/I relationships. See CL 619719 + issue #68641. + if xiface == yiface { return false } - // Rearrange if needed so "a" is the concrete type. - if aIsIntf { - a, b = b, a + // Rearrange if needed so x is the concrete type. + if xiface { + x, y = y, x } + // Inv: y is an interface type. + + // For each interface method of y, check that x has it too. + // It is not necessary to compute x's complete method set. + // + // If y is a constraint interface (!y.IsMethodSet()), we + // ignore non-interface terms, leading to occasional spurious + // matches. We could in future filter based on them, but it + // would lead to divergence with the global (fingerprint-based) + // algorithm, which operates only on methodsets. + ymset := msets.MethodSet(y) + for i := range ymset.Len() { + ym := ymset.At(i).Obj().(*types.Func) + + xobj, _, _ := types.LookupFieldOrMethod(x, false, ym.Pkg(), ym.Name()) + xm, ok := xobj.(*types.Func) + if !ok { + return false // x lacks a method of y + } + if !unify(xm.Signature(), ym.Signature()) { + return false // signatures do not match + } + } + return true // all methods found +} + +// unify reports whether the types of x and y match, allowing free +// type parameters to stand for anything at all, without regard to +// consistency of substitutions. +// +// TODO(adonovan): implement proper unification (#63982), finding the +// most general unifier across all the interface methods. +// +// See also: unify in cache/methodsets/fingerprint, which uses a +// similar ersatz unification approach on type fingerprints, for +// the global index. +func unify(x, y types.Type) bool { + x = types.Unalias(x) + y = types.Unalias(y) + + // For now, allow a type parameter to match anything, + // without regard to consistency of substitutions. + if is[*types.TypeParam](x) || is[*types.TypeParam](y) { + return true + } + + if reflect.TypeOf(x) != reflect.TypeOf(y) { + return false // mismatched types + } + + switch x := x.(type) { + case *types.Array: + y := y.(*types.Array) + return x.Len() == y.Len() && + unify(x.Elem(), y.Elem()) + + case *types.Basic: + y := y.(*types.Basic) + return x.Kind() == y.Kind() + + case *types.Chan: + y := y.(*types.Chan) + return x.Dir() == y.Dir() && + unify(x.Elem(), y.Elem()) + + case *types.Interface: + y := y.(*types.Interface) + // TODO(adonovan): fix: for correctness, we must check + // that both interfaces have the same set of methods + // modulo type parameters, while avoiding the risk of + // unbounded interface recursion. + // + // Since non-empty interface literals are vanishingly + // rare in methods signatures, we ignore this for now. + // If more precision is needed we could compare method + // names and arities, still without full recursion. + return x.NumMethods() == y.NumMethods() + + case *types.Map: + y := y.(*types.Map) + return unify(x.Key(), y.Key()) && + unify(x.Elem(), y.Elem()) + + case *types.Named: + y := y.(*types.Named) + if x.Origin() != y.Origin() { + return false // different named types + } + xtargs := x.TypeArgs() + ytargs := y.TypeArgs() + if xtargs.Len() != ytargs.Len() { + return false // arity error (ill-typed) + } + for i := range xtargs.Len() { + if !unify(xtargs.At(i), ytargs.At(i)) { + return false // mismatched type args + } + } + return true + + case *types.Pointer: + y := y.(*types.Pointer) + return unify(x.Elem(), y.Elem()) + + case *types.Signature: + y := y.(*types.Signature) + return x.Variadic() == y.Variadic() && + unify(x.Params(), y.Params()) && + unify(x.Results(), y.Results()) - // TODO(adonovan): this should really use GenericAssignableTo - // to report (e.g.) "ArrayList[T] implements List[T]", but - // GenericAssignableTo doesn't work correctly on pointers to - // generic named types. Thus the legacy implementation and the - // "local" part of implementations fail to report generics. - // The global algorithm based on subsets does the right thing. - return types.AssignableTo(a, b) + case *types.Slice: + y := y.(*types.Slice) + return unify(x.Elem(), y.Elem()) + + case *types.Struct: + y := y.(*types.Struct) + if x.NumFields() != y.NumFields() { + return false + } + for i := range x.NumFields() { + xf := x.Field(i) + yf := y.Field(i) + if xf.Embedded() != yf.Embedded() || + xf.Name() != yf.Name() || + x.Tag(i) != y.Tag(i) || + !xf.Exported() && xf.Pkg() != yf.Pkg() || + !unify(xf.Type(), yf.Type()) { + return false + } + } + return true + + case *types.Tuple: + y := y.(*types.Tuple) + if x.Len() != y.Len() { + return false + } + for i := range x.Len() { + if !unify(x.At(i).Type(), y.At(i).Type()) { + return false + } + } + return true + + default: // incl. *Union, *TypeParam + panic(fmt.Sprintf("unexpected Type %#v", x)) + } } var ( diff --git a/gopls/internal/golang/inlay_hint.go b/gopls/internal/golang/inlay_hint.go index 478843fac98..bc85745cb0b 100644 --- a/gopls/internal/golang/inlay_hint.go +++ b/gopls/internal/golang/inlay_hint.go @@ -17,7 +17,6 @@ import ( "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/typesutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" @@ -48,7 +47,7 @@ func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pR } info := pkg.TypesInfo() - q := typesutil.FileQualifier(pgf.File, pkg.Types(), info) + q := typesinternal.FileQualifier(pgf.File, pkg.Types()) // Set the range to the full file if the range is not valid. start, end := pgf.File.FileStart, pgf.File.FileEnd diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go index 3df2019a1f9..8050937a88b 100644 --- a/gopls/internal/golang/pkgdoc.go +++ b/gopls/internal/golang/pkgdoc.go @@ -270,6 +270,13 @@ type Web interface { // The posURL function returns a URL that when visited, has the side // effect of causing gopls to direct the client editor to navigate to // the specified file/line/column position, in UTF-8 coordinates. +// +// TODO(adonovan): this function could use some unit tests; we +// shouldn't have to use integration tests to cover microdetails of +// HTML rendering. (It is tempting to abstract this function so that +// it depends only on FileSet/File/Types/TypeInfo/etc, but we should +// bend the tests to the production interfaces, not the other way +// around.) func PackageDocHTML(viewID string, pkg *cache.Package, web Web) ([]byte, error) { // We can't use doc.NewFromFiles (even with doc.PreserveAST // mode) as it calls ast.NewPackage which assumes that each diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go index 6679b45df6b..3ecaab6e3e1 100644 --- a/gopls/internal/golang/references.go +++ b/gopls/internal/golang/references.go @@ -25,6 +25,7 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" @@ -524,7 +525,7 @@ func expandMethodSearch(ctx context.Context, snapshot *cache.Snapshot, workspace index := index group.Go(func() error { // Consult index for matching methods. - results := index.Search(key, method.Name()) + results := index.Search(key, method) if len(results) == 0 { return nil } @@ -579,6 +580,8 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo } } + var msets typeutil.MethodSetCache + // matches reports whether obj either is or corresponds to a target. // (Correspondence is defined as usual for interface methods.) matches := func(obj types.Object) bool { @@ -588,7 +591,7 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo if methodRecvs != nil && obj.Name() == methodName { if orecv := effectiveReceiver(obj); orecv != nil { for _, mrecv := range methodRecvs { - if concreteImplementsIntf(orecv, mrecv) { + if concreteImplementsIntf(&msets, orecv, mrecv) { return true } } diff --git a/gopls/internal/golang/signature_help.go b/gopls/internal/golang/signature_help.go index 6680a14378c..2211a45de61 100644 --- a/gopls/internal/golang/signature_help.go +++ b/gopls/internal/golang/signature_help.go @@ -18,8 +18,8 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/typesinternal" ) // SignatureHelp returns information about the signature of the innermost @@ -105,7 +105,7 @@ loop: } // Inv: sig != nil - qf := typesutil.FileQualifier(pgf.File, pkg.Types(), info) + qual := typesinternal.FileQualifier(pgf.File, pkg.Types()) // Get the object representing the function, if available. // There is no object in certain cases such as calling a function returned by @@ -156,7 +156,7 @@ loop: name = "func" } mq := MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()) - s, err := NewSignature(ctx, snapshot, pkg, sig, comment, qf, mq) + s, err := NewSignature(ctx, snapshot, pkg, sig, comment, qual, mq) if err != nil { return nil, 0, err } diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go index 036c1f959eb..a04a82988c5 100644 --- a/gopls/internal/golang/stub.go +++ b/gopls/internal/golang/stub.go @@ -84,7 +84,7 @@ func insertDeclsAfter(ctx context.Context, snapshot *cache.Snapshot, mp *metadat } // Build import environment for the declaring file. - // (typesutil.FileQualifier works only for complete + // (typesinternal.FileQualifier works only for complete // import mappings, and requires types.) importEnv := make(map[ImportPath]string) // value is local name for _, imp := range declPGF.File.Imports { diff --git a/gopls/internal/golang/types_format.go b/gopls/internal/golang/types_format.go index 55abb06a0ea..5bc5667cc7c 100644 --- a/gopls/internal/golang/types_format.go +++ b/gopls/internal/golang/types_format.go @@ -25,7 +25,7 @@ import ( ) // FormatType returns the detail and kind for a types.Type. -func FormatType(typ types.Type, qf types.Qualifier) (detail string, kind protocol.CompletionItemKind) { +func FormatType(typ types.Type, qual types.Qualifier) (detail string, kind protocol.CompletionItemKind) { typ = typ.Underlying() if types.IsInterface(typ) { detail = "interface{...}" @@ -34,7 +34,7 @@ func FormatType(typ types.Type, qf types.Qualifier) (detail string, kind protoco detail = "struct{...}" kind = protocol.StructCompletion } else { - detail = types.TypeString(typ, qf) + detail = types.TypeString(typ, qual) kind = protocol.ClassCompletion } return detail, kind @@ -178,7 +178,7 @@ func formatFieldList(ctx context.Context, fset *token.FileSet, list *ast.FieldLi } // NewSignature returns formatted signature for a types.Signature struct. -func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, sig *types.Signature, comment *ast.CommentGroup, qf types.Qualifier, mq MetadataQualifier) (*signature, error) { +func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, sig *types.Signature, comment *ast.CommentGroup, qual types.Qualifier, mq MetadataQualifier) (*signature, error) { var tparams []string tpList := sig.TypeParams() for i := 0; i < tpList.Len(); i++ { @@ -191,7 +191,7 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si params := make([]string, 0, sig.Params().Len()) for i := 0; i < sig.Params().Len(); i++ { el := sig.Params().At(i) - typ, err := FormatVarType(ctx, s, pkg, el, qf, mq) + typ, err := FormatVarType(ctx, s, pkg, el, qual, mq) if err != nil { return nil, err } @@ -212,7 +212,7 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si needResultParens = true } el := sig.Results().At(i) - typ, err := FormatVarType(ctx, s, pkg, el, qf, mq) + typ, err := FormatVarType(ctx, s, pkg, el, qual, mq) if err != nil { return nil, err } @@ -255,8 +255,8 @@ var invalidTypeString = types.Typ[types.Invalid].String() // // 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) +func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache.Package, obj *types.Var, qual types.Qualifier, mq MetadataQualifier) (string, error) { + typeString := types.TypeString(obj.Type(), qual) // 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+. diff --git a/gopls/internal/golang/undeclared.go b/gopls/internal/golang/undeclared.go index 3d9954639b4..35a5c7a1e57 100644 --- a/gopls/internal/golang/undeclared.go +++ b/gopls/internal/golang/undeclared.go @@ -17,7 +17,6 @@ import ( "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/analysisinternal" "golang.org/x/tools/internal/typesinternal" ) @@ -126,9 +125,9 @@ func CreateUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, return nil, nil, fmt.Errorf("no identifier found") } p, _ := astutil.PathEnclosingInterval(file, firstRef.Pos(), firstRef.Pos()) - insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(p) - if insertBeforeStmt == nil { - return nil, nil, fmt.Errorf("could not locate insertion point") + insertBeforeStmt, err := stmtToInsertVarBefore(p, nil) + if err != nil { + return nil, nil, fmt.Errorf("could not locate insertion point: %v", err) } indent, err := calculateIndentation(content, fset.File(file.FileStart), insertBeforeStmt) if err != nil { @@ -139,10 +138,11 @@ func CreateUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, // Default to 0. typs = []types.Type{types.Typ[types.Int]} } + expr, _ := typesinternal.ZeroExpr(typs[0], typesinternal.FileQualifier(file, pkg)) assignStmt := &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent(ident.Name)}, Tok: token.DEFINE, - Rhs: []ast.Expr{typesinternal.ZeroExpr(file, pkg, typs[0])}, + Rhs: []ast.Expr{expr}, } var buf bytes.Buffer if err := format.Node(&buf, fset, assignStmt); err != nil { @@ -282,7 +282,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, } params := &ast.FieldList{} - + qual := typesinternal.FileQualifier(file, pkg) for i, name := range paramNames { if suffix, repeats := nameCounts[name]; repeats { nameCounts[name]++ @@ -306,7 +306,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, Names: []*ast.Ident{ ast.NewIdent(name), }, - Type: typesinternal.TypeExpr(file, pkg, paramTypes[i]), + Type: typesinternal.TypeExpr(paramTypes[i], qual), }) } @@ -314,7 +314,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, retTypes := typesutil.TypesFromContext(info, path[1:], path[1].Pos()) for _, rt := range retTypes { rets.List = append(rets.List, &ast.Field{ - Type: typesinternal.TypeExpr(file, pkg, rt), + Type: typesinternal.TypeExpr(rt, qual), }) } diff --git a/gopls/internal/golang/util.go b/gopls/internal/golang/util.go index be5c7c0a735..23fd3443fac 100644 --- a/gopls/internal/golang/util.go +++ b/gopls/internal/golang/util.go @@ -18,7 +18,6 @@ import ( "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/tokeninternal" @@ -130,31 +129,6 @@ func findFileInDeps(s metadata.Source, mp *metadata.Package, uri protocol.Docume return search(mp) } -// CollectScopes returns all scopes in an ast path, ordered as innermost scope -// first. -// -// TODO(adonovan): move this to golang/completion and simplify to use -// Scopes.Innermost and LookupParent instead. -func CollectScopes(info *types.Info, path []ast.Node, pos token.Pos) []*types.Scope { - // scopes[i], where i y, where false < true. +func boolCompare(x, y bool) int { + return btoi(x) - btoi(y) +} + // AbbreviateVarName returns an abbreviated var name based on the given full // name (which may be a type name, for example). // diff --git a/gopls/internal/golang/workspace_symbol.go b/gopls/internal/golang/workspace_symbol.go index c80174c78fd..feba6081515 100644 --- a/gopls/internal/golang/workspace_symbol.go +++ b/gopls/internal/golang/workspace_symbol.go @@ -5,16 +5,19 @@ package golang import ( + "cmp" "context" "fmt" "path/filepath" "runtime" + "slices" "sort" "strings" "unicode" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/symbols" "golang.org/x/tools/gopls/internal/fuzzy" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" @@ -291,8 +294,8 @@ func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherTyp // Extract symbols from all files. var work []symbolFile var roots []string - seen := make(map[protocol.DocumentURI]bool) - // TODO(adonovan): opt: parallelize this loop? How often is len > 1? + seen := make(map[protocol.DocumentURI]*metadata.Package) // only scan each file once + for _, snapshot := range snapshots { // Use the root view URIs for determining (lexically) // whether a URI is in any open workspace. @@ -303,32 +306,84 @@ func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherTyp filterer := cache.NewFilterer(filters) folder := filepath.ToSlash(folderURI.Path()) - workspaceOnly := true + var ( + mps []*metadata.Package + err error + ) if snapshot.Options().SymbolScope == settings.AllSymbolScope { - workspaceOnly = false + mps, err = snapshot.AllMetadata(ctx) + } else { + mps, err = snapshot.WorkspaceMetadata(ctx) } - symbols, err := snapshot.Symbols(ctx, workspaceOnly) if err != nil { return nil, err } + metadata.RemoveIntermediateTestVariants(&mps) - for uri, syms := range symbols { - norm := filepath.ToSlash(uri.Path()) - nm := strings.TrimPrefix(norm, folder) - if filterer.Disallow(nm) { - continue + // We'll process packages in order to consider candidate symbols. + // + // The order here doesn't matter for correctness, but can affect + // performance: + // - As workspace packages score higher than non-workspace packages, + // sort them first to increase the likelihood that non-workspace + // symbols are skipped. + // - As files can be contained in multiple packages, sort by wider + // packages first, to cover all files with fewer packages. + workspacePackages := snapshot.WorkspacePackages() + slices.SortFunc(mps, func(a, b *metadata.Package) int { + _, aworkspace := workspacePackages.Value(a.ID) + _, bworkspace := workspacePackages.Value(b.ID) + if cmp := boolCompare(aworkspace, bworkspace); cmp != 0 { + return -cmp // workspace packages first } - // Only scan each file once. - if seen[uri] { - continue + return -cmp.Compare(len(a.CompiledGoFiles), len(b.CompiledGoFiles)) // widest first + }) + + // Filter out unneeded mps in place, and collect file<->package + // associations. + var ids []metadata.PackageID + for _, mp := range mps { + used := false + for _, list := range [][]protocol.DocumentURI{mp.GoFiles, mp.CompiledGoFiles} { + for _, uri := range list { + if _, ok := seen[uri]; !ok { + seen[uri] = mp + used = true + } + } } - meta, err := NarrowestMetadataForFile(ctx, snapshot, uri) - if err != nil { - event.Error(ctx, fmt.Sprintf("missing metadata for %q", uri), err) + if used { + mps[len(ids)] = mp + ids = append(ids, mp.ID) + } + } + mps = mps[:len(ids)] + + symbolPkgs, err := snapshot.Symbols(ctx, ids...) + if err != nil { + return nil, err + } + + for i, sp := range symbolPkgs { + if sp == nil { continue } - seen[uri] = true - work = append(work, symbolFile{uri, meta, syms}) + mp := mps[i] + for i, syms := range sp.Symbols { + uri := sp.Files[i] + norm := filepath.ToSlash(uri.Path()) + nm := strings.TrimPrefix(norm, folder) + if filterer.Disallow(nm) { + continue + } + // Only scan each file once. + if seen[uri] != mp { + continue + } + // seen[uri] = true + _, workspace := workspacePackages.Value(mp.ID) + work = append(work, symbolFile{mp, uri, syms, workspace}) + } } } @@ -343,7 +398,7 @@ func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherTyp store := new(symbolStore) // Assign files to workers in round-robin fashion. for j := i; j < len(work); j += nmatchers { - matchFile(store, symbolizer, matcher, roots, work[j]) + matchFile(store, symbolizer, matcher, work[j]) } results <- store }(i) @@ -354,7 +409,9 @@ func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherTyp for i := 0; i < nmatchers; i++ { store := <-results for _, syms := range store.res { - unified.store(syms) + if syms != nil { + unified.store(syms) + } } } return unified.results(), nil @@ -362,16 +419,17 @@ func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherTyp // symbolFile holds symbol information for a single file. type symbolFile struct { - uri protocol.DocumentURI - mp *metadata.Package - syms []cache.Symbol + mp *metadata.Package + uri protocol.DocumentURI + syms []symbols.Symbol + workspace bool } // matchFile scans a symbol file and adds matching symbols to the store. -func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, roots []string, i symbolFile) { +func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, f symbolFile) { space := make([]string, 0, 3) - for _, sym := range i.syms { - symbolParts, score := symbolizer(space, sym.Name, i.mp, matcher) + for _, sym := range f.syms { + symbolParts, score := symbolizer(space, sym.Name, f.mp, matcher) // Check if the score is too low before applying any downranking. if store.tooLow(score) { @@ -404,6 +462,10 @@ func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, r depthFactor = 0.01 ) + // TODO(rfindley): compute this downranking *before* calling the symbolizer + // (which is expensive), so that we can pre-filter candidates whose score + // will always be too low, even with a perfect match. + startWord := true exported := true depth := 0.0 @@ -419,18 +481,8 @@ func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, r } } - // TODO(rfindley): use metadata to determine if the file is in a workspace - // package, rather than this heuristic. - inWorkspace := false - for _, root := range roots { - if strings.HasPrefix(string(i.uri), root) { - inWorkspace = true - break - } - } - // Apply downranking based on workspace position. - if !inWorkspace { + if !f.workspace { score *= nonWorkspaceFactor if !exported { score *= nonWorkspaceUnexportedFactor @@ -447,80 +499,70 @@ func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, r continue } - si := symbolInformation{ - score: score, - symbol: strings.Join(symbolParts, ""), - kind: sym.Kind, - uri: i.uri, - rng: sym.Range, - container: string(i.mp.PkgPath), + si := &scoredSymbol{ + score: score, + info: protocol.SymbolInformation{ + Name: strings.Join(symbolParts, ""), + Kind: sym.Kind, + Location: protocol.Location{ + URI: f.uri, + Range: sym.Range, + }, + ContainerName: string(f.mp.PkgPath), + }, } store.store(si) } } type symbolStore struct { - res [maxSymbols]symbolInformation + res [maxSymbols]*scoredSymbol } // store inserts si into the sorted results, if si has a high enough score. -func (sc *symbolStore) store(si symbolInformation) { - if sc.tooLow(si.score) { +func (sc *symbolStore) store(ss *scoredSymbol) { + if sc.tooLow(ss.score) { return } insertAt := sort.Search(len(sc.res), func(i int) bool { + if sc.res[i] == nil { + return true + } // Sort by score, then symbol length, and finally lexically. - if sc.res[i].score != si.score { - return sc.res[i].score < si.score + if ss.score != sc.res[i].score { + return ss.score > sc.res[i].score } - if len(sc.res[i].symbol) != len(si.symbol) { - return len(sc.res[i].symbol) > len(si.symbol) + if cmp := cmp.Compare(len(ss.info.Name), len(sc.res[i].info.Name)); cmp != 0 { + return cmp < 0 // shortest first } - return sc.res[i].symbol > si.symbol + return ss.info.Name < sc.res[i].info.Name }) if insertAt < len(sc.res)-1 { copy(sc.res[insertAt+1:], sc.res[insertAt:len(sc.res)-1]) } - sc.res[insertAt] = si + sc.res[insertAt] = ss } func (sc *symbolStore) tooLow(score float64) bool { - return score <= sc.res[len(sc.res)-1].score + last := sc.res[len(sc.res)-1] + if last == nil { + return false + } + return score <= last.score } func (sc *symbolStore) results() []protocol.SymbolInformation { var res []protocol.SymbolInformation for _, si := range sc.res { - if si.score <= 0 { + if si == nil || si.score <= 0 { return res } - res = append(res, si.asProtocolSymbolInformation()) + res = append(res, si.info) } return res } -// symbolInformation is a cut-down version of protocol.SymbolInformation that -// allows struct values of this type to be used as map keys. -type symbolInformation struct { - score float64 - symbol string - container string - kind protocol.SymbolKind - uri protocol.DocumentURI - rng protocol.Range -} - -// asProtocolSymbolInformation converts s to a protocol.SymbolInformation value. -// -// TODO: work out how to handle tags if/when they are needed. -func (s symbolInformation) asProtocolSymbolInformation() protocol.SymbolInformation { - return protocol.SymbolInformation{ - Name: s.symbol, - Kind: s.kind, - Location: protocol.Location{ - URI: s.uri, - Range: s.rng, - }, - ContainerName: s.container, - } +type scoredSymbol struct { + score float64 + info protocol.SymbolInformation } diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index 9991c95680e..28a7f44e88f 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go @@ -60,7 +60,6 @@ const ( StopProfile Command = "gopls.stop_profile" Test Command = "gopls.test" Tidy Command = "gopls.tidy" - ToggleGCDetails Command = "gopls.toggle_gc_details" UpdateGoSum Command = "gopls.update_go_sum" UpgradeDependency Command = "gopls.upgrade_dependency" Vendor Command = "gopls.vendor" @@ -106,7 +105,6 @@ var Commands = []Command{ StopProfile, Test, Tidy, - ToggleGCDetails, UpdateGoSum, UpgradeDependency, Vendor, @@ -326,12 +324,6 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.Tidy(ctx, a0) - case ToggleGCDetails: - var a0 URIArg - if err := UnmarshalArgs(params.Arguments, &a0); err != nil { - return nil, err - } - return nil, s.ToggleGCDetails(ctx, a0) case UpdateGoSum: var a0 URIArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -652,14 +644,6 @@ func NewTidyCommand(title string, a0 URIArgs) *protocol.Command { } } -func NewToggleGCDetailsCommand(title string, a0 URIArg) *protocol.Command { - return &protocol.Command{ - Title: title, - Command: ToggleGCDetails.String(), - Arguments: MustMarshalArgs(a0), - } -} - func NewUpdateGoSumCommand(title string, a0 URIArgs) *protocol.Command { return &protocol.Command{ Title: title, diff --git a/gopls/internal/protocol/command/commandmeta/meta.go b/gopls/internal/protocol/command/commandmeta/meta.go index 166fae61e04..f147898e192 100644 --- a/gopls/internal/protocol/command/commandmeta/meta.go +++ b/gopls/internal/protocol/command/commandmeta/meta.go @@ -219,8 +219,8 @@ func lspName(methodName string) string { // // For example: // -// "RunTests" -> []string{"Run", "Tests"} -// "GCDetails" -> []string{"GC", "Details"} +// "RunTests" -> []string{"Run", "Tests"} +// "ClientOpenURL" -> []string{"Client", "Open", "URL"} func splitCamel(s string) []string { var words []string for len(s) > 0 { diff --git a/gopls/internal/protocol/command/gen/gen.go b/gopls/internal/protocol/command/gen/gen.go index d9722902ca9..98155282499 100644 --- a/gopls/internal/protocol/command/gen/gen.go +++ b/gopls/internal/protocol/command/gen/gen.go @@ -110,7 +110,7 @@ func Generate() ([]byte, error) { return nil, fmt.Errorf("loading command data: %v", err) } const thispkg = "golang.org/x/tools/gopls/internal/protocol/command" - qf := func(p *types.Package) string { + qual := func(p *types.Package) string { if p.Path() == thispkg { return "" } @@ -118,7 +118,7 @@ func Generate() ([]byte, error) { } tmpl, err := template.New("").Funcs(template.FuncMap{ "typeString": func(t types.Type) string { - return types.TypeString(t, qf) + return types.TypeString(t, qual) }, "fallible": func(args []*commandmeta.Field) bool { var fallible func(types.Type) bool diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index 0ce3af2aff9..b0e80a4129e 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -132,17 +132,15 @@ type Interface interface { // Runs `go get` to fetch a package. GoGetPackage(context.Context, GoGetPackageArgs) error - // GCDetails: Toggle gc_details + // GCDetails: Toggle display of compiler optimization details // - // Toggle the calculation of gc annotations. - GCDetails(context.Context, protocol.DocumentURI) error - - // TODO: deprecate GCDetails in favor of ToggleGCDetails below. - - // ToggleGCDetails: Toggle gc_details + // Toggle the per-package flag that causes Go compiler + // optimization decisions to be reported as diagnostics. // - // Toggle the calculation of gc annotations. - ToggleGCDetails(context.Context, URIArg) error + // (The name is a legacy of a time when the Go compiler was + // known as "gc". Renaming the command would break custom + // client-side logic in VS Code.) + GCDetails(context.Context, protocol.DocumentURI) error // ListKnownPackages: List known packages // @@ -525,7 +523,7 @@ type RunVulncheckResult struct { Token protocol.ProgressToken } -// GovulncheckResult holds the result of synchronously running the vulncheck +// VulncheckResult holds the result of synchronously running the vulncheck // command. type VulncheckResult struct { // Result holds the result of running vulncheck. @@ -695,7 +693,7 @@ type PackagesResult struct { // Packages is an unordered list of package metadata. Packages []Package - // Modules maps module path to module metadata for + // Module maps module path to module metadata for // all the modules of the returned Packages. Module map[string]Module } diff --git a/gopls/internal/protocol/generate/types.go b/gopls/internal/protocol/generate/types.go index 0537748eb5b..17e9a9a7776 100644 --- a/gopls/internal/protocol/generate/types.go +++ b/gopls/internal/protocol/generate/types.go @@ -112,7 +112,7 @@ type Type struct { Line int `json:"line"` // JSON source line } -// ParsedLiteral is Type.Value when Type.Kind is "literal" +// ParseLiteral is Type.Value when Type.Kind is "literal" type ParseLiteral struct { Properties `json:"properties"` } diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go index 2e1c83f407f..c36e7c33f94 100644 --- a/gopls/internal/server/code_action.go +++ b/gopls/internal/server/code_action.go @@ -192,7 +192,8 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara settings.GoDoc, settings.GoFreeSymbols, settings.GoAssembly, - settings.GoplsDocFeatures: + settings.GoplsDocFeatures, + settings.GoToggleCompilerOptDetails: return false // read-only query } return true // potential write operation @@ -324,14 +325,18 @@ func (s *server) findMatchingDiagnostics(uri protocol.DocumentURI, pd protocol.D defer s.diagnosticsMu.Unlock() var sds []*cache.Diagnostic - for _, viewDiags := range s.diagnostics[uri].byView { - for _, sd := range viewDiags.diagnostics { - sameDiagnostic := (pd.Message == strings.TrimSpace(sd.Message) && // extra space may have been trimmed when converting to protocol.Diagnostic - protocol.CompareRange(pd.Range, sd.Range) == 0 && - pd.Source == string(sd.Source)) + if fileDiags := s.diagnostics[uri]; fileDiags != nil { + for _, viewDiags := range fileDiags.byView { + for _, sd := range viewDiags.diagnostics { + // extra space may have been trimmed when + // converting to protocol.Diagnostic + sameDiagnostic := pd.Message == strings.TrimSpace(sd.Message) && + protocol.CompareRange(pd.Range, sd.Range) == 0 && + pd.Source == string(sd.Source) - if sameDiagnostic { - sds = append(sds, sd) + if sameDiagnostic { + sds = append(sds, sd) + } } } } diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 9995d02117e..e785625655e 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -1025,23 +1025,19 @@ func (s *server) getUpgrades(ctx context.Context, snapshot *cache.Snapshot, uri } func (c *commandHandler) GCDetails(ctx context.Context, uri protocol.DocumentURI) error { - return c.ToggleGCDetails(ctx, command.URIArg{URI: uri}) -} - -func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIArg) error { return c.run(ctx, commandConfig{ - progress: "Toggling GC Details", - forURI: args.URI, + progress: "Toggling display of compiler optimization details", + forURI: uri, }, func(ctx context.Context, deps commandDeps) error { - return c.modifyState(ctx, FromToggleGCDetails, func() (*cache.Snapshot, func(), error) { + return c.modifyState(ctx, FromToggleCompilerOptDetails, func() (*cache.Snapshot, func(), error) { meta, err := golang.NarrowestMetadataForFile(ctx, deps.snapshot, deps.fh.URI()) if err != nil { return nil, nil, err } - wantDetails := !deps.snapshot.WantGCDetails(meta.ID) // toggle the gc details state + want := !deps.snapshot.WantCompilerOptDetails(meta.ID) // toggle per-package flag return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{ - GCDetails: map[metadata.PackageID]bool{ - meta.ID: wantDetails, + CompilerOptDetails: map[metadata.PackageID]bool{ + meta.ID: want, }, }) }) diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go index 22eafaf2d2e..e95bf297501 100644 --- a/gopls/internal/server/diagnostics.go +++ b/gopls/internal/server/diagnostics.go @@ -285,7 +285,7 @@ func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snaps // golang/go#65801: only diagnose changes to workspace packages. Otherwise, // diagnostics will be unstable, as the slow-path diagnostics will erase // them. - if snapshot.IsWorkspacePackage(ctx, meta.ID) { + if snapshot.IsWorkspacePackage(meta.ID) { toDiagnose[meta.ID] = meta } } @@ -433,7 +433,7 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa // For analysis, we use the *widest* package for each open file, // for two reasons: // - // - Correctness: some analyzers (e.g. unusedparam) depend + // - Correctness: some analyzers (e.g. unused{param,func}) depend // on it. If applied to a non-test package for which a // corresponding test package exists, they make assumptions // that are falsified in the test package, for example that @@ -484,8 +484,8 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa wg.Add(1) go func() { defer wg.Done() - gcDetailsReports, err := s.gcDetailsDiagnostics(ctx, snapshot, toDiagnose) - store("collecting gc_details", gcDetailsReports, err) + compilerOptDetailsDiags, err := s.compilerOptDetailsDiagnostics(ctx, snapshot, toDiagnose) + store("collecting compiler optimization details", compilerOptDetailsDiags, err) }() // Package diagnostics and analysis diagnostics must both be computed and @@ -536,30 +536,30 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa return diagnostics, nil } -func (s *server) gcDetailsDiagnostics(ctx context.Context, snapshot *cache.Snapshot, toDiagnose map[metadata.PackageID]*metadata.Package) (diagMap, error) { - // Process requested gc_details diagnostics. +func (s *server) compilerOptDetailsDiagnostics(ctx context.Context, snapshot *cache.Snapshot, toDiagnose map[metadata.PackageID]*metadata.Package) (diagMap, error) { + // Process requested diagnostics about compiler optimization details. // // TODO(rfindley): This should memoize its results if the package has not changed. // Consider that these points, in combination with the note below about - // races, suggest that gc_details should be tracked on the Snapshot. - var toGCDetail map[metadata.PackageID]*metadata.Package + // races, suggest that compiler optimization details should be tracked on the Snapshot. + var detailPkgs map[metadata.PackageID]*metadata.Package for _, mp := range toDiagnose { - if snapshot.WantGCDetails(mp.ID) { - if toGCDetail == nil { - toGCDetail = make(map[metadata.PackageID]*metadata.Package) + if snapshot.WantCompilerOptDetails(mp.ID) { + if detailPkgs == nil { + detailPkgs = make(map[metadata.PackageID]*metadata.Package) } - toGCDetail[mp.ID] = mp + detailPkgs[mp.ID] = mp } } diagnostics := make(diagMap) - for _, mp := range toGCDetail { - gcReports, err := golang.GCOptimizationDetails(ctx, snapshot, mp) + for _, mp := range detailPkgs { + perFileDiags, err := golang.CompilerOptDetails(ctx, snapshot, mp) if err != nil { - event.Error(ctx, "warning: gc details", err, append(snapshot.Labels(), label.Package.Of(string(mp.ID)))...) + event.Error(ctx, "warning: compiler optimization details", err, append(snapshot.Labels(), label.Package.Of(string(mp.ID)))...) continue } - for uri, diags := range gcReports { + for uri, diags := range perFileDiags { diagnostics[uri] = append(diagnostics[uri], diags...) } } diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index 92e9729a0a6..3a3a5efcd70 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go @@ -610,7 +610,7 @@ func (s *server) fileOf(ctx context.Context, uri protocol.DocumentURI) (file.Han return fh, snapshot, release, nil } -// shutdown implements the 'shutdown' LSP handler. It releases resources +// Shutdown implements the 'shutdown' LSP handler. It releases resources // associated with the server and waits for all ongoing work to complete. func (s *server) Shutdown(ctx context.Context) error { ctx, done := event.Start(ctx, "lsp.Server.shutdown") diff --git a/gopls/internal/server/selection_range.go b/gopls/internal/server/selection_range.go index 042812217f3..484e1cf67ab 100644 --- a/gopls/internal/server/selection_range.go +++ b/gopls/internal/server/selection_range.go @@ -15,7 +15,7 @@ import ( "golang.org/x/tools/internal/event" ) -// selectionRange defines the textDocument/selectionRange feature, +// SelectionRange defines the textDocument/selectionRange feature, // which, given a list of positions within a file, // reports a linked list of enclosing syntactic blocks, innermost first. // diff --git a/gopls/internal/server/text_synchronization.go b/gopls/internal/server/text_synchronization.go index 6aef24691d6..ad1266d783e 100644 --- a/gopls/internal/server/text_synchronization.go +++ b/gopls/internal/server/text_synchronization.go @@ -61,9 +61,9 @@ const ( // ResetGoModDiagnostics command. FromResetGoModDiagnostics - // FromToggleGCDetails refers to state changes resulting from toggling - // gc_details on or off for a package. - FromToggleGCDetails + // FromToggleCompilerOptDetails refers to state changes resulting from toggling + // a package's compiler optimization details flag. + FromToggleCompilerOptDetails ) func (m ModificationSource) String() string { diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go index d20526fc583..7e13c801a85 100644 --- a/gopls/internal/settings/analysis.go +++ b/gopls/internal/settings/analysis.go @@ -50,14 +50,15 @@ import ( "golang.org/x/tools/gopls/internal/analysis/embeddirective" "golang.org/x/tools/gopls/internal/analysis/fillreturns" "golang.org/x/tools/gopls/internal/analysis/infertypeargs" + "golang.org/x/tools/gopls/internal/analysis/modernize" "golang.org/x/tools/gopls/internal/analysis/nonewvars" "golang.org/x/tools/gopls/internal/analysis/noresultvalues" "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/unusedfunc" "golang.org/x/tools/gopls/internal/analysis/unusedparams" "golang.org/x/tools/gopls/internal/analysis/unusedvariable" - "golang.org/x/tools/gopls/internal/analysis/useany" "golang.org/x/tools/gopls/internal/analysis/yield" "golang.org/x/tools/gopls/internal/protocol" ) @@ -149,6 +150,7 @@ func init() { // - some (nilness, yield) use go/ssa; see #59714. // - others don't meet the "frequency" criterion; // see GOROOT/src/cmd/vet/README. + // - some (modernize) report diagnostics on perfectly valid code (hence severity=info) {analyzer: atomicalign.Analyzer, enabled: true}, {analyzer: deepequalerrors.Analyzer, enabled: true}, {analyzer: nilness.Analyzer, enabled: true}, // uses go/ssa @@ -156,10 +158,10 @@ func init() { {analyzer: sortslice.Analyzer, enabled: true}, {analyzer: embeddirective.Analyzer, enabled: true}, {analyzer: waitgroup.Analyzer, enabled: true}, // to appear in cmd/vet@go1.25 + {analyzer: modernize.Analyzer, enabled: true, severity: protocol.SeverityInformation}, // disabled due to high false positives {analyzer: shadow.Analyzer, enabled: false}, // very noisy - {analyzer: useany.Analyzer, enabled: false}, // never a bug // fieldalignment is not even off-by-default; see #67762. // "simplifiers": analyzers that offer mere style fixes @@ -170,6 +172,7 @@ func init() { // other simplifiers: {analyzer: infertypeargs.Analyzer, enabled: true, severity: protocol.SeverityHint}, {analyzer: unusedparams.Analyzer, enabled: true}, + {analyzer: unusedfunc.Analyzer, enabled: true}, {analyzer: unusedwrite.Analyzer, enabled: true}, // uses go/ssa // type-error analyzers diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index 7bc4f4e4d66..fcce7cd2682 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go @@ -75,11 +75,12 @@ import "golang.org/x/tools/gopls/internal/protocol" // is not VS Code's default behavior; see editor.codeActionsOnSave.) const ( // source - GoAssembly protocol.CodeActionKind = "source.assembly" - GoDoc protocol.CodeActionKind = "source.doc" - GoFreeSymbols protocol.CodeActionKind = "source.freesymbols" - GoTest protocol.CodeActionKind = "source.test" - AddTest protocol.CodeActionKind = "source.addTest" + GoAssembly protocol.CodeActionKind = "source.assembly" + GoDoc protocol.CodeActionKind = "source.doc" + GoFreeSymbols protocol.CodeActionKind = "source.freesymbols" + GoTest protocol.CodeActionKind = "source.test" + GoToggleCompilerOptDetails protocol.CodeActionKind = "source.toggleCompilerOptDetails" + AddTest protocol.CodeActionKind = "source.addTest" // gopls GoplsDocFeatures protocol.CodeActionKind = "gopls.doc.features" @@ -99,11 +100,13 @@ const ( RefactorInlineCall protocol.CodeActionKind = "refactor.inline.call" // refactor.extract - RefactorExtractConstant protocol.CodeActionKind = "refactor.extract.constant" - RefactorExtractFunction protocol.CodeActionKind = "refactor.extract.function" - RefactorExtractMethod protocol.CodeActionKind = "refactor.extract.method" - RefactorExtractVariable protocol.CodeActionKind = "refactor.extract.variable" - RefactorExtractToNewFile protocol.CodeActionKind = "refactor.extract.toNewFile" + RefactorExtractConstant protocol.CodeActionKind = "refactor.extract.constant" + RefactorExtractConstantAll protocol.CodeActionKind = "refactor.extract.constant-all" + RefactorExtractFunction protocol.CodeActionKind = "refactor.extract.function" + RefactorExtractMethod protocol.CodeActionKind = "refactor.extract.method" + RefactorExtractVariable protocol.CodeActionKind = "refactor.extract.variable" + RefactorExtractVariableAll protocol.CodeActionKind = "refactor.extract.variable-all" + RefactorExtractToNewFile protocol.CodeActionKind = "refactor.extract.toNewFile" // Note: add new kinds to: // - the SupportedCodeActions map in default.go diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index 0354101f045..f9b947b31a8 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -62,9 +62,11 @@ func DefaultOptions(overrides ...func(*Options)) *Options { RefactorRewriteSplitLines: true, RefactorInlineCall: true, RefactorExtractConstant: true, + RefactorExtractConstantAll: true, RefactorExtractFunction: true, RefactorExtractMethod: true, RefactorExtractVariable: true, + RefactorExtractVariableAll: true, RefactorExtractToNewFile: true, // Not GoTest: it must be explicit in CodeActionParams.Context.Only }, @@ -87,12 +89,6 @@ func DefaultOptions(overrides ...func(*Options)) *Options { }, UIOptions: UIOptions{ DiagnosticOptions: DiagnosticOptions{ - Annotations: map[Annotation]bool{ - Bounds: true, - Escape: true, - Inline: true, - Nil: true, - }, Vulncheck: ModeVulncheckOff, DiagnosticsDelay: 1 * time.Second, DiagnosticsTrigger: DiagnosticsOnEdit, @@ -120,7 +116,6 @@ func DefaultOptions(overrides ...func(*Options)) *Options { CodeLensGenerate: true, CodeLensRegenerateCgo: true, CodeLensTidy: true, - CodeLensGCDetails: false, CodeLensUpgradeDependency: true, CodeLensVendor: true, CodeLensRunGovulncheck: false, // TODO(hyangah): enable diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 5f1efef040d..785ebd8b582 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -16,22 +16,6 @@ import ( "golang.org/x/tools/gopls/internal/util/frob" ) -type Annotation string - -const ( - // Nil controls nil checks. - Nil Annotation = "nil" - - // Escape controls diagnostics about escape choices. - Escape Annotation = "escape" - - // Inline controls diagnostics about inlining choices. - Inline Annotation = "inline" - - // Bounds controls bounds checking diagnostics. - Bounds Annotation = "bounds" -) - // Options holds various configuration that affects Gopls execution, organized // by the nature or origin of the settings. // @@ -175,7 +159,6 @@ type UIOptions struct { // ... // "codelenses": { // "generate": false, // Don't show the `go generate` lens. - // "gc_details": true // Show a code lens toggling the display of gc's choices. // } // ... // } @@ -209,25 +192,6 @@ type CodeLensSource string // matches the name of one of the command.Commands returned by it, // but that isn't essential.) const ( - // Toggle display of Go compiler optimization decisions - // - // This codelens source causes the `package` declaration of - // each file to be annotated with a command to toggle the - // state of the per-session variable that controls whether - // optimization decisions from the Go compiler (formerly known - // as "gc") should be displayed as diagnostics. - // - // Optimization decisions include: - // - whether a variable escapes, and how escape is inferred; - // - whether a nil-pointer check is implied or eliminated; - // - whether a function can be inlined. - // - // TODO(adonovan): this source is off by default because the - // annotation is annoying and because VS Code has a separate - // "Toggle gc details" command. Replace it with a Code Action - // ("Source action..."). - CodeLensGCDetails CodeLensSource = "gc_details" - // Run `go generate` // // This codelens source annotates any `//go:generate` comments @@ -346,7 +310,7 @@ type CompletionOptions struct { // Note: DocumentationOptions must be comparable with reflect.DeepEqual. type DocumentationOptions struct { // HoverKind controls the information that appears in the hover text. - // SingleLine and Structured are intended for use only by authors of editor plugins. + // SingleLine is intended for use only by authors of editor plugins. HoverKind HoverKind // LinkTarget is the base URL for links to Go package @@ -442,10 +406,6 @@ type DiagnosticOptions struct { // [Staticcheck's website](https://staticcheck.io/docs/checks/). Staticcheck bool `status:"experimental"` - // Annotations specifies the various kinds of optimization diagnostics - // that should be reported by the gc_details command. - Annotations map[Annotation]bool `status:"experimental"` - // Vulncheck enables vulnerability scanning. Vulncheck VulncheckMode `status:"experimental"` @@ -791,13 +751,6 @@ const ( NoDocumentation HoverKind = "NoDocumentation" SynopsisDocumentation HoverKind = "SynopsisDocumentation" FullDocumentation HoverKind = "FullDocumentation" - - // Structured is an experimental setting that returns a structured hover format. - // This format separates the signature from the documentation, so that the client - // can do more manipulation of these fields. - // - // This should only be used by clients that support this behavior. - Structured HoverKind = "Structured" ) type VulncheckMode string @@ -1021,12 +974,14 @@ func (o *Options) setOne(name string, value any) error { AllSymbolScope) case "hoverKind": + if s, ok := value.(string); ok && strings.EqualFold(s, "structured") { + return deprecatedError("the experimental hoverKind='structured' setting was removed in gopls/v0.18.0 (https://go.dev/issue/70233)") + } return setEnum(&o.HoverKind, value, NoDocumentation, SingleLine, SynopsisDocumentation, - FullDocumentation, - Structured) + FullDocumentation) case "linkTarget": return setString(&o.LinkTarget, value) @@ -1062,7 +1017,7 @@ func (o *Options) setOne(name string, value any) error { return setBoolMap(&o.Hints, value) case "annotations": - return setAnnotationMap(&o.Annotations, value) + return deprecatedError("the 'annotations' setting was removed in gopls/v0.18.0; all compiler optimization details are now shown") case "vulncheck": return setEnum(&o.Vulncheck, value, @@ -1318,48 +1273,6 @@ func setDuration(dest *time.Duration, value any) error { return nil } -func setAnnotationMap(dest *map[Annotation]bool, value any) error { - all, err := asBoolMap[string](value) - if err != nil { - return err - } - if all == nil { - return nil - } - // Default to everything enabled by default. - m := make(map[Annotation]bool) - for k, enabled := range all { - var a Annotation - if err := setEnum(&a, k, - Nil, - Escape, - Inline, - Bounds); err != nil { - // In case of an error, process any legacy values. - switch k { - case "noEscape": - m[Escape] = false - return fmt.Errorf(`"noEscape" is deprecated, set "Escape: false" instead`) - case "noNilcheck": - m[Nil] = false - return fmt.Errorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) - - case "noInline": - m[Inline] = false - return fmt.Errorf(`"noInline" is deprecated, set "Inline: false" instead`) - case "noBounds": - m[Bounds] = false - return fmt.Errorf(`"noBounds" is deprecated, set "Bounds: false" instead`) - default: - return err - } - } - m[a] = enabled - } - *dest = m - return nil -} - func setBoolMap[K ~string](dest *map[K]bool, value any) error { m, err := asBoolMap[K](value) if err != nil { diff --git a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go index 6f865083a9d..63b4aded8bd 100644 --- a/gopls/internal/settings/settings_test.go +++ b/gopls/internal/settings/settings_test.go @@ -90,18 +90,34 @@ func TestOptions_Set(t *testing.T) { return o.HoverKind == SingleLine }, }, + { + name: "hoverKind", + value: "Structured", + wantError: true, + check: func(o Options) bool { + return o.HoverKind == FullDocumentation + }, + }, + { + name: "ui.documentation.hoverKind", + value: "Structured", + wantError: true, + check: func(o Options) bool { + return o.HoverKind == FullDocumentation + }, + }, { name: "hoverKind", - value: "Structured", + value: "FullDocumentation", check: func(o Options) bool { - return o.HoverKind == Structured + return o.HoverKind == FullDocumentation }, }, { name: "ui.documentation.hoverKind", - value: "Structured", + value: "FullDocumentation", check: func(o Options) bool { - return o.HoverKind == Structured + return o.HoverKind == FullDocumentation }, }, { @@ -164,17 +180,6 @@ func TestOptions_Set(t *testing.T) { return len(o.DirectoryFilters) == 0 }, }, - { - name: "annotations", - value: map[string]any{ - "Nil": false, - "noBounds": true, - }, - wantError: true, - check: func(o Options) bool { - return !o.Annotations[Nil] && !o.Annotations[Bounds] - }, - }, { name: "vulncheck", value: []any{"invalid"}, diff --git a/gopls/internal/test/integration/bench/bench_test.go b/gopls/internal/test/integration/bench/bench_test.go index 3e163d11127..d7c1fd976bd 100644 --- a/gopls/internal/test/integration/bench/bench_test.go +++ b/gopls/internal/test/integration/bench/bench_test.go @@ -306,19 +306,20 @@ func startProfileIfSupported(b *testing.B, env *integration.Env, name string) fu b.Fatalf("reading profile: %v", err) } b.ReportMetric(totalCPU.Seconds()/float64(b.N), "cpu_seconds/op") - if *cpuProfile == "" { - // The user didn't request profiles, so delete it to clean up. - if err := os.Remove(profFile); err != nil { - b.Errorf("removing profile file: %v", err) + if *cpuProfile != "" { + // Read+write to avoid exdev errors. + data, err := os.ReadFile(profFile) + if err != nil { + b.Fatalf("reading profile: %v", err) } - } else { - // NOTE: if this proves unreliable (due to e.g. EXDEV), we can fall back - // on Read+Write+Remove. name := qualifiedName(name, *cpuProfile) - if err := os.Rename(profFile, name); err != nil { - b.Fatalf("renaming profile file: %v", err) + if err := os.WriteFile(name, data, 0666); err != nil { + b.Fatalf("writing profile: %v", err) } } + if err := os.Remove(profFile); err != nil { + b.Errorf("removing profile file: %v", err) + } } } diff --git a/gopls/internal/test/integration/bench/workspace_symbols_test.go b/gopls/internal/test/integration/bench/workspace_symbols_test.go index 94dd9e08cf3..d3e1d207b2d 100644 --- a/gopls/internal/test/integration/bench/workspace_symbols_test.go +++ b/gopls/internal/test/integration/bench/workspace_symbols_test.go @@ -8,6 +8,7 @@ import ( "flag" "fmt" "testing" + "time" ) var symbolQuery = flag.String("symbol_query", "test", "symbol query to use in benchmark") @@ -18,10 +19,11 @@ func BenchmarkWorkspaceSymbols(b *testing.B) { for name := range repos { b.Run(name, func(b *testing.B) { env := getRepo(b, name).sharedEnv(b) + start := time.Now() symbols := env.Symbol(*symbolQuery) // warm the cache if testing.Verbose() { - fmt.Println("Results:") + fmt.Printf("Results (after %s):\n", time.Since(start)) for i, symbol := range symbols { fmt.Printf("\t%d. %s (%s)\n", i, symbol.Name, symbol.ContainerName) } diff --git a/gopls/internal/test/integration/codelens/gcdetails_test.go b/gopls/internal/test/integration/codelens/gcdetails_test.go deleted file mode 100644 index 67750382de0..00000000000 --- a/gopls/internal/test/integration/codelens/gcdetails_test.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2020 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 codelens - -import ( - "runtime" - "testing" - - "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/test/integration" - "golang.org/x/tools/gopls/internal/test/integration/fake" - "golang.org/x/tools/gopls/internal/util/bug" -) - -func TestGCDetails_Toggle(t *testing.T) { - if runtime.GOOS == "android" { - t.Skipf("the gc details code lens doesn't work on Android") - } - - const mod = ` --- go.mod -- -module mod.com - -go 1.15 --- main.go -- -package main - -import "fmt" - -func main() { - fmt.Println(42) -} -` - WithOptions( - Settings{ - "codelenses": map[string]bool{ - "gc_details": true, - }, - }, - ).Run(t, mod, func(t *testing.T, env *Env) { - env.OpenFile("main.go") - env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) - - env.OnceMet( - CompletedWork(server.DiagnosticWorkTitle(server.FromToggleGCDetails), 1, true), - Diagnostics( - ForFile("main.go"), - WithMessage("42 escapes"), - WithSeverityTags("optimizer details", protocol.SeverityInformation, nil), - ), - ) - - // GCDetails diagnostics should be reported even on unsaved - // edited buffers, thanks to the magic of overlays. - env.SetBufferContent("main.go", ` -package main -func main() {} -func f(x int) *int { return &x }`) - env.AfterChange(Diagnostics( - ForFile("main.go"), - WithMessage("x escapes"), - WithSeverityTags("optimizer details", protocol.SeverityInformation, nil), - )) - - // Toggle the GC details code lens again so now it should be off. - env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) - env.OnceMet( - CompletedWork(server.DiagnosticWorkTitle(server.FromToggleGCDetails), 2, true), - NoDiagnostics(ForFile("main.go")), - ) - }) -} - -// Test for the crasher in golang/go#54199 -func TestGCDetails_NewFile(t *testing.T) { - bug.PanicOnBugs = false - const src = ` --- go.mod -- -module mod.test - -go 1.12 -` - - WithOptions( - Settings{ - "codelenses": map[string]bool{ - "gc_details": true, - }, - }, - ).Run(t, src, func(t *testing.T, env *Env) { - env.CreateBuffer("p_test.go", "") - - hasGCDetails := func() bool { - lenses := env.CodeLens("p_test.go") // should not crash - for _, lens := range lenses { - if lens.Command.Command == command.GCDetails.String() { - return true - } - } - return false - } - - // With an empty file, we shouldn't get the gc_details codelens because - // there is nowhere to position it (it needs a package name). - if hasGCDetails() { - t.Errorf("got the gc_details codelens for an empty file") - } - - // Edit to provide a package name. - env.EditBuffer("p_test.go", fake.NewEdit(0, 0, 0, 0, "package p")) - - // Now we should get the gc_details codelens. - if !hasGCDetails() { - t.Errorf("didn't get the gc_details codelens for a valid non-empty Go file") - } - }) -} diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go index 0b8895b3d31..9e6c504cc86 100644 --- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go +++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go @@ -79,7 +79,7 @@ go 1.12 ).Run(t, onlyMod, func(t *testing.T, env *Env) { env.CreateBuffer("main.go", `package main -func m() { +func _() { log.Println() } `) diff --git a/gopls/internal/test/integration/diagnostics/gopackagesdriver_test.go b/gopls/internal/test/integration/diagnostics/gopackagesdriver_test.go index 65700b69795..3e7c0f5f2fd 100644 --- a/gopls/internal/test/integration/diagnostics/gopackagesdriver_test.go +++ b/gopls/internal/test/integration/diagnostics/gopackagesdriver_test.go @@ -25,9 +25,6 @@ go 1.12 package foo import "mod.com/hello" - -func f() { -} ` WithOptions( FakeGoPackagesDriver(t), diff --git a/gopls/internal/test/integration/env.go b/gopls/internal/test/integration/env.go index 4acd4603827..64344d0d146 100644 --- a/gopls/internal/test/integration/env.go +++ b/gopls/internal/test/integration/env.go @@ -225,7 +225,7 @@ func (a *Awaiter) onShowMessage(_ context.Context, params *protocol.ShowMessageP return nil } -// ListenToShownDocuments registers a listener to incoming showDocument +// ListenToShownMessages registers a listener to incoming showMessage // notifications. Call the resulting func to deregister the listener and // receive all notifications that have occurred since the listener was // registered. diff --git a/gopls/internal/test/integration/expectation.go b/gopls/internal/test/integration/expectation.go index d5e6030bf20..ad41423d098 100644 --- a/gopls/internal/test/integration/expectation.go +++ b/gopls/internal/test/integration/expectation.go @@ -452,7 +452,7 @@ type WorkStatus struct { EndMsg string } -// CompletedProgress expects that workDone progress is complete for the given +// CompletedProgressToken expects that workDone progress is complete for the given // progress token. When non-nil WorkStatus is provided, it will be filled // when the expectation is met. // diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go index a9d0ce8b149..c62a3898e9b 100644 --- a/gopls/internal/test/integration/misc/codeactions_test.go +++ b/gopls/internal/test/integration/misc/codeactions_test.go @@ -68,12 +68,14 @@ func g() {} settings.GoAssembly, settings.GoDoc, settings.GoFreeSymbols, + settings.GoToggleCompilerOptDetails, settings.GoplsDocFeatures, settings.RefactorInlineCall) check("gen/a.go", settings.GoAssembly, settings.GoDoc, settings.GoFreeSymbols, + settings.GoToggleCompilerOptDetails, settings.GoplsDocFeatures) }) } diff --git a/gopls/internal/test/integration/misc/compileropt_test.go b/gopls/internal/test/integration/misc/compileropt_test.go new file mode 100644 index 00000000000..8b8f78cd62d --- /dev/null +++ b/gopls/internal/test/integration/misc/compileropt_test.go @@ -0,0 +1,81 @@ +// 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 misc + +import ( + "runtime" + "testing" + + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/server" + "golang.org/x/tools/gopls/internal/settings" + . "golang.org/x/tools/gopls/internal/test/integration" +) + +// TestCompilerOptDetails exercises the "Toggle compiler optimization details" code action. +func TestCompilerOptDetails(t *testing.T) { + if runtime.GOOS == "android" { + t.Skipf("the compiler optimization details code action doesn't work on Android") + } + + const mod = ` +-- go.mod -- +module mod.com + +go 1.15 +-- main.go -- +package main + +import "fmt" + +func main() { + fmt.Println(42) +} +` + Run(t, mod, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + actions := env.CodeActionForFile("main.go", nil) + + // Execute the "Toggle compiler optimization details" command. + docAction, err := codeActionByKind(actions, settings.GoToggleCompilerOptDetails) + if err != nil { + t.Fatal(err) + } + params := &protocol.ExecuteCommandParams{ + Command: docAction.Command.Command, + Arguments: docAction.Command.Arguments, + } + env.ExecuteCommand(params, nil) + + env.OnceMet( + CompletedWork(server.DiagnosticWorkTitle(server.FromToggleCompilerOptDetails), 1, true), + Diagnostics( + ForFile("main.go"), + AtPosition("main.go", 5, 13), // (LSP coordinates) + WithMessage("42 escapes"), + WithSeverityTags("optimizer details", protocol.SeverityInformation, nil), + ), + ) + + // Diagnostics should be reported even on unsaved + // edited buffers, thanks to the magic of overlays. + env.SetBufferContent("main.go", ` +package main +func main() { _ = f } +func f(x int) *int { return &x }`) + env.AfterChange(Diagnostics( + ForFile("main.go"), + WithMessage("x escapes"), + WithSeverityTags("optimizer details", protocol.SeverityInformation, nil), + )) + + // Toggle the flag again so now it should be off. + env.ExecuteCommand(params, nil) + env.OnceMet( + CompletedWork(server.DiagnosticWorkTitle(server.FromToggleCompilerOptDetails), 2, true), + NoDiagnostics(ForFile("main.go")), + ) + }) +} diff --git a/gopls/internal/test/integration/misc/imports_test.go b/gopls/internal/test/integration/misc/imports_test.go index 30a161017dc..5b8b020124d 100644 --- a/gopls/internal/test/integration/misc/imports_test.go +++ b/gopls/internal/test/integration/misc/imports_test.go @@ -66,7 +66,7 @@ package main import "fmt" //this comment is necessary for failure -func a() { +func _() { fmt.Println("hello") } ` @@ -96,7 +96,7 @@ func f(x float64) float64 { -- b.go -- package foo -func g() { +func _() { _ = rand.Int63() } ` diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index 11cd56eef99..2bde7df8aa2 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go @@ -349,10 +349,11 @@ module mod.com go 1.20 -- a/a1.go -- +// Package a refers to [b.T] [b.U] [alias.D] [d.D] [c.T] [c.U] [nope.Nope] package a -import "b" -import alias "d" +import "mod.com/b" +import alias "mod.com/d" // [b.T] indeed refers to b.T. // @@ -363,7 +364,7 @@ type A1 int -- a/a2.go -- package a -import b "c" +import b "mod.com/c" // [b.U] actually refers to c.U. type A2 int @@ -393,14 +394,24 @@ type D int // Check that the doc links are resolved using the // appropriate import mapping for the file in which // they appear. - checkMatch(t, true, doc, `pkg/b\?.*#T">b.T indeed refers to b.T`) - checkMatch(t, true, doc, `pkg/c\?.*#U">b.U actually refers to c.U`) + checkMatch(t, true, doc, `pkg/mod.com/b\?.*#T">b.T indeed refers to b.T`) + checkMatch(t, true, doc, `pkg/mod.com/c\?.*#U">b.U actually refers to c.U`) // Check that doc links can be resolved using either // the original or the local name when they refer to a // renaming import. (Local names are preferred.) - checkMatch(t, true, doc, `pkg/d\?.*#D">alias.D refers to d.D`) - checkMatch(t, true, doc, `pkg/d\?.*#D">d.D also refers to d.D`) + checkMatch(t, true, doc, `pkg/mod.com/d\?.*#D">alias.D refers to d.D`) + checkMatch(t, true, doc, `pkg/mod.com/d\?.*#D">d.D also refers to d.D`) + + // Check that links in the package doc comment are + // resolved, and relative to the correct file (a1.go). + checkMatch(t, true, doc, `Package a refers to.*pkg/mod.com/b\?.*#T">b.T`) + checkMatch(t, true, doc, `Package a refers to.*pkg/mod.com/b\?.*#U">b.U`) + checkMatch(t, true, doc, `Package a refers to.*pkg/mod.com/d\?.*#D">alias.D`) + checkMatch(t, true, doc, `Package a refers to.*pkg/mod.com/d\?.*#D">d.D`) + checkMatch(t, true, doc, `Package a refers to.*pkg/mod.com/c\?.*#T">c.T`) + checkMatch(t, true, doc, `Package a refers to.*pkg/mod.com/c\?.*#U">c.U`) + checkMatch(t, true, doc, `Package a refers to.* \[nope.Nope\]`) }) } diff --git a/gopls/internal/test/integration/workspace/packages_test.go b/gopls/internal/test/integration/workspace/packages_test.go index 106734a1864..7ee19bcca54 100644 --- a/gopls/internal/test/integration/workspace/packages_test.go +++ b/gopls/internal/test/integration/workspace/packages_test.go @@ -124,6 +124,7 @@ func TestFoo2(t *testing.T) package foo import "testing" func TestFoo(t *testing.T) +func Issue70927(*error) -- foo2_test.go -- package foo_test diff --git a/gopls/internal/test/marker/testdata/callhierarchy/issue64451.txt b/gopls/internal/test/marker/testdata/callhierarchy/issue64451.txt index 618d6ed6e34..3e6928e6f1d 100644 --- a/gopls/internal/test/marker/testdata/callhierarchy/issue64451.txt +++ b/gopls/internal/test/marker/testdata/callhierarchy/issue64451.txt @@ -13,7 +13,7 @@ go 1.0 -- a/a.go -- package a -func foo() { //@ loc(foo, "foo") +func Foo() { //@ loc(Foo, "Foo") bar() } @@ -38,14 +38,14 @@ func init() { //@ loc(init, "init") baz() } -//@ outgoingcalls(foo, bar) +//@ outgoingcalls(Foo, bar) //@ outgoingcalls(bar, baz) //@ outgoingcalls(baz, bluh) //@ outgoingcalls(bluh) //@ outgoingcalls(init, baz) -//@ incomingcalls(foo) -//@ incomingcalls(bar, foo) +//@ incomingcalls(Foo) +//@ incomingcalls(bar, Foo) //@ incomingcalls(baz, bar, global, init) //@ incomingcalls(bluh, baz) //@ incomingcalls(init) 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 fabbbee99d3..96c09cd0246 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt @@ -25,5 +25,5 @@ func main() { -- @type_switch_func_call/extract_switch.go -- @@ -10 +10,2 @@ - switch r := f().(type) { //@codeaction("f()", "refactor.extract.variable", edit=type_switch_func_call) -+ x := f() -+ switch r := x.(type) { //@codeaction("f()", "refactor.extract.variable", edit=type_switch_func_call) ++ newVar := f() ++ switch r := newVar.(type) { //@codeaction("f()", "refactor.extract.variable", edit=type_switch_func_call) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable-70563.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable-70563.txt new file mode 100644 index 00000000000..1317815ea32 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable-70563.txt @@ -0,0 +1,50 @@ +This test verifies the fix for golang/go#70563: refactor.extract.variable +inserts new statement before the scope of its free symbols. + +-- flags -- +-ignore_extra_diags + +-- inside_else.go -- +package extract + +func _() { + if x := 1; true { + + } else if y := x + 1; true { //@codeaction("x + 1", "refactor.extract.variable", err=re"Else's init statement has free variable declaration") + + } +} +-- inside_case.go -- +package extract + +func _() { + switch x := 1; x { + case x + 1: //@codeaction("x + 1", "refactor.extract.variable-all", err=re"SwitchStmt's init statement has free variable declaration") + y := x + 1 //@codeaction("x + 1", "refactor.extract.variable-all", err=re"SwitchStmt's init statement has free variable declaration") + _ = y + case 3: + y := x + 1 //@codeaction("x + 1", "refactor.extract.variable-all", err=re"SwitchStmt's init statement has free variable declaration") + _ = y + } +} +-- parent_if.go -- +package extract + +func _() { + if x := 1; x > 0 { + y = x + 1 //@codeaction("x + 1", "refactor.extract.variable-all", err=re"IfStmt's init statement has free variable declaration") + } else { + y = x + 1 //@codeaction("x + 1", "refactor.extract.variable-all", err=re"IfStmt's init statement has free variable declaration") + } +} +-- parent_switch.go -- +package extract + +func _() { + switch x := 1; x { + case 1: + y = x + 1 //@codeaction("x + 1", "refactor.extract.variable-all", err=re"SwitchStmt's init statement has free variable declaration") + case 3: + y = x + 1 //@codeaction("x + 1", "refactor.extract.variable-all", err=re"SwitchStmt's init statement has free variable declaration") + } +} diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable-if.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable-if.txt index ab9d76b8602..fdc00d3bf8f 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable-if.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable-if.txt @@ -29,13 +29,13 @@ func variable(y int) { -- @constant/a.go -- @@ -4 +4 @@ -+ const k = 1 + 2 ++ const newConst = 1 + 2 @@ -5 +6 @@ - } else if 1 + 2 > 0 { //@ codeaction("1 + 2", "refactor.extract.constant", edit=constant) -+ } else if k > 0 { //@ codeaction("1 + 2", "refactor.extract.constant", edit=constant) ++ } else if newConst > 0 { //@ codeaction("1 + 2", "refactor.extract.constant", edit=constant) -- @variable/a.go -- @@ -10 +10 @@ -+ x := y + y ++ newVar := y + y @@ -11 +12 @@ - } else if y + y > 0 { //@ codeaction("y + y", "refactor.extract.variable", edit=variable) -+ } else if x > 0 { //@ codeaction("y + y", "refactor.extract.variable", edit=variable) ++ } else if newVar > 0 { //@ codeaction("y + y", "refactor.extract.variable", edit=variable) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable-inexact.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable-inexact.txt index 1781b3ce6af..5ddff1182f6 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable-inexact.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable-inexact.txt @@ -17,20 +17,20 @@ func _(ptr *int) { -- @spaces/a.go -- @@ -4 +4,2 @@ - var _ = 1 + 2 + 3 //@codeaction("1 + 2 ", "refactor.extract.constant", edit=spaces) -+ const k = 1 + 2 -+ var _ = k+ 3 //@codeaction("1 + 2 ", "refactor.extract.constant", edit=spaces) ++ const newConst = 1 + 2 ++ var _ = newConst + 3 //@codeaction("1 + 2 ", "refactor.extract.constant", edit=spaces) -- @funclit/a.go -- @@ -5 +5,2 @@ - var _ = func() {} //@codeaction("func() {}", "refactor.extract.variable", edit=funclit) -+ x := func() {} -+ var _ = x //@codeaction("func() {}", "refactor.extract.variable", edit=funclit) ++ newVar := func() {} ++ var _ = newVar //@codeaction("func() {}", "refactor.extract.variable", edit=funclit) -- @ptr/a.go -- @@ -6 +6,2 @@ - var _ = *ptr //@codeaction("*ptr", "refactor.extract.variable", edit=ptr) -+ x := *ptr -+ var _ = x //@codeaction("*ptr", "refactor.extract.variable", edit=ptr) ++ newVar := *ptr ++ var _ = newVar //@codeaction("*ptr", "refactor.extract.variable", edit=ptr) -- @paren/a.go -- @@ -7 +7,2 @@ - var _ = (ptr) //@codeaction("(ptr)", "refactor.extract.variable", edit=paren) -+ x := (ptr) -+ var _ = x //@codeaction("(ptr)", "refactor.extract.variable", edit=paren) ++ newVar := (ptr) ++ var _ = newVar //@codeaction("(ptr)", "refactor.extract.variable", edit=paren) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable-toplevel.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable-toplevel.txt index b9166c6299d..00d3bc6983e 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable-toplevel.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable-toplevel.txt @@ -10,42 +10,42 @@ var slice = append([]int{}, 1, 2, 3) //@codeaction("[]int{}", "refactor.extract. type SHA256 [32]byte //@codeaction("32", "refactor.extract.constant", edit=arraylen) -func f([2]int) {} //@codeaction("2", "refactor.extract.constant", edit=paramtypearraylen) +func F([2]int) {} //@codeaction("2", "refactor.extract.constant", edit=paramtypearraylen) -- @lenhello/a.go -- @@ -3 +3,2 @@ -const length = len("hello") + 2 //@codeaction(`len("hello")`, "refactor.extract.constant", edit=lenhello) -+const k = len("hello") -+const length = k + 2 //@codeaction(`len("hello")`, "refactor.extract.constant", edit=lenhello) ++const newConst = len("hello") ++const length = newConst + 2 //@codeaction(`len("hello")`, "refactor.extract.constant", edit=lenhello) -- @sliceliteral/a.go -- @@ -5 +5,2 @@ -var slice = append([]int{}, 1, 2, 3) //@codeaction("[]int{}", "refactor.extract.variable", edit=sliceliteral) -+var x = []int{} -+var slice = append(x, 1, 2, 3) //@codeaction("[]int{}", "refactor.extract.variable", edit=sliceliteral) ++var newVar = []int{} ++var slice = append(newVar, 1, 2, 3) //@codeaction("[]int{}", "refactor.extract.variable", edit=sliceliteral) -- @arraylen/a.go -- @@ -7 +7,2 @@ -type SHA256 [32]byte //@codeaction("32", "refactor.extract.constant", edit=arraylen) -+const k = 32 -+type SHA256 [k]byte //@codeaction("32", "refactor.extract.constant", edit=arraylen) ++const newConst = 32 ++type SHA256 [newConst]byte //@codeaction("32", "refactor.extract.constant", edit=arraylen) -- @paramtypearraylen/a.go -- @@ -9 +9,2 @@ --func f([2]int) {} //@codeaction("2", "refactor.extract.constant", edit=paramtypearraylen) -+const k = 2 -+func f([k]int) {} //@codeaction("2", "refactor.extract.constant", edit=paramtypearraylen) +-func F([2]int) {} //@codeaction("2", "refactor.extract.constant", edit=paramtypearraylen) ++const newConst = 2 ++func F([newConst]int) {} //@codeaction("2", "refactor.extract.constant", edit=paramtypearraylen) -- b/b.go -- package b // Check that package- and file-level name collisions are avoided. -import x3 "errors" +import newVar3 "errors" -var x, x1, x2 any // these names are taken already -var _ = x3.New("") +var newVar, newVar1, newVar2 any // these names are taken already +var _ = newVar3.New("") var a, b int var c = a + b //@codeaction("a + b", "refactor.extract.variable", edit=fresh) -- @fresh/b/b.go -- @@ -10 +10,2 @@ -var c = a + b //@codeaction("a + b", "refactor.extract.variable", edit=fresh) -+var x4 = a + b -+var c = x4 //@codeaction("a + b", "refactor.extract.variable", edit=fresh) ++var newVar4 = a + b ++var c = newVar4 //@codeaction("a + b", "refactor.extract.variable", edit=fresh) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt index c14fb732978..9dd0f766e05 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt @@ -15,13 +15,13 @@ func _() { -- @basic_lit1/basic_lit.go -- @@ -4 +4,2 @@ - var _ = 1 + 2 //@codeaction("1", "refactor.extract.constant", edit=basic_lit1) -+ const k = 1 -+ var _ = k + 2 //@codeaction("1", "refactor.extract.constant", edit=basic_lit1) ++ const newConst = 1 ++ var _ = newConst + 2 //@codeaction("1", "refactor.extract.constant", edit=basic_lit1) -- @basic_lit2/basic_lit.go -- @@ -5 +5,2 @@ - var _ = 3 + 4 //@codeaction("3 + 4", "refactor.extract.constant", edit=basic_lit2) -+ const k = 3 + 4 -+ var _ = k //@codeaction("3 + 4", "refactor.extract.constant", edit=basic_lit2) ++ const newConst = 3 + 4 ++ var _ = newConst //@codeaction("3 + 4", "refactor.extract.constant", edit=basic_lit2) -- func_call.go -- package extract @@ -36,13 +36,13 @@ func _() { -- @func_call1/func_call.go -- @@ -6 +6,2 @@ - x0 := append([]int{}, 1) //@codeaction("append([]int{}, 1)", "refactor.extract.variable", edit=func_call1) -+ x := append([]int{}, 1) -+ x0 := x //@codeaction("append([]int{}, 1)", "refactor.extract.variable", edit=func_call1) ++ newVar := append([]int{}, 1) ++ x0 := newVar //@codeaction("append([]int{}, 1)", "refactor.extract.variable", edit=func_call1) -- @func_call2/func_call.go -- @@ -8 +8,2 @@ - b, err := strconv.Atoi(str) //@codeaction("strconv.Atoi(str)", "refactor.extract.variable", edit=func_call2) -+ x, x1 := strconv.Atoi(str) -+ b, err := x, x1 //@codeaction("strconv.Atoi(str)", "refactor.extract.variable", edit=func_call2) ++ newVar, newVar1 := strconv.Atoi(str) ++ b, err := newVar, newVar1 //@codeaction("strconv.Atoi(str)", "refactor.extract.variable", edit=func_call2) -- scope.go -- package extract @@ -61,10 +61,10 @@ func _() { -- @scope1/scope.go -- @@ -8 +8,2 @@ - y := ast.CompositeLit{} //@codeaction("ast.CompositeLit{}", "refactor.extract.variable", edit=scope1) -+ x := ast.CompositeLit{} -+ y := x //@codeaction("ast.CompositeLit{}", "refactor.extract.variable", edit=scope1) ++ newVar := ast.CompositeLit{} ++ y := newVar //@codeaction("ast.CompositeLit{}", "refactor.extract.variable", edit=scope1) -- @scope2/scope.go -- @@ -11 +11,2 @@ - x := !false //@codeaction("!false", "refactor.extract.constant", edit=scope2) -+ const k = !false -+ x := k //@codeaction("!false", "refactor.extract.constant", edit=scope2) ++ const newConst = !false ++ x := newConst //@codeaction("!false", "refactor.extract.constant", edit=scope2) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable_all.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable_all.txt new file mode 100644 index 00000000000..050f29bfec7 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable_all.txt @@ -0,0 +1,238 @@ +This test checks the behavior of the 'replace all occurrences of expression' code action, with resolve support. +See extract_expressions.txt for the same test without resolve support. + +-- flags -- +-ignore_extra_diags + +-- basic_lit.go -- +package extract_all + +func _() { + var _ = 1 + 2 + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) + var _ = 1 + 2 + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) +} +-- @basic_lit/basic_lit.go -- +@@ -4,2 +4,3 @@ +- var _ = 1 + 2 + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) +- var _ = 1 + 2 + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) ++ const newConst = 1 + 2 ++ var _ = newConst + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) ++ var _ = newConst + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) +-- nested_scope.go -- +package extract_all + +func _() { + newConst1 := 0 + if true { + x := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) + } + if true { + newConst := 0 + if false { + y := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) + } + } + z := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) +} +-- @nested_scope/nested_scope.go -- +@@ -5 +5 @@ ++ const newConst2 = 1 + 2 + 3 +@@ -6 +7 @@ +- x := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) ++ x := newConst2 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) +@@ -11 +12 @@ +- y := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) ++ y := newConst2 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) +@@ -14 +15 @@ +- z := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) ++ z := newConst2 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) +-- function_call.go -- +package extract_all + +import "fmt" + +func _() { + result := fmt.Sprintf("%d", 42) //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) + if result != "" { + anotherResult := fmt.Sprintf("%d", 42) //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) + _ = anotherResult + } +} +-- @replace_func_call/function_call.go -- +@@ -6 +6,2 @@ +- result := fmt.Sprintf("%d", 42) //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) ++ newVar := fmt.Sprintf("%d", 42) ++ result := newVar //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) +@@ -8 +9 @@ +- anotherResult := fmt.Sprintf("%d", 42) //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) ++ anotherResult := newVar //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) +-- composite_literals.go -- +package extract_all + +func _() { + data := []int{1, 2, 3} //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) + processData(data) + moreData := []int{1, 2, 3} //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) + processData(moreData) +} + +func processData(d []int) {} +-- @composite/composite_literals.go -- +@@ -4 +4,2 @@ +- data := []int{1, 2, 3} //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) ++ newVar := []int{1, 2, 3} ++ data := newVar //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) +@@ -6 +7 @@ +- moreData := []int{1, 2, 3} //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) ++ moreData := newVar //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) +-- selector.go -- +package extract_all + +type MyStruct struct { + Value int +} + +func _() { + s := MyStruct{Value: 10} + v := s.Value //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) + if v > 0 { + w := s.Value //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) + _ = w + } +} +-- @sel/selector.go -- +@@ -9 +9,2 @@ +- v := s.Value //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) ++ newVar := s.Value ++ v := newVar //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) +@@ -11 +12 @@ +- w := s.Value //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) ++ w := newVar //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) +-- index.go -- +package extract_all + +func _() { + arr := []int{1, 2, 3} + val := arr[0] //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) + val2 := arr[0] //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) +} +-- @index/index.go -- +@@ -5,2 +5,3 @@ +- val := arr[0] //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) +- val2 := arr[0] //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) ++ newVar := arr[0] ++ val := newVar //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) ++ val2 := newVar //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) +-- slice_expr.go -- +package extract_all + +func _() { + data := []int{1, 2, 3, 4, 5} + part := data[1:3] //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) + anotherPart := data[1:3] //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) +} +-- @slice/slice_expr.go -- +@@ -5,2 +5,3 @@ +- part := data[1:3] //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) +- anotherPart := data[1:3] //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) ++ newVar := data[1:3] ++ part := newVar //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) ++ anotherPart := newVar //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) +-- nested_func.go -- +package extract_all + +func outer() { + inner := func() { + val := 100 + 200 //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) + _ = val + } + inner() + val := 100 + 200 //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) + _ = val +} +-- @nested/nested_func.go -- +@@ -4 +4 @@ ++ const newConst = 100 + 200 +@@ -5 +6 @@ +- val := 100 + 200 //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) ++ val := newConst //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) +@@ -9 +10 @@ +- val := 100 + 200 //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) ++ val := newConst //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) +-- switch.go -- +package extract_all + +func _() { + value := 2 + switch value { + case 1: + result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) + _ = result + case 2: + result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) + _ = result + default: + result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) + _ = result + } +} +-- @switch/switch.go -- +@@ -5 +5 @@ ++ newVar := value * 10 +@@ -7 +8 @@ +- result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) ++ result := newVar //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) +@@ -10 +11 @@ +- result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) ++ result := newVar //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) +@@ -13 +14 @@ +- result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) ++ result := newVar //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) +-- switch_single.go -- +package extract_all + +func _() { + value := 2 + switch value { + case 1: + result := value * 10 + _ = result + case 2: + result := value * 10 + _ = result + default: + result := value * 10 //@codeaction("value * 10", "refactor.extract.variable", edit=switch_single) + _ = result + } +} +-- @switch_single/switch_single.go -- +@@ -13 +13,2 @@ +- result := value * 10 //@codeaction("value * 10", "refactor.extract.variable", edit=switch_single) ++ newVar := value * 10 ++ result := newVar //@codeaction("value * 10", "refactor.extract.variable", edit=switch_single) +-- func_list.go -- +package extract_all + +func _() { + x := func(a int) int { //@codeaction("func", "refactor.extract.variable-all", end=closeBracket1, edit=func_list) + b := 1 + return b + a + } //@loc(closeBracket1, "}") + y := func(a int) int { //@codeaction("func", "refactor.extract.variable-all", end=closeBracket2, edit=func_list) + b := 1 + return b + a + }//@loc(closeBracket2, "}") +} +-- @func_list/func_list.go -- +@@ -4 +4 @@ +- x := func(a int) int { //@codeaction("func", "refactor.extract.variable-all", end=closeBracket1, edit=func_list) ++ newVar := func(a int) int { +@@ -7,5 +7,3 @@ +- } //@loc(closeBracket1, "}") +- y := func(a int) int { //@codeaction("func", "refactor.extract.variable-all", end=closeBracket2, edit=func_list) +- b := 1 +- return b + a +- }//@loc(closeBracket2, "}") ++ } ++ x := newVar //@loc(closeBracket1, "}") ++ y := newVar//@loc(closeBracket2, "}") diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable_all_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable_all_resolve.txt new file mode 100644 index 00000000000..02c03929567 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable_all_resolve.txt @@ -0,0 +1,249 @@ +This test checks the behavior of the 'replace all occurrences of expression' code action, with resolve support. +See extract_expressions.txt for the same test without resolve support. + +-- capabilities.json -- +{ + "textDocument": { + "codeAction": { + "dataSupport": true, + "resolveSupport": { + "properties": ["edit"] + } + } + } +} +-- flags -- +-ignore_extra_diags + +-- basic_lit.go -- +package extract_all + +func _() { + var _ = 1 + 2 + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) + var _ = 1 + 2 + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) +} +-- @basic_lit/basic_lit.go -- +@@ -4,2 +4,3 @@ +- var _ = 1 + 2 + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) +- var _ = 1 + 2 + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) ++ const newConst = 1 + 2 ++ var _ = newConst + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) ++ var _ = newConst + 3 //@codeaction("1 + 2", "refactor.extract.constant-all", edit=basic_lit) +-- nested_scope.go -- +package extract_all + +func _() { + newConst1 := 0 + if true { + x := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) + } + if true { + newConst := 0 + if false { + y := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) + } + } + z := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) +} +-- @nested_scope/nested_scope.go -- +@@ -5 +5 @@ ++ const newConst2 = 1 + 2 + 3 +@@ -6 +7 @@ +- x := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) ++ x := newConst2 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) +@@ -11 +12 @@ +- y := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) ++ y := newConst2 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) +@@ -14 +15 @@ +- z := 1 + 2 + 3 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) ++ z := newConst2 //@codeaction("1 + 2 + 3", "refactor.extract.constant-all", edit=nested_scope) +-- function_call.go -- +package extract_all + +import "fmt" + +func _() { + result := fmt.Sprintf("%d", 42) //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) + if result != "" { + anotherResult := fmt.Sprintf("%d", 42) //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) + _ = anotherResult + } +} +-- @replace_func_call/function_call.go -- +@@ -6 +6,2 @@ +- result := fmt.Sprintf("%d", 42) //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) ++ newVar := fmt.Sprintf("%d", 42) ++ result := newVar //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) +@@ -8 +9 @@ +- anotherResult := fmt.Sprintf("%d", 42) //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) ++ anotherResult := newVar //@codeaction(`fmt.Sprintf("%d", 42)`, "refactor.extract.variable-all", edit=replace_func_call) +-- composite_literals.go -- +package extract_all + +func _() { + data := []int{1, 2, 3} //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) + processData(data) + moreData := []int{1, 2, 3} //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) + processData(moreData) +} + +func processData(d []int) {} +-- @composite/composite_literals.go -- +@@ -4 +4,2 @@ +- data := []int{1, 2, 3} //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) ++ newVar := []int{1, 2, 3} ++ data := newVar //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) +@@ -6 +7 @@ +- moreData := []int{1, 2, 3} //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) ++ moreData := newVar //@codeaction("[]int{1, 2, 3}", "refactor.extract.variable-all", edit=composite) +-- selector.go -- +package extract_all + +type MyStruct struct { + Value int +} + +func _() { + s := MyStruct{Value: 10} + v := s.Value //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) + if v > 0 { + w := s.Value //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) + _ = w + } +} +-- @sel/selector.go -- +@@ -9 +9,2 @@ +- v := s.Value //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) ++ newVar := s.Value ++ v := newVar //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) +@@ -11 +12 @@ +- w := s.Value //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) ++ w := newVar //@codeaction("s.Value", "refactor.extract.variable-all", edit=sel) +-- index.go -- +package extract_all + +func _() { + arr := []int{1, 2, 3} + val := arr[0] //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) + val2 := arr[0] //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) +} +-- @index/index.go -- +@@ -5,2 +5,3 @@ +- val := arr[0] //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) +- val2 := arr[0] //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) ++ newVar := arr[0] ++ val := newVar //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) ++ val2 := newVar //@codeaction("arr[0]", "refactor.extract.variable-all", edit=index) +-- slice_expr.go -- +package extract_all + +func _() { + data := []int{1, 2, 3, 4, 5} + part := data[1:3] //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) + anotherPart := data[1:3] //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) +} +-- @slice/slice_expr.go -- +@@ -5,2 +5,3 @@ +- part := data[1:3] //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) +- anotherPart := data[1:3] //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) ++ newVar := data[1:3] ++ part := newVar //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) ++ anotherPart := newVar //@codeaction("data[1:3]", "refactor.extract.variable-all", edit=slice) +-- nested_func.go -- +package extract_all + +func outer() { + inner := func() { + val := 100 + 200 //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) + _ = val + } + inner() + val := 100 + 200 //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) + _ = val +} +-- @nested/nested_func.go -- +@@ -4 +4 @@ ++ const newConst = 100 + 200 +@@ -5 +6 @@ +- val := 100 + 200 //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) ++ val := newConst //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) +@@ -9 +10 @@ +- val := 100 + 200 //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) ++ val := newConst //@codeaction("100 + 200", "refactor.extract.constant-all", edit=nested) +-- switch.go -- +package extract_all + +func _() { + value := 2 + switch value { + case 1: + result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) + _ = result + case 2: + result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) + _ = result + default: + result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) + _ = result + } +} +-- @switch/switch.go -- +@@ -5 +5 @@ ++ newVar := value * 10 +@@ -7 +8 @@ +- result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) ++ result := newVar //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) +@@ -10 +11 @@ +- result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) ++ result := newVar //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) +@@ -13 +14 @@ +- result := value * 10 //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) ++ result := newVar //@codeaction("value * 10", "refactor.extract.variable-all", edit=switch) +-- switch_single.go -- +package extract_all + +func _() { + value := 2 + switch value { + case 1: + result := value * 10 + _ = result + case 2: + result := value * 10 + _ = result + default: + result := value * 10 //@codeaction("value * 10", "refactor.extract.variable", edit=switch_single) + _ = result + } +} +-- @switch_single/switch_single.go -- +@@ -13 +13,2 @@ +- result := value * 10 //@codeaction("value * 10", "refactor.extract.variable", edit=switch_single) ++ newVar := value * 10 ++ result := newVar //@codeaction("value * 10", "refactor.extract.variable", edit=switch_single) +-- func_list.go -- +package extract_all + +func _() { + x := func(a int) int { //@codeaction("func", "refactor.extract.variable-all", end=closeBracket1, edit=func_list) + b := 1 + return b + a + } //@loc(closeBracket1, "}") + y := func(a int) int { //@codeaction("func", "refactor.extract.variable-all", end=closeBracket2, edit=func_list) + b := 1 + return b + a + }//@loc(closeBracket2, "}") +} +-- @func_list/func_list.go -- +@@ -4 +4 @@ +- x := func(a int) int { //@codeaction("func", "refactor.extract.variable-all", end=closeBracket1, edit=func_list) ++ newVar := func(a int) int { +@@ -7,5 +7,3 @@ +- } //@loc(closeBracket1, "}") +- y := func(a int) int { //@codeaction("func", "refactor.extract.variable-all", end=closeBracket2, edit=func_list) +- b := 1 +- return b + a +- }//@loc(closeBracket2, "}") ++ } ++ x := newVar //@loc(closeBracket1, "}") ++ y := newVar//@loc(closeBracket2, "}") 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 2bf1803a7d8..203b6d1eadc 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt @@ -26,13 +26,13 @@ func _() { -- @basic_lit1/basic_lit.go -- @@ -4 +4,2 @@ - var _ = 1 + 2 //@codeaction("1", "refactor.extract.constant", edit=basic_lit1) -+ const k = 1 -+ var _ = k + 2 //@codeaction("1", "refactor.extract.constant", edit=basic_lit1) ++ const newConst = 1 ++ var _ = newConst + 2 //@codeaction("1", "refactor.extract.constant", edit=basic_lit1) -- @basic_lit2/basic_lit.go -- @@ -5 +5,2 @@ - var _ = 3 + 4 //@codeaction("3 + 4", "refactor.extract.constant", edit=basic_lit2) -+ const k = 3 + 4 -+ var _ = k //@codeaction("3 + 4", "refactor.extract.constant", edit=basic_lit2) ++ const newConst = 3 + 4 ++ var _ = newConst //@codeaction("3 + 4", "refactor.extract.constant", edit=basic_lit2) -- func_call.go -- package extract @@ -47,13 +47,13 @@ func _() { -- @func_call1/func_call.go -- @@ -6 +6,2 @@ - x0 := append([]int{}, 1) //@codeaction("append([]int{}, 1)", "refactor.extract.variable", edit=func_call1) -+ x := append([]int{}, 1) -+ x0 := x //@codeaction("append([]int{}, 1)", "refactor.extract.variable", edit=func_call1) ++ newVar := append([]int{}, 1) ++ x0 := newVar //@codeaction("append([]int{}, 1)", "refactor.extract.variable", edit=func_call1) -- @func_call2/func_call.go -- @@ -8 +8,2 @@ - b, err := strconv.Atoi(str) //@codeaction("strconv.Atoi(str)", "refactor.extract.variable", edit=func_call2) -+ x, x1 := strconv.Atoi(str) -+ b, err := x, x1 //@codeaction("strconv.Atoi(str)", "refactor.extract.variable", edit=func_call2) ++ newVar, newVar1 := strconv.Atoi(str) ++ b, err := newVar, newVar1 //@codeaction("strconv.Atoi(str)", "refactor.extract.variable", edit=func_call2) -- scope.go -- package extract @@ -72,10 +72,10 @@ func _() { -- @scope1/scope.go -- @@ -8 +8,2 @@ - y := ast.CompositeLit{} //@codeaction("ast.CompositeLit{}", "refactor.extract.variable", edit=scope1) -+ x := ast.CompositeLit{} -+ y := x //@codeaction("ast.CompositeLit{}", "refactor.extract.variable", edit=scope1) ++ newVar := ast.CompositeLit{} ++ y := newVar //@codeaction("ast.CompositeLit{}", "refactor.extract.variable", edit=scope1) -- @scope2/scope.go -- @@ -11 +11,2 @@ - x := !false //@codeaction("!false", "refactor.extract.constant", edit=scope2) -+ const k = !false -+ x := k //@codeaction("!false", "refactor.extract.constant", edit=scope2) ++ const newConst = !false ++ x := newConst //@codeaction("!false", "refactor.extract.constant", edit=scope2) diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt index 2cbd49cffe4..600119dad8e 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt @@ -113,24 +113,27 @@ var _ = funStructEmpty{} //@codeaction("}", "refactor.rewrite.fillStruct", edit= + a: [2]string{}, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a21) -- @a22/a2.go -- -@@ -17 +17,4 @@ +@@ -17 +17,5 @@ -var _ = funStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a22) +var _ = funStruct{ + fn: func(i int) int { ++ panic("TODO") + }, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a22) -- @a23/a2.go -- -@@ -23 +23,4 @@ +@@ -23 +23,5 @@ -var _ = funStructComplex{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a23) +var _ = funStructComplex{ + fn: func(i int, s string) (string, int) { ++ panic("TODO") + }, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a23) -- @a24/a2.go -- -@@ -29 +29,4 @@ +@@ -29 +29,5 @@ -var _ = funStructEmpty{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a24) +var _ = funStructEmpty{ + fn: func() { ++ panic("TODO") + }, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a24) -- a3.go -- @@ -184,7 +187,7 @@ var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edi + Y: &Foo{}, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a31) -- @a32/a3.go -- -@@ -28 +28,9 @@ +@@ -28 +28,10 @@ -var _ = importedStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a32) +var _ = importedStruct{ + m: map[*ast.CompositeLit]ast.Field{}, @@ -192,6 +195,7 @@ var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edi + a: [3]token.Token{}, + c: make(chan ast.EmptyStmt), + fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { ++ panic("TODO") + }, + st: ast.CompositeLit{}, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a32) @@ -289,6 +293,8 @@ func fill() { -- fillStruct.go -- package fillstruct +type StructB struct{} + type StructA struct { unexportedIntField int ExportedIntField int @@ -315,7 +321,7 @@ func fill() { } -- @fillStruct1/fillStruct.go -- -@@ -20 +20,7 @@ +@@ -22 +22,7 @@ - a := StructA{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct1) + a := StructA{ + unexportedIntField: 0, @@ -325,19 +331,19 @@ func fill() { + StructB: StructB{}, + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct1) -- @fillStruct2/fillStruct.go -- -@@ -21 +21,3 @@ +@@ -23 +23,3 @@ - b := StructA2{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct2) + b := StructA2{ + B: &StructB{}, + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct2) -- @fillStruct3/fillStruct.go -- -@@ -22 +22,3 @@ +@@ -24 +24,3 @@ - c := StructA3{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct3) + c := StructA3{ + B: StructB{}, + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct3) -- @fillStruct4/fillStruct.go -- -@@ -24 +24,3 @@ +@@ -26 +26,3 @@ - _ = StructA3{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct4) + _ = StructA3{ + B: StructB{}, @@ -347,7 +353,7 @@ package fillstruct type StructAnon struct { a struct{} - b map[string]interface{} + b map[string]any c map[string]struct { d int e bool @@ -362,7 +368,7 @@ func fill() { - _ := StructAnon{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon) + _ := StructAnon{ + a: struct{}{}, -+ b: map[string]interface{}{}, ++ b: map[string]any{}, + c: map[string]struct{d int; e bool}{}, + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon) -- fillStruct_nested.go -- @@ -408,11 +414,12 @@ func unexported() { + ExportedInt: 0, + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_package1) -- @fillStruct_package2/fillStruct_package.go -- -@@ -11 +11,7 @@ +@@ -11 +11,8 @@ - _ = h2.Client{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_package2) + _ = h2.Client{ + Transport: nil, + CheckRedirect: func(req *h2.Request, via []*h2.Request) error { ++ panic("TODO") + }, + Jar: nil, + Timeout: 0, @@ -529,6 +536,21 @@ var _ = nestedStructWithTypeParams{} //@codeaction("}", "refactor.rewrite.fillSt func _[T any]() { type S struct{ t T } _ = S{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams5) + + type P struct{ t *T } + _ = P{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams6) + + type Alias[u any] = struct { + x u + y *T + } + _ = Alias[string]{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams7) + + type Named[u any] struct { + x u + y T + } + _ = Named[int]{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams8) } -- @typeparams1/typeparams.go -- @@ -11 +11,3 @@ @@ -551,7 +573,7 @@ func _[T any]() { -var _ = nestedStructWithTypeParams{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams4) +var _ = nestedStructWithTypeParams{ + bar: "", -+ basic: basicStructWithTypeParams{}, ++ basic: basicStructWithTypeParams[int]{}, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams4) -- @typeparams5/typeparams.go -- @@ -33 +33,3 @@ @@ -559,6 +581,26 @@ func _[T any]() { + _ = S{ + t: *new(T), + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams5) +-- @typeparams6/typeparams.go -- +@@ -36 +36,3 @@ +- _ = P{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams6) ++ _ = P{ ++ t: new(T), ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams6) +-- @typeparams7/typeparams.go -- +@@ -42 +42,4 @@ +- _ = Alias[string]{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams7) ++ _ = Alias[string]{ ++ x: "", ++ y: new(T), ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams7) +-- @typeparams8/typeparams.go -- +@@ -48 +48,4 @@ +- _ = Named[int]{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams8) ++ _ = Named[int]{ ++ x: 0, ++ y: *new(T), ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams8) -- issue63921.go -- package fillstruct @@ -573,3 +615,111 @@ func _() { // edits, but does not panic. invalidStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue63921) } +-- named/named.go -- +package named + +type foo struct {} +type aliasFoo = foo + +func _() { + type namedInt int + type namedString string + type namedBool bool + type namedPointer *foo + type namedSlice []foo + type namedInterface interface{ Error() string } + type namedChan chan int + type namedMap map[string]foo + type namedSignature func(string) string + type namedStruct struct{} + type namedArray [3]foo + type namedAlias aliasFoo + + type bar struct { + namedInt namedInt + namedString namedString + namedBool namedBool + namedPointer namedPointer + namedSlice namedSlice + namedInterface namedInterface + namedChan namedChan + namedMap namedMap + namedSignature namedSignature + namedStruct namedStruct + namedArray namedArray + namedAlias namedAlias + } + + bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=named) +} +-- @named/named/named.go -- +@@ -35 +35,14 @@ +- bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=named) ++ bar{ ++ namedInt: 0, ++ namedString: "", ++ namedBool: false, ++ namedPointer: nil, ++ namedSlice: namedSlice{}, ++ namedInterface: nil, ++ namedChan: nil, ++ namedMap: namedMap{}, ++ namedSignature: nil, ++ namedStruct: namedStruct{}, ++ namedArray: namedArray{}, ++ namedAlias: namedAlias{}, ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=named) +-- alias/alias.go -- +package alias + +type foo struct {} +type aliasFoo = foo + +func _() { + type aliasInt = int + type aliasString = string + type aliasBool = bool + type aliasPointer = *foo + type aliasSlice = []foo + type aliasInterface = interface{ Error() string } + type aliasChan = chan int + type aliasMap = map[string]foo + type aliasSignature = func(string) string + type aliasStruct = struct{ bar string } + type aliasArray = [3]foo + type aliasNamed = foo + + type bar struct { + aliasInt aliasInt + aliasString aliasString + aliasBool aliasBool + aliasPointer aliasPointer + aliasSlice aliasSlice + aliasInterface aliasInterface + aliasChan aliasChan + aliasMap aliasMap + aliasSignature aliasSignature + aliasStruct aliasStruct + aliasArray aliasArray + aliasNamed aliasNamed + } + + bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=alias) +} +-- @alias/alias/alias.go -- +@@ -35 +35,14 @@ +- bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=alias) ++ bar{ ++ aliasInt: 0, ++ aliasString: "", ++ aliasBool: false, ++ aliasPointer: nil, ++ aliasSlice: aliasSlice{}, ++ aliasInterface: nil, ++ aliasChan: nil, ++ aliasMap: aliasMap{}, ++ aliasSignature: nil, ++ aliasStruct: aliasStruct{}, ++ aliasArray: aliasArray{}, ++ aliasNamed: aliasNamed{}, ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=alias) 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 843bb20252d..6d1250e26aa 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt @@ -124,24 +124,27 @@ var _ = funStructEmpty{} //@codeaction("}", "refactor.rewrite.fillStruct", edit= + a: [2]string{}, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a21) -- @a22/a2.go -- -@@ -17 +17,4 @@ +@@ -17 +17,5 @@ -var _ = funStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a22) +var _ = funStruct{ + fn: func(i int) int { ++ panic("TODO") + }, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a22) -- @a23/a2.go -- -@@ -23 +23,4 @@ +@@ -23 +23,5 @@ -var _ = funStructComplex{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a23) +var _ = funStructComplex{ + fn: func(i int, s string) (string, int) { ++ panic("TODO") + }, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a23) -- @a24/a2.go -- -@@ -29 +29,4 @@ +@@ -29 +29,5 @@ -var _ = funStructEmpty{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a24) +var _ = funStructEmpty{ + fn: func() { ++ panic("TODO") + }, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a24) -- a3.go -- @@ -195,7 +198,7 @@ var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edi + Y: &Foo{}, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a31) -- @a32/a3.go -- -@@ -28 +28,9 @@ +@@ -28 +28,10 @@ -var _ = importedStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a32) +var _ = importedStruct{ + m: map[*ast.CompositeLit]ast.Field{}, @@ -203,6 +206,7 @@ var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edi + a: [3]token.Token{}, + c: make(chan ast.EmptyStmt), + fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { ++ panic("TODO") + }, + st: ast.CompositeLit{}, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a32) @@ -358,7 +362,7 @@ package fillstruct type StructAnon struct { a struct{} - b map[string]interface{} + b map[string]any c map[string]struct { d int e bool @@ -373,7 +377,7 @@ func fill() { - _ := StructAnon{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon) + _ := StructAnon{ + a: struct{}{}, -+ b: map[string]interface{}{}, ++ b: map[string]any{}, + c: map[string]struct{d int; e bool}{}, + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon) -- fillStruct_nested.go -- @@ -419,11 +423,12 @@ func unexported() { + ExportedInt: 0, + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_package1) -- @fillStruct_package2/fillStruct_package.go -- -@@ -11 +11,7 @@ +@@ -11 +11,8 @@ - _ = h2.Client{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_package2) + _ = h2.Client{ + Transport: nil, + CheckRedirect: func(req *h2.Request, via []*h2.Request) error { ++ panic("TODO") + }, + Jar: nil, + Timeout: 0, @@ -540,6 +545,21 @@ var _ = nestedStructWithTypeParams{} //@codeaction("}", "refactor.rewrite.fillSt func _[T any]() { type S struct{ t T } _ = S{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams5) + + type P struct{ t *T } + _ = P{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams6) + + type Alias[u any] = struct { + x u + y *T + } + _ = Alias[string]{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams7) + + type Named[u any] struct { + x u + y T + } + _ = Named[int]{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams8) } -- @typeparams1/typeparams.go -- @@ -11 +11,3 @@ @@ -562,7 +582,7 @@ func _[T any]() { -var _ = nestedStructWithTypeParams{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams4) +var _ = nestedStructWithTypeParams{ + bar: "", -+ basic: basicStructWithTypeParams{}, ++ basic: basicStructWithTypeParams[int]{}, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams4) -- @typeparams5/typeparams.go -- @@ -33 +33,3 @@ @@ -570,6 +590,26 @@ func _[T any]() { + _ = S{ + t: *new(T), + } //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams5) +-- @typeparams6/typeparams.go -- +@@ -36 +36,3 @@ +- _ = P{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams6) ++ _ = P{ ++ t: new(T), ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams6) +-- @typeparams7/typeparams.go -- +@@ -42 +42,4 @@ +- _ = Alias[string]{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams7) ++ _ = Alias[string]{ ++ x: "", ++ y: new(T), ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams7) +-- @typeparams8/typeparams.go -- +@@ -48 +48,4 @@ +- _ = Named[int]{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams8) ++ _ = Named[int]{ ++ x: 0, ++ y: *new(T), ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams8) -- issue63921.go -- package fillstruct @@ -584,3 +624,111 @@ func _() { // edits, but does not panic. invalidStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue63921) } +-- named/named.go -- +package named + +type foo struct {} +type aliasFoo = foo + +func _() { + type namedInt int + type namedString string + type namedBool bool + type namedPointer *foo + type namedSlice []foo + type namedInterface interface{ Error() string } + type namedChan chan int + type namedMap map[string]foo + type namedSignature func(string) string + type namedStruct struct{} + type namedArray [3]foo + type namedAlias aliasFoo + + type bar struct { + namedInt namedInt + namedString namedString + namedBool namedBool + namedPointer namedPointer + namedSlice namedSlice + namedInterface namedInterface + namedChan namedChan + namedMap namedMap + namedSignature namedSignature + namedStruct namedStruct + namedArray namedArray + namedAlias namedAlias + } + + bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=named) +} +-- @named/named/named.go -- +@@ -35 +35,14 @@ +- bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=named) ++ bar{ ++ namedInt: 0, ++ namedString: "", ++ namedBool: false, ++ namedPointer: nil, ++ namedSlice: namedSlice{}, ++ namedInterface: nil, ++ namedChan: nil, ++ namedMap: namedMap{}, ++ namedSignature: nil, ++ namedStruct: namedStruct{}, ++ namedArray: namedArray{}, ++ namedAlias: namedAlias{}, ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=named) +-- alias/alias.go -- +package alias + +type foo struct {} +type aliasFoo = foo + +func _() { + type aliasInt = int + type aliasString = string + type aliasBool = bool + type aliasPointer = *foo + type aliasSlice = []foo + type aliasInterface = interface{ Error() string } + type aliasChan = chan int + type aliasMap = map[string]foo + type aliasSignature = func(string) string + type aliasStruct = struct{ bar string } + type aliasArray = [3]foo + type aliasNamed = foo + + type bar struct { + aliasInt aliasInt + aliasString aliasString + aliasBool aliasBool + aliasPointer aliasPointer + aliasSlice aliasSlice + aliasInterface aliasInterface + aliasChan aliasChan + aliasMap aliasMap + aliasSignature aliasSignature + aliasStruct aliasStruct + aliasArray aliasArray + aliasNamed aliasNamed + } + + bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=alias) +} +-- @alias/alias/alias.go -- +@@ -35 +35,14 @@ +- bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=alias) ++ bar{ ++ aliasInt: 0, ++ aliasString: "", ++ aliasBool: false, ++ aliasPointer: nil, ++ aliasSlice: aliasSlice{}, ++ aliasInterface: nil, ++ aliasChan: nil, ++ aliasMap: aliasMap{}, ++ aliasSignature: nil, ++ aliasStruct: aliasStruct{}, ++ aliasArray: aliasArray{}, ++ aliasNamed: aliasNamed{}, ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=alias) diff --git a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt index f84eeae7b4c..73276cbd03b 100644 --- a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt +++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt @@ -367,6 +367,8 @@ func newFunction1() int { return 1 } +var _ = newFunction1 + -- @scope/scope.go -- package extract @@ -385,6 +387,8 @@ func newFunction1() int { return 1 } +var _ = newFunction1 + -- smart_initialization.go -- package extract diff --git a/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue50851.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue50851.txt index b085559cf2a..52a4b412055 100644 --- a/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue50851.txt +++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue50851.txt @@ -6,7 +6,7 @@ package main type F struct{} -func (f *F) func1() { +func (f *F) _() { println("a") println("b") //@ codeaction("print", "refactor.extract.function", end=end, result=result) @@ -20,7 +20,7 @@ package main type F struct{} -func (f *F) func1() { +func (f *F) _() { println("a") newFunction() //@loc(end, ")") diff --git a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt index 766b13b7f56..4817d8d7241 100644 --- a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt +++ b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt @@ -79,7 +79,7 @@ package func_call import "fmt" -func a() { +func F() { fmt.Println( 1 /*@codeaction("1", "refactor.rewrite.joinLines", result=func_call)*/, 2, @@ -93,7 +93,7 @@ package func_call import "fmt" -func a() { +func F() { fmt.Println(1 /*@codeaction("1", "refactor.rewrite.joinLines", result=func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4)) } @@ -102,7 +102,7 @@ package indent import "fmt" -func a() { +func F() { fmt.Println( 1, 2, @@ -118,7 +118,7 @@ package indent import "fmt" -func a() { +func F() { fmt.Println( 1, 2, @@ -134,7 +134,7 @@ type A struct{ b int } -func a() { +func F() { _ = A{ a: 1, b: 2 /*@codeaction("b", "refactor.rewrite.joinLines", result=structelts)*/, @@ -149,14 +149,14 @@ type A struct{ b int } -func a() { +func F() { _ = A{a: 1, b: 2 /*@codeaction("b", "refactor.rewrite.joinLines", result=structelts)*/} } -- sliceelts/sliceelts.go -- package sliceelts -func a() { +func F() { _ = []int{ 1 /*@codeaction("1", "refactor.rewrite.joinLines", result=sliceelts)*/, 2, @@ -166,14 +166,14 @@ func a() { -- @sliceelts/sliceelts/sliceelts.go -- package sliceelts -func a() { +func F() { _ = []int{1 /*@codeaction("1", "refactor.rewrite.joinLines", result=sliceelts)*/, 2} } -- mapelts/mapelts.go -- package mapelts -func a() { +func F() { _ = map[string]int{ "a": 1 /*@codeaction("1", "refactor.rewrite.joinLines", result=mapelts)*/, "b": 2, @@ -183,7 +183,7 @@ func a() { -- @mapelts/mapelts/mapelts.go -- package mapelts -func a() { +func F() { _ = map[string]int{"a": 1 /*@codeaction("1", "refactor.rewrite.joinLines", result=mapelts)*/, "b": 2} } diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_issue67336.txt b/gopls/internal/test/marker/testdata/codeaction/inline_issue67336.txt index daae6e41144..437fb474fb2 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline_issue67336.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline_issue67336.txt @@ -44,7 +44,7 @@ const ( someConst = 5 ) -func foo() { +func _() { inlined.Baz(context.TODO()) //@ codeaction("Baz", "refactor.inline.call", result=inline) pkg.Bar() } @@ -65,7 +65,7 @@ const ( someConst = 5 ) -func foo() { +func _() { var _ context.Context = context.TODO() pkg0.Foo(typ.T(5)) //@ codeaction("Baz", "refactor.inline.call", result=inline) pkg.Bar() diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_issue68554.txt b/gopls/internal/test/marker/testdata/codeaction/inline_issue68554.txt index 49b18b27935..868b30fce85 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline_issue68554.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline_issue68554.txt @@ -8,7 +8,7 @@ import ( "io" ) -func f(d discard) { +func _(d discard) { g(d) //@codeaction("g", "refactor.inline.call", result=out) } @@ -25,7 +25,7 @@ import ( "io" ) -func f(d discard) { +func _(d discard) { fmt.Println(d) //@codeaction("g", "refactor.inline.call", result=out) } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt index c8fddb0fff7..8bebfc29c40 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt @@ -60,6 +60,8 @@ func _() { a.A(f(), 1) } +var _ = g + -- @a/a/a2.go -- package a @@ -96,6 +98,8 @@ func g() int { func _() { a.A(f()) } + +var _ = g -- field/field.go -- package field diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt index b51dd6fb8cf..3d92d758b13 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt @@ -71,6 +71,8 @@ func _() { a.A(f(), 1) } +var _ = g + -- @a/a/a2.go -- package a @@ -107,6 +109,8 @@ func g() int { func _() { a.A(f()) } + +var _ = g -- field/field.go -- package field diff --git a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt index f0f6ef6091c..65178715bb0 100644 --- a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt +++ b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt @@ -79,7 +79,7 @@ package func_call import "fmt" -func a() { +func F() { fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "refactor.rewrite.splitLines", result=func_call) } @@ -88,7 +88,7 @@ package func_call import "fmt" -func a() { +func F() { fmt.Println( 1, 2, @@ -102,7 +102,7 @@ package indent import "fmt" -func a() { +func F() { fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("hello", "refactor.rewrite.splitLines", result=indent) } @@ -111,7 +111,7 @@ package indent import "fmt" -func a() { +func F() { fmt.Println(1, 2, 3, fmt.Sprintf( "hello %d", 4, @@ -123,7 +123,7 @@ package indent2 import "fmt" -func a() { +func F() { fmt. Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "refactor.rewrite.splitLines", result=indent2) } @@ -133,7 +133,7 @@ package indent2 import "fmt" -func a() { +func F() { fmt. Println( 1, @@ -151,7 +151,7 @@ type A struct{ b int } -func a() { +func F() { _ = A{a: 1, b: 2} //@codeaction("b", "refactor.rewrite.splitLines", result=structelts) } @@ -163,7 +163,7 @@ type A struct{ b int } -func a() { +func F() { _ = A{ a: 1, b: 2, @@ -173,14 +173,14 @@ func a() { -- sliceelts/sliceelts.go -- package sliceelts -func a() { +func F() { _ = []int{1, 2} //@codeaction("1", "refactor.rewrite.splitLines", result=sliceelts) } -- @sliceelts/sliceelts/sliceelts.go -- package sliceelts -func a() { +func F() { _ = []int{ 1, 2, @@ -190,14 +190,14 @@ func a() { -- mapelts/mapelts.go -- package mapelts -func a() { +func F() { _ = map[string]int{"a": 1, "b": 2} //@codeaction("1", "refactor.rewrite.splitLines", result=mapelts) } -- @mapelts/mapelts/mapelts.go -- package mapelts -func a() { +func F() { _ = map[string]int{ "a": 1, "b": 2, diff --git a/gopls/internal/test/marker/testdata/codelens/test.txt b/gopls/internal/test/marker/testdata/codelens/test.txt index ba68cf019df..60d573a81e5 100644 --- a/gopls/internal/test/marker/testdata/codelens/test.txt +++ b/gopls/internal/test/marker/testdata/codelens/test.txt @@ -30,3 +30,9 @@ func BenchmarkFuncWithCodeLens(b *testing.B) { //@codelens(re"()func", "run benc } func helper() {} // expect no code lens + +func _() { + // pacify unusedfunc + thisShouldNotHaveACodeLens(nil) + helper() +} diff --git a/gopls/internal/test/marker/testdata/completion/append.txt b/gopls/internal/test/marker/testdata/completion/append.txt index 89172211314..54937e43d08 100644 --- a/gopls/internal/test/marker/testdata/completion/append.txt +++ b/gopls/internal/test/marker/testdata/completion/append.txt @@ -22,7 +22,7 @@ func _() { ) append(aStrings, a) //@rank(")", appendString, appendInt) - var _ interface{} = append(aStrings, a) //@rank(")", appendString, appendInt) + var _ any = append(aStrings, a) //@rank(")", appendString, appendInt) var _ []string = append(oops, a) //@rank(")", appendString, appendInt) foo(append()) //@rank("))", appendStrings, appendInt),rank("))", appendStrings, appendString) diff --git a/gopls/internal/test/marker/testdata/completion/labels.txt b/gopls/internal/test/marker/testdata/completion/labels.txt index 3caaa5a211a..2e12072d77b 100644 --- a/gopls/internal/test/marker/testdata/completion/labels.txt +++ b/gopls/internal/test/marker/testdata/completion/labels.txt @@ -36,7 +36,7 @@ Foo1: //@item(label1, "Foo1", "label", "const") } Foo4: //@item(label4, "Foo4", "label", "const") - switch interface{}(a).(type) { + switch any(a).(type) { case int: break F //@complete(" //", label4, label1) } diff --git a/gopls/internal/test/marker/testdata/completion/multi_return.txt b/gopls/internal/test/marker/testdata/completion/multi_return.txt index 72facfcf6f3..0a83a126fd6 100644 --- a/gopls/internal/test/marker/testdata/completion/multi_return.txt +++ b/gopls/internal/test/marker/testdata/completion/multi_return.txt @@ -46,7 +46,7 @@ func _() { } func _() { - var baz func(...interface{}) + var baz func(...any) var otterNap func() (int, int) //@item(multiTwo, "otterNap", "func() (int, int)", "var") var one int //@item(multiOne, "one", "int", "var") diff --git a/gopls/internal/test/marker/testdata/completion/printf.txt b/gopls/internal/test/marker/testdata/completion/printf.txt index 270927e8211..61b464a92b9 100644 --- a/gopls/internal/test/marker/testdata/completion/printf.txt +++ b/gopls/internal/test/marker/testdata/completion/printf.txt @@ -8,7 +8,7 @@ package printf import "fmt" -func myPrintf(string, ...interface{}) {} +func myPrintf(string, ...any) {} func _() { var ( diff --git a/gopls/internal/test/marker/testdata/completion/rank.txt b/gopls/internal/test/marker/testdata/completion/rank.txt index 03ea565a400..48ced6fb5d5 100644 --- a/gopls/internal/test/marker/testdata/completion/rank.txt +++ b/gopls/internal/test/marker/testdata/completion/rank.txt @@ -122,8 +122,8 @@ func _() { var mu myUint mu = conv //@rank(" //", convertD, convertE) - // don't downrank constants when assigning to interface{} - var _ interface{} = c //@rank(" //", convertD, complex) + // don't downrank constants when assigning to any + var _ any = c //@rank(" //", convertD, complex) var _ time.Duration = conv //@rank(" //", convertD, convertE),snippet(" //", convertE, "time.Duration(convE)") diff --git a/gopls/internal/test/marker/testdata/completion/statements.txt b/gopls/internal/test/marker/testdata/completion/statements.txt index 9856d938ea3..f189e8ec27f 100644 --- a/gopls/internal/test/marker/testdata/completion/statements.txt +++ b/gopls/internal/test/marker/testdata/completion/statements.txt @@ -98,6 +98,20 @@ func two() error { //@snippet("", stmtTwoIfErrReturn, "if s.err != nil {\n\treturn ${1:s.err}\n\\}") } +-- if_err_check_return3.go -- +package statements + +import "os" + +// Check that completion logic handles an invalid return type. +func badReturn() (NotAType, error) { + _, err := os.Open("foo") + //@snippet("", stmtOneIfErrReturn, "if err != nil {\n\treturn , ${1:err}\n\\}") + + _, err = os.Open("foo") + if er //@snippet(" //", stmtOneErrReturn, "err != nil {\n\treturn , ${1:err}\n\\}") +} + -- if_err_check_test.go -- package statements @@ -132,3 +146,10 @@ func foo() (int, string, error) { func bar() (int, string, error) { return //@snippet(" ", stmtReturnZeroValues, "return ${1:0}, ${2:\"\"}, ${3:nil}") } + + +//@item(stmtReturnInvalidValues, `return `) + +func invalidReturnStatement() NotAType { + return //@snippet(" ", stmtReturnInvalidValues, "return ${1:}") +} diff --git a/gopls/internal/test/marker/testdata/completion/type_params.txt b/gopls/internal/test/marker/testdata/completion/type_params.txt index 8e2f5d7e401..12d3634181f 100644 --- a/gopls/internal/test/marker/testdata/completion/type_params.txt +++ b/gopls/internal/test/marker/testdata/completion/type_params.txt @@ -15,11 +15,13 @@ package typeparams func one[a int | string]() {} func two[a int | string, b float64 | int]() {} +type three[a any] int func _() { one[]() //@rank("]", string, float64) two[]() //@rank("]", int, float64) two[int, f]() //@rank("]", float64, float32) + int(three[]) //@rank("]") // must not crash (golang/go#70889) } func slices[a []int | []float64]() {} //@item(tpInts, "[]int", "[]int", "type"),item(tpFloats, "[]float64", "[]float64", "type") diff --git a/gopls/internal/test/marker/testdata/completion/unresolved.txt b/gopls/internal/test/marker/testdata/completion/unresolved.txt index d509b2670c4..da5a0a65a8c 100644 --- a/gopls/internal/test/marker/testdata/completion/unresolved.txt +++ b/gopls/internal/test/marker/testdata/completion/unresolved.txt @@ -11,6 +11,6 @@ This test verifies gopls does not crash on fake "resolved" types. -- unresolved.go -- package unresolved -func foo(interface{}) { +func foo(any) { foo(func(i, j f //@complete(" //") } diff --git a/gopls/internal/test/marker/testdata/completion/variadic.txt b/gopls/internal/test/marker/testdata/completion/variadic.txt index 0b2ae8212df..2e7ec3634ee 100644 --- a/gopls/internal/test/marker/testdata/completion/variadic.txt +++ b/gopls/internal/test/marker/testdata/completion/variadic.txt @@ -17,7 +17,7 @@ func _() { i int //@item(vInt, "i", "int", "var") s string //@item(vStr, "s", "string", "var") ss []string //@item(vStrSlice, "ss", "[]string", "var") - v interface{} //@item(vIntf, "v", "interface{}", "var") + v any //@item(vIntf, "v", "any", "var") ) foo() //@rank(")", vInt, vStr),rank(")", vInt, vStrSlice) @@ -28,7 +28,7 @@ func _() { // snippet will add the "..." for you foo(123, ) //@snippet(")", vStrSlice, "ss..."),snippet(")", vFunc, "bar()..."),snippet(")", vStr, "s") - // don't add "..." for interface{} + // don't add "..." for any foo(123, ) //@snippet(")", vIntf, "v") } diff --git a/gopls/internal/test/marker/testdata/definition/branch.txt b/gopls/internal/test/marker/testdata/definition/branch.txt new file mode 100644 index 00000000000..e80c83a92ae --- /dev/null +++ b/gopls/internal/test/marker/testdata/definition/branch.txt @@ -0,0 +1,168 @@ +This test checks definition operations in branch statements break, goto and continue. + +-- go.mod -- +module mod.com + +go 1.18 + +-- a/a.go -- +package a + +import "log" + +func BreakLoop() { + for i := 0; i < 10; i++ { + if i > 6 { + break //@def("break", rbrace1) + } + } //@loc(rbrace1, `}`) +} + +func BreakNestedLoop() { + for i := 0; i < 10; i++ { + for j := 0; j < 5; j++ { + if j > 1 { + break //@def("break", rbrace2) + } + } //@loc(rbrace2, `}`) + } +} + +func BreakNestedLoopWithLabel() { + Outer: + for i := 0; i < 10; i++ { + for j := 0; j < 5; j++ { + if j > 1 { + break Outer//@def("break", outerparen) + } + } + } //@loc(outerparen, `}`) +} + +func BreakSwitch(i int) { + switch i { + case 1: + break //@def("break", rbrace4) + case 2: + log.Printf("2") + case 3: + log.Printf("3") + } //@loc(rbrace4, `}`) +} + +func BreakSwitchLabel(i int) { +loop: + for { + switch i { + case 1: + break loop //@def("break", loopparen) + case 2: + log.Printf("2") + case 3: + continue loop + } + } //@loc(loopparen, `}`) +} + +func BreakSelect(c, quit chan int) { + x, y := 0, 1 + for { + select { + case c <- x: + x, y = y, x+y + break //@def("break", rbrace5) + case <-quit: + log.Println("quit") + return + } //@loc(rbrace5, `}`) + } +} + +func BreakWithContinue() { + for j := 0; j < 5; j++ { + if (j < 4) { + continue + } + break //@def("break", rbrace6) + } //@loc(rbrace6, `}`) +} + +func GotoNestedLoop() { + Outer: //@loc(outer, "Outer") + for i := 0; i < 10; i++ { + for j := 0; j < 5; j++ { + if (j > 1) { + goto Outer//@def("goto", outer) + } + } + } +} + +func ContinueLoop() { + for j := 0; j < 5; j++ { //@loc(for3, `for`) + if (j < 4) { + continue //@def("continue", for3) + } + break + } +} + +func ContinueDoubleLoop() { + for i := 0; i < 10; i++ { //@loc(for4, `for`) + for j := 0; j < 5; j++ { + if (j > 1) { + break + } + } + if (i > 7) { + continue//@def("continue", for4) + } + } +} + +func BreakInBlockStmt() { + for { + if 0 < 10 { + { + break //@def("break", rbrace9) + } + } + } //@loc(rbrace9, `}`) +} + +func BreakInLabeledStmt() { + outer: + for { + goto inner + inner: + break outer //@def("break", for5) + } //@loc(for5, `}`) +} + +func BreakToLabel(n int) { + outer1: + switch n { + case 1: + print("1") + for i := 0; i < 10; i++ { + if i > 3 { + break outer1 //@def("break", outer1) + } + } + } //@loc(outer1, "}") +} + +func ContinueToLabel(n int) { + outer1: + for { //@loc(outer2, "for") + switch n { + case 1: + print("1") + for i := 0; i < 10; i++ { + if i > 3 { + continue outer1 //@def("continue", outer2) + } + } + } + } +} diff --git a/gopls/internal/test/marker/testdata/definition/return.txt b/gopls/internal/test/marker/testdata/definition/return.txt new file mode 100644 index 00000000000..e61c77d5b6f --- /dev/null +++ b/gopls/internal/test/marker/testdata/definition/return.txt @@ -0,0 +1,23 @@ +This test checks definition operations in function return statements. +Go to definition on 'return' should go to the result parameter list. + +-- go.mod -- +module mod.com + +go 1.18 + +-- a/a.go -- +package a + +func Hi() string { //@loc(HiReturn, "string") + return "Hello" //@def("return", HiReturn) +} + +func Bye() (int, int, int) { //@loc(ByeReturn, "(int, int, int)") + return 1, 2, 3 //@def("return", ByeReturn) +} + +func TestLit() { + f := func(a, b int) bool { return a*b < 100 } //@loc(FuncLitReturn, "bool"),def("return", FuncLitReturn) + f(1, 2) +} diff --git a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt index 76f65a4ecd7..7ba338032e9 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt @@ -8,7 +8,7 @@ copylocks, printf, slog, tests, timeformat, nilness, and cgocall. -- go.mod -- module example.com -go 1.12 +go 1.18 -- flags -- -cgo @@ -35,7 +35,7 @@ func _() { 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{}) { +func printfWrapper(format string, args ...any) { fmt.Printf(format, args...) } diff --git a/gopls/internal/test/marker/testdata/fixedbugs/issue59944.txt b/gopls/internal/test/marker/testdata/fixedbugs/issue59944.txt index c4cd4409bf0..7bd5070dd60 100644 --- a/gopls/internal/test/marker/testdata/fixedbugs/issue59944.txt +++ b/gopls/internal/test/marker/testdata/fixedbugs/issue59944.txt @@ -32,6 +32,6 @@ type Layout = C.struct_layout // Bindingf is a printf wrapper. This was necessary to trigger the panic in // objectpath while encoding facts. -func (l *Layout) Bindingf(format string, args ...interface{}) { +func (l *Layout) Bindingf(format string, args ...any) { fmt.Printf(format, args...) } diff --git a/gopls/internal/test/marker/testdata/foldingrange/a.txt b/gopls/internal/test/marker/testdata/foldingrange/a.txt index 2946767ec30..864442e1b0c 100644 --- a/gopls/internal/test/marker/testdata/foldingrange/a.txt +++ b/gopls/internal/test/marker/testdata/foldingrange/a.txt @@ -12,9 +12,9 @@ import ( import _ "os" -// bar is a function. +// Bar is a function. // With a multiline doc comment. -func bar() ( +func Bar() ( string, ) { /* This is a single line comment */ @@ -85,15 +85,15 @@ func _() { slice := []int{1, 2, 3} sort.Slice(slice, func(i, j int) bool { a, b := slice[i], slice[j] - return a < b + return a > b }) - sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] }) + sort.Slice(slice, func(i, j int) bool { return slice[i] > slice[j] }) sort.Slice( slice, func(i, j int) bool { - return slice[i] < slice[j] + return slice[i] > slice[j] }, ) @@ -143,9 +143,9 @@ import (<0 kind="imports"> import _ "os" -// bar is a function.<1 kind="comment"> +// Bar is a function.<1 kind="comment"> // With a multiline doc comment. -func bar() (<2 kind=""> +func Bar() (<2 kind=""> string, ) {<3 kind=""> /* This is a single line comment */ @@ -216,15 +216,15 @@ func _() {<35 kind=""> slice := []int{<36 kind="">1, 2, 3} sort.Slice(<37 kind="">slice, func(<38 kind="">i, j int) bool {<39 kind=""> a, b := slice[i], slice[j] - return a < b + return a > b }) - sort.Slice(<40 kind="">slice, func(<41 kind="">i, j int) bool {<42 kind=""> return slice[i] < slice[j] }) + sort.Slice(<40 kind="">slice, func(<41 kind="">i, j int) bool {<42 kind=""> return slice[i] > slice[j] }) sort.Slice(<43 kind=""> slice, func(<44 kind="">i, j int) bool {<45 kind=""> - return slice[i] < slice[j] + return slice[i] > slice[j] }, ) diff --git a/gopls/internal/test/marker/testdata/foldingrange/a_lineonly.txt b/gopls/internal/test/marker/testdata/foldingrange/a_lineonly.txt index fde2fb29c27..909dbc814bf 100644 --- a/gopls/internal/test/marker/testdata/foldingrange/a_lineonly.txt +++ b/gopls/internal/test/marker/testdata/foldingrange/a_lineonly.txt @@ -21,9 +21,9 @@ import ( import _ "os" -// bar is a function. +// Bar is a function. // With a multiline doc comment. -func bar() string { +func Bar() string { /* This is a single line comment */ switch { case true: @@ -92,15 +92,15 @@ func _() { slice := []int{1, 2, 3} sort.Slice(slice, func(i, j int) bool { a, b := slice[i], slice[j] - return a < b + return a > b }) - sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] }) + sort.Slice(slice, func(i, j int) bool { return slice[i] > slice[j] }) sort.Slice( slice, func(i, j int) bool { - return slice[i] < slice[j] + return slice[i] > slice[j] }, ) @@ -150,9 +150,9 @@ import (<0 kind="imports"> import _ "os" -// bar is a function.<1 kind="comment"> +// Bar is a function.<1 kind="comment"> // With a multiline doc comment. -func bar() string {<2 kind=""> +func Bar() string {<2 kind=""> /* This is a single line comment */ switch {<3 kind=""> case true:<4 kind=""> @@ -221,15 +221,15 @@ func _() {<23 kind=""> slice := []int{1, 2, 3} sort.Slice(slice, func(i, j int) bool {<24 kind=""> a, b := slice[i], slice[j] - return a < b + return a > b }) - sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] }) + sort.Slice(slice, func(i, j int) bool { return slice[i] > slice[j] }) sort.Slice(<25 kind=""> slice, func(i, j int) bool {<26 kind=""> - return slice[i] < slice[j] + return slice[i] > slice[j] }, ) diff --git a/gopls/internal/test/marker/testdata/foldingrange/bad.txt b/gopls/internal/test/marker/testdata/foldingrange/bad.txt index 14444e7aa44..fa18f1bc2c2 100644 --- a/gopls/internal/test/marker/testdata/foldingrange/bad.txt +++ b/gopls/internal/test/marker/testdata/foldingrange/bad.txt @@ -11,8 +11,8 @@ import ( "fmt" import ( _ "os" ) -// badBar is a function. -func badBar() string { x := true +// BadBar is a function. +func BadBar() string { x := true if x { // This is the only foldable thing in this file when lineFoldingOnly fmt.Println("true") @@ -30,8 +30,8 @@ import (<0 kind="imports"> "fmt" import (<1 kind="imports"> _ "os" ) -// badBar is a function. -func badBar() string {<2 kind=""> x := true +// BadBar is a function. +func BadBar() string {<2 kind=""> x := true if x {<3 kind=""> // This is the only foldable thing in this file when lineFoldingOnly fmt.Println(<4 kind="">"true") diff --git a/gopls/internal/test/marker/testdata/highlight/highlight_kind.txt b/gopls/internal/test/marker/testdata/highlight/highlight_kind.txt index bd059f77450..880e5bd720e 100644 --- a/gopls/internal/test/marker/testdata/highlight/highlight_kind.txt +++ b/gopls/internal/test/marker/testdata/highlight/highlight_kind.txt @@ -15,7 +15,7 @@ type MyMap map[string]string type NestMap map[Nest]Nest -func highlightTest() { +func _() { const constIdent = 1 //@hiloc(constIdent, "constIdent", write) //@highlightall(constIdent) var varNoInit int //@hiloc(varNoInit, "varNoInit", write) diff --git a/gopls/internal/test/marker/testdata/hover/comment.txt b/gopls/internal/test/marker/testdata/hover/comment.txt index 86a268f5981..c6eddf37962 100644 --- a/gopls/internal/test/marker/testdata/hover/comment.txt +++ b/gopls/internal/test/marker/testdata/hover/comment.txt @@ -25,8 +25,8 @@ func Conv(s string) int { return int(i) } -// unsafeConv converts s to a byte slice using [unsafe.Pointer]. hover("Pointer", "Pointer", unsafePointer) -func unsafeConv(s string) []byte { +// UnsafeConv converts s to a byte slice using [unsafe.Pointer]. hover("Pointer", "Pointer", unsafePointer) +func UnsafeConv(s string) []byte { p := unsafe.StringData(s) b := unsafe.Slice(p, len(s)) return b diff --git a/gopls/internal/test/marker/testdata/hover/embed.txt b/gopls/internal/test/marker/testdata/hover/embed.txt index 3f4086c2332..2abc25bfcad 100644 --- a/gopls/internal/test/marker/testdata/hover/embed.txt +++ b/gopls/internal/test/marker/testdata/hover/embed.txt @@ -34,6 +34,8 @@ func (P) m() {} var p P //@hover("P", "P", P) +var _ = P.m + -- @P -- ```go type P struct { diff --git a/gopls/internal/test/marker/testdata/hover/generics.txt b/gopls/internal/test/marker/testdata/hover/generics.txt index 50a7c706ee0..81e0c993ab6 100644 --- a/gopls/internal/test/marker/testdata/hover/generics.txt +++ b/gopls/internal/test/marker/testdata/hover/generics.txt @@ -107,7 +107,7 @@ type parameter P interface{~int | string} -- inferred.go -- package generics -func app[S interface{ ~[]E }, E interface{}](s S, e E) S { +func app[S interface{ ~[]E }, E any](s S, e E) S { return append(s, e) } @@ -120,5 +120,5 @@ func _() { -- @appint -- ```go -func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S +func app(s []int, e int) []int // func[S interface{~[]E}, E any](s S, e E) S ``` diff --git a/gopls/internal/test/marker/testdata/hover/godef.txt b/gopls/internal/test/marker/testdata/hover/godef.txt index e7bf4817fd6..ff7c8fbb663 100644 --- a/gopls/internal/test/marker/testdata/hover/godef.txt +++ b/gopls/internal/test/marker/testdata/hover/godef.txt @@ -173,7 +173,7 @@ import "fmt" func TypeStuff() { var x string - switch y := interface{}(x).(type) { //@loc(y, "y"), hover("y", "y", y) , def("y", y) + switch y := any(x).(type) { //@loc(y, "y"), hover("y", "y", y) , def("y", y) case int: //@loc(intY, "int") fmt.Printf("%v", y) //@hover("y", "y", inty), def("y", y) case string: //@loc(stringY, "string") @@ -191,7 +191,7 @@ var y string ``` -- @y -- ```go -var y interface{} +var y any ``` -- a/h.go -- package a @@ -310,7 +310,7 @@ func _() { // test key key string //@loc(testInputKey, "key") // test value - value interface{} //@loc(testInputValue, "value") + value any //@loc(testInputValue, "value") } result struct { v <-chan struct { @@ -413,7 +413,7 @@ def(bFunc, Things) */ func _() { - var x interface{} + var x any switch x := x.(type) { //@hover("x", "x", xInterface) case string: //@loc(eString, "string") fmt.Println(x) //@hover("x", "x", xString) @@ -427,7 +427,7 @@ var x int ``` -- @xInterface -- ```go -var x interface{} +var x any ``` -- @xString -- ```go diff --git a/gopls/internal/test/marker/testdata/hover/hover.txt b/gopls/internal/test/marker/testdata/hover/hover.txt index d2ae4fde9fa..fce1facc208 100644 --- a/gopls/internal/test/marker/testdata/hover/hover.txt +++ b/gopls/internal/test/marker/testdata/hover/hover.txt @@ -26,7 +26,7 @@ package aa //@hover("aa", "aa", aa2) package aa func _() { - var y interface{} + var y any switch x := y.(type) { //@hover("x", "x", x) case int: println(x) //@hover("x", "x", xint),hover(")", "x", xint) @@ -52,7 +52,7 @@ const abc untyped int = 0x2a // 42 @hover("b", "abc", abc),hover(" =", "abc", abc) -- @x -- ```go -var x interface{} +var x any ``` -- @xint -- ```go diff --git a/gopls/internal/test/marker/testdata/hover/linkable.txt b/gopls/internal/test/marker/testdata/hover/linkable.txt index 6dc8076523e..e5d2efe8480 100644 --- a/gopls/internal/test/marker/testdata/hover/linkable.txt +++ b/gopls/internal/test/marker/testdata/hover/linkable.txt @@ -36,6 +36,8 @@ func (T) M() {} // m is not exported, and so should not be linkable. func (T) m() {} +var _ = T.m + func _() { var t T diff --git a/gopls/internal/test/marker/testdata/hover/linkname.txt b/gopls/internal/test/marker/testdata/hover/linkname.txt index 6e128a2f215..2633506eac7 100644 --- a/gopls/internal/test/marker/testdata/hover/linkname.txt +++ b/gopls/internal/test/marker/testdata/hover/linkname.txt @@ -22,6 +22,8 @@ func bar() string { return "foo by bar" } +var _ = bar + -- @bar -- ```go func bar() string diff --git a/gopls/internal/test/marker/testdata/hover/methods.txt b/gopls/internal/test/marker/testdata/hover/methods.txt index 142f3ffc97f..402b9274c6a 100644 --- a/gopls/internal/test/marker/testdata/hover/methods.txt +++ b/gopls/internal/test/marker/testdata/hover/methods.txt @@ -28,6 +28,8 @@ func (s S) b() {} func (s *S) PA() {} func (s *S) pb() {} +var _ = (*S).pb + -- a/a.go -- package a diff --git a/gopls/internal/test/marker/testdata/hover/return.txt b/gopls/internal/test/marker/testdata/hover/return.txt new file mode 100644 index 00000000000..998c7a19d16 --- /dev/null +++ b/gopls/internal/test/marker/testdata/hover/return.txt @@ -0,0 +1,12 @@ +This test checks that hovering over a return statement reveals the result type. + +-- a.go -- +package a + +func _() int { + return 1 //@hover("return", "return 1", "returns (int)") +} + +func _() (int, int) { + return 1, 2 //@hover("return", "return 1, 2", "returns (int, int)") +} diff --git a/gopls/internal/test/marker/testdata/implementation/basic.txt b/gopls/internal/test/marker/testdata/implementation/basic.txt index 28522cb5bc4..7882437ccb6 100644 --- a/gopls/internal/test/marker/testdata/implementation/basic.txt +++ b/gopls/internal/test/marker/testdata/implementation/basic.txt @@ -2,7 +2,7 @@ Basic test of implementation query. -- go.mod -- module example.com -go 1.12 +go 1.18 -- implementation/implementation.go -- package implementation @@ -35,7 +35,7 @@ type cryer int //@implementation("cryer", Cryer) func (cryer) Cry(other.CryType) {} //@loc(CryImpl, "Cry"),implementation("Cry", Cry) -type Empty interface{} //@implementation("Empty") +type Empty any //@implementation("Empty") var _ interface{ Joke() } //@implementation("Joke", ImpJoker) diff --git a/gopls/internal/test/marker/testdata/implementation/generics-basicalias.txt b/gopls/internal/test/marker/testdata/implementation/generics-basicalias.txt new file mode 100644 index 00000000000..bd17a8a72ab --- /dev/null +++ b/gopls/internal/test/marker/testdata/implementation/generics-basicalias.txt @@ -0,0 +1,26 @@ +Test of special case of 'implementation' query: aliases of basic types +(rune vs int32) in the "tricky" (=generic) algorithm for unifying +method signatures. + +We test both the local (intra-package) and global (cross-package) +algorithms. + +-- go.mod -- +module example.com +go 1.18 + +-- a/a.go -- +package a + +type C[T any] struct{} +func (C[T]) F(rune, T) {} //@ loc(aCF, "F"), implementation("F", aIF, bIF) + +type I[T any] interface{ F(int32, T) } //@ loc(aIF, "F"), implementation("F", aCF, bCF) + +-- b/b.go -- +package b + +type C[T any] struct{} +func (C[T]) F(rune, T) {} //@ loc(bCF, "F"), implementation("F", aIF, bIF) + +type I[T any] interface{ F(int32, T) } //@ loc(bIF, "F"), implementation("F", aCF, bCF) diff --git a/gopls/internal/test/marker/testdata/implementation/generics.txt b/gopls/internal/test/marker/testdata/implementation/generics.txt index 4a6c31b22f8..a526102890a 100644 --- a/gopls/internal/test/marker/testdata/implementation/generics.txt +++ b/gopls/internal/test/marker/testdata/implementation/generics.txt @@ -7,25 +7,25 @@ go 1.18 -- implementation/implementation.go -- package implementation -type GenIface[T any] interface { //@loc(GenIface, "GenIface"),implementation("GenIface", GC) - F(int, string, T) //@loc(GenIfaceF, "F"),implementation("F", GCF) +type GenIface[T any] interface { //@loc(GenIface, "GenIface"),implementation("GenIface", GC, GenConc, GenConcString) + F(int, string, T) //@loc(GenIfaceF, "F"),implementation("F", GCF, GenConcF) } -type GenConc[U any] int //@loc(GenConc, "GenConc"),implementation("GenConc", GI) +type GenConc[U any] int //@loc(GenConc, "GenConc"),implementation("GenConc", GI, GIString, GenIface) -func (GenConc[V]) F(int, string, V) {} //@loc(GenConcF, "F"),implementation("F", GIF) +func (GenConc[V]) F(int, string, V) {} //@loc(GenConcF, "F"),implementation("F", GIF, GenIfaceF) -type GenConcString struct{ GenConc[string] } //@loc(GenConcString, "GenConcString"),implementation(GenConcString, GIString) +type GenConcString struct{ GenConc[string] } //@loc(GenConcString, "GenConcString"),implementation(GenConcString, GIString, GI, GenIface) -- other/other.go -- package other -type GI[T any] interface { //@loc(GI, "GI"),implementation("GI", GenConc) - F(int, string, T) //@loc(GIF, "F"),implementation("F", GenConcF) +type GI[T any] interface { //@loc(GI, "GI"),implementation("GI", GenConc, GenConcString, GC) + F(int, string, T) //@loc(GIF, "F"),implementation("F", GenConcF, GCF) } -type GIString GI[string] //@loc(GIString, "GIString"),implementation("GIString", GenConcString) +type GIString GI[string] //@loc(GIString, "GIString"),implementation("GIString", GenConcString, GenConc, GC) -type GC[U any] int //@loc(GC, "GC"),implementation("GC", GenIface) +type GC[U any] int //@loc(GC, "GC"),implementation("GC", GenIface, GI, GIString) -func (GC[V]) F(int, string, V) {} //@loc(GCF, "F"),implementation("F", GenIfaceF) +func (GC[V]) F(int, string, V) {} //@loc(GCF, "F"),implementation("F", GenIfaceF, GIF) diff --git a/gopls/internal/test/marker/testdata/implementation/issue43655.txt b/gopls/internal/test/marker/testdata/implementation/issue43655.txt index a7f1d57f80d..3913e3c583f 100644 --- a/gopls/internal/test/marker/testdata/implementation/issue43655.txt +++ b/gopls/internal/test/marker/testdata/implementation/issue43655.txt @@ -2,7 +2,7 @@ This test verifies that we fine implementations of the built-in error interface. -- go.mod -- module example.com -go 1.12 +go 1.18 -- p.go -- package p diff --git a/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt b/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt index e690df72c1c..0ea40f78bc2 100644 --- a/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt +++ b/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt @@ -378,7 +378,7 @@ func funcLitType() { } func compositeLitType() { - foo := map[string]interface{}{"": ""} + foo := map[string]any{"": ""} } -- @vartypes -- @@ -400,6 +400,6 @@ func funcLitType() { } func compositeLitType() { - foo< map[string]interface{}> := map[string]interface{}{"": ""} + foo< map[string]any> := map[string]any{"": ""} } diff --git a/gopls/internal/test/marker/testdata/quickfix/embeddirective.txt b/gopls/internal/test/marker/testdata/quickfix/embeddirective.txt index f0915476f7f..124b729868c 100644 --- a/gopls/internal/test/marker/testdata/quickfix/embeddirective.txt +++ b/gopls/internal/test/marker/testdata/quickfix/embeddirective.txt @@ -13,7 +13,7 @@ import ( //go:embed embed.txt //@quickfix("//go:embed", re`must import "embed"`, fix_import) var t string -func unused() { +func _() { _ = os.Stdin _ = io.EOF } diff --git a/gopls/internal/test/marker/testdata/quickfix/infertypeargs.txt b/gopls/internal/test/marker/testdata/quickfix/infertypeargs.txt index ffb7baa7089..d29a8f45fce 100644 --- a/gopls/internal/test/marker/testdata/quickfix/infertypeargs.txt +++ b/gopls/internal/test/marker/testdata/quickfix/infertypeargs.txt @@ -8,7 +8,7 @@ go 1.18 -- p.go -- package infertypeargs -func app[S interface{ ~[]E }, E interface{}](s S, e E) S { +func app[S interface{ ~[]E }, E any](s S, e E) S { return append(s, e) } diff --git a/gopls/internal/test/marker/testdata/quickfix/self_assignment.txt b/gopls/internal/test/marker/testdata/quickfix/self_assignment.txt index 44a6ad5b8ad..fca3d6d16d7 100644 --- a/gopls/internal/test/marker/testdata/quickfix/self_assignment.txt +++ b/gopls/internal/test/marker/testdata/quickfix/self_assignment.txt @@ -7,7 +7,7 @@ import ( "log" ) -func goodbye() { +func _() { s := "hiiiiiii" s = s //@quickfix("s = s", re"self-assignment", fix) log.Print(s) diff --git a/gopls/internal/test/marker/testdata/references/issue58506.txt b/gopls/internal/test/marker/testdata/references/issue58506.txt index 6285ad425a8..6e52441524c 100644 --- a/gopls/internal/test/marker/testdata/references/issue58506.txt +++ b/gopls/internal/test/marker/testdata/references/issue58506.txt @@ -15,7 +15,7 @@ to B.F. -- go.mod -- module example.com -go 1.12 +go 1.18 -- a/a.go -- package a @@ -53,4 +53,4 @@ package d import "example.com/b" -var _ interface{} = b.B.F //@loc(refd, "F") +var _ any = b.B.F //@loc(refd, "F") diff --git a/gopls/internal/test/marker/testdata/references/typeswitch.txt b/gopls/internal/test/marker/testdata/references/typeswitch.txt index 63a3f13825a..3eb214fdec1 100644 --- a/gopls/internal/test/marker/testdata/references/typeswitch.txt +++ b/gopls/internal/test/marker/testdata/references/typeswitch.txt @@ -3,12 +3,12 @@ a special case in go/types.Info{Def,Use,Implicits}. -- go.mod -- module example.com -go 1.12 +go 1.18 -- a/a.go -- package a -func _(x interface{}) { +func _(x any) { switch y := x.(type) { //@loc(yDecl, "y"), refs("y", yDecl, yInt, yDefault) case int: println(y) //@loc(yInt, "y"), refs("y", yDecl, yInt, yDefault) diff --git a/gopls/internal/test/marker/testdata/rename/basic.txt b/gopls/internal/test/marker/testdata/rename/basic.txt index 618f9593668..73de726e98e 100644 --- a/gopls/internal/test/marker/testdata/rename/basic.txt +++ b/gopls/internal/test/marker/testdata/rename/basic.txt @@ -3,12 +3,12 @@ This test performs basic coverage of 'rename' within a single package. -- basic.go -- package p -func f(x int) { println(x) } //@rename("x", "y", xToy) +func _(x int) { println(x) } //@rename("x", "y", xToy) -- @xToy/basic.go -- @@ -3 +3 @@ --func f(x int) { println(x) } //@rename("x", "y", xToy) -+func f(y int) { println(y) } //@rename("x", "y", xToy) +-func _(x int) { println(x) } //@rename("x", "y", xToy) ++func _(y int) { println(y) } //@rename("x", "y", xToy) -- alias.go -- package p diff --git a/gopls/internal/test/marker/testdata/rename/conflict.txt b/gopls/internal/test/marker/testdata/rename/conflict.txt index 3d7d21cb3e4..9b520a01dad 100644 --- a/gopls/internal/test/marker/testdata/rename/conflict.txt +++ b/gopls/internal/test/marker/testdata/rename/conflict.txt @@ -10,7 +10,7 @@ package super var x int -func f(y int) { +func _(y int) { println(x) println(y) //@renameerr("y", "x", errSuperBlockConflict) } @@ -24,7 +24,7 @@ package sub var a int -func f2(b int) { +func _(b int) { println(a) //@renameerr("a", "b", errSubBlockConflict) println(b) } @@ -32,7 +32,7 @@ func f2(b int) { -- @errSubBlockConflict -- sub/p.go:3:5: renaming this var "a" to "b" sub/p.go:6:10: would cause this reference to become shadowed -sub/p.go:5:9: by this intervening var definition +sub/p.go:5:8: by this intervening var definition -- pkgname/p.go -- package pkgname diff --git a/gopls/internal/test/marker/testdata/rename/crosspkg.txt b/gopls/internal/test/marker/testdata/rename/crosspkg.txt index c60930b0114..76b6ee519eb 100644 --- a/gopls/internal/test/marker/testdata/rename/crosspkg.txt +++ b/gopls/internal/test/marker/testdata/rename/crosspkg.txt @@ -29,6 +29,8 @@ func _() { x.F() //@rename("F", "G", FToG) } +var _ = C.g + -- crosspkg/other/other.go -- package other diff --git a/gopls/internal/test/marker/testdata/rename/generics.txt b/gopls/internal/test/marker/testdata/rename/generics.txt index 0f57570a5fb..61d7801295e 100644 --- a/gopls/internal/test/marker/testdata/rename/generics.txt +++ b/gopls/internal/test/marker/testdata/rename/generics.txt @@ -24,10 +24,15 @@ func _[P ~[]int]() { _ = P{} } +var _ = I.m + -- @mToM/a.go -- @@ -5 +5 @@ -func (I) m() {} //@rename("m", "M", mToM) +func (I) M() {} //@rename("m", "M", mToM) +@@ -11 +11 @@ +-var _ = I.m ++var _ = I.M -- g.go -- package a diff --git a/gopls/internal/test/marker/testdata/rename/prepare.txt b/gopls/internal/test/marker/testdata/rename/prepare.txt index 7ac9581898e..2542648bc4b 100644 --- a/gopls/internal/test/marker/testdata/rename/prepare.txt +++ b/gopls/internal/test/marker/testdata/rename/prepare.txt @@ -33,6 +33,8 @@ func (*Y) Bobby() {} -- good/good0.go -- package good +var _ = stuff + func stuff() { //@item(good_stuff, "stuff", "func()", "func"),preparerename("stu", "stuff", span="stuff") x := 5 random2(x) //@preparerename("dom", "random2", span="random2") @@ -45,6 +47,8 @@ import ( "golang.org/lsptests/types" //@item(types_import, "types", "\"golang.org/lsptests/types\"", "package") ) +var _ = random + func random() int { //@item(good_random, "random", "func() int", "func") _ = "random() int" //@preparerename("random", "") y := 6 + 7 //@preparerename("7", "") diff --git a/gopls/internal/test/marker/testdata/rename/random.txt b/gopls/internal/test/marker/testdata/rename/random.txt index 5c58b3db626..9ddf8e1d97b 100644 --- a/gopls/internal/test/marker/testdata/rename/random.txt +++ b/gopls/internal/test/marker/testdata/rename/random.txt @@ -39,7 +39,7 @@ func _() { } func sw() { - var x interface{} + var x any switch y := x.(type) { //@rename("y", "y0", yToy0) case int: diff --git a/gopls/internal/test/marker/testdata/rename/typeswitch.txt b/gopls/internal/test/marker/testdata/rename/typeswitch.txt index ec550021745..c4d15ad7216 100644 --- a/gopls/internal/test/marker/testdata/rename/typeswitch.txt +++ b/gopls/internal/test/marker/testdata/rename/typeswitch.txt @@ -3,7 +3,7 @@ This test covers the special case of renaming a type switch var. -- p.go -- package p -func _(x interface{}) { +func _(x any) { switch y := x.(type) { //@rename("y", "z", yToZ) case string: print(y) //@rename("y", "z", yToZ) diff --git a/gopls/internal/test/marker/testdata/signature/signature.txt b/gopls/internal/test/marker/testdata/signature/signature.txt index 1da4eb5843e..4f4064397c6 100644 --- a/gopls/internal/test/marker/testdata/signature/signature.txt +++ b/gopls/internal/test/marker/testdata/signature/signature.txt @@ -69,7 +69,7 @@ func Qux() { fnPtr := &fn (*fnPtr)("hi", "there") //@signature(",", "func(hi string, there string) func(i int) rune", 0) - var fnIntf interface{} = Foo + var fnIntf any = Foo fnIntf.(func(string, int) bool)("hi", 123) //@signature("123", "func(string, int) bool", 1) (&bytes.Buffer{}).Next(2) //@signature("2", "Next(n int) []byte", 0) diff --git a/gopls/internal/test/marker/testdata/symbol/basic.txt b/gopls/internal/test/marker/testdata/symbol/basic.txt index 49c54b0fdb0..d993dd2ad60 100644 --- a/gopls/internal/test/marker/testdata/symbol/basic.txt +++ b/gopls/internal/test/marker/testdata/symbol/basic.txt @@ -70,12 +70,14 @@ type WithEmbeddeds interface { io.Writer } -type EmptyInterface interface{} +type EmptyInterface any func Dunk() int { return 0 } func dunk() {} +var _ = dunk + -- @want -- (*Quux).Do "func()" (Foo).Baz "func() string" +2 lines @@ -86,7 +88,7 @@ Alias "string" BoolAlias "bool" Boolean "bool" Dunk "func() int" -EmptyInterface "interface{}" +EmptyInterface "any" EmptyStruct "struct{}" Foo "struct{...}" +6 lines Foo.Bar "int" diff --git a/gopls/internal/test/marker/testdata/workspacesymbol/casesensitive.txt b/gopls/internal/test/marker/testdata/workspacesymbol/casesensitive.txt index 725e9dbb52d..e170aef87f1 100644 --- a/gopls/internal/test/marker/testdata/workspacesymbol/casesensitive.txt +++ b/gopls/internal/test/marker/testdata/workspacesymbol/casesensitive.txt @@ -77,6 +77,8 @@ func Dunk() int { return 0 } func dunk() {} +var _ = dunk + -- p/p.go -- package p diff --git a/gopls/internal/test/marker/testdata/workspacesymbol/issue44806.txt b/gopls/internal/test/marker/testdata/workspacesymbol/issue44806.txt index b2cd0b5c5a2..b88a1512df7 100644 --- a/gopls/internal/test/marker/testdata/workspacesymbol/issue44806.txt +++ b/gopls/internal/test/marker/testdata/workspacesymbol/issue44806.txt @@ -7,21 +7,21 @@ go 1.18 -- symbol.go -- package symbol -//@workspacesymbol("m", m) +//@workspacesymbol("M", M) type T struct{} // We should accept all valid receiver syntax when scanning symbols. -func (*(T)) m1() {} -func (*T) m2() {} -func (T) m3() {} -func ((T)) m4() {} -func ((*T)) m5() {} +func (*(T)) M1() {} +func (*T) M2() {} +func (T) M3() {} +func ((T)) M4() {} +func ((*T)) M5() {} --- @m -- -symbol.go:8:13-15 T.m1 Method -symbol.go:9:11-13 T.m2 Method -symbol.go:10:10-12 T.m3 Method -symbol.go:11:12-14 T.m4 Method -symbol.go:12:13-15 T.m5 Method +-- @M -- +symbol.go:8:13-15 T.M1 Method +symbol.go:9:11-13 T.M2 Method +symbol.go:10:10-12 T.M3 Method +symbol.go:11:12-14 T.M4 Method +symbol.go:12:13-15 T.M5 Method symbol.go:5:6-7 symbol.T Struct diff --git a/gopls/internal/util/astutil/util.go b/gopls/internal/util/astutil/util.go index ac7515d1daf..ccfa931d882 100644 --- a/gopls/internal/util/astutil/util.go +++ b/gopls/internal/util/astutil/util.go @@ -7,12 +7,13 @@ package astutil import ( "go/ast" "go/token" + "reflect" "golang.org/x/tools/internal/typeparams" ) // UnpackRecv unpacks a receiver type expression, reporting whether it is a -// pointer recever, along with the type name identifier and any receiver type +// pointer receiver, along with the type name identifier and any receiver type // parameter identifiers. // // Copied (with modifications) from go/types. @@ -61,11 +62,115 @@ L: // unpack receiver type return } -// NodeContains returns true if a node encloses a given position pos. -// The end point will also be inclusive, which will to allow hovering when the -// cursor is behind some nodes. +// NodeContains reports whether the Pos/End range of node n encloses +// the given position pos. +// +// It is inclusive of both end points, to allow hovering (etc) when +// the cursor is immediately after a node. +// +// For unfortunate historical reasons, the Pos/End extent of an +// ast.File runs from the start of its package declaration---excluding +// copyright comments, build tags, and package documentation---to the +// end of its last declaration, excluding any trailing comments. So, +// as a special case, if n is an [ast.File], NodeContains uses +// n.FileStart <= pos && pos <= n.FileEnd to report whether the +// position lies anywhere within the file. // // Precondition: n must not be nil. func NodeContains(n ast.Node, pos token.Pos) bool { - return n.Pos() <= pos && pos <= n.End() + var start, end token.Pos + if file, ok := n.(*ast.File); ok { + start, end = file.FileStart, file.FileEnd // entire file + } else { + start, end = n.Pos(), n.End() + } + return start <= pos && pos <= end +} + +// Equal reports whether two nodes are structurally equal, +// ignoring fields of type [token.Pos], [ast.Object], +// and [ast.Scope], and comments. +// +// The operands x and y may be nil. +// A nil slice is not equal to an empty slice. +// +// The provided function determines whether two identifiers +// should be considered identical. +func Equal(x, y ast.Node, identical func(x, y *ast.Ident) bool) bool { + if x == nil || y == nil { + return x == y + } + return equal(reflect.ValueOf(x), reflect.ValueOf(y), identical) +} + +func equal(x, y reflect.Value, identical func(x, y *ast.Ident) bool) bool { + // Ensure types are the same + if x.Type() != y.Type() { + return false + } + switch x.Kind() { + case reflect.Pointer: + if x.IsNil() || y.IsNil() { + return x.IsNil() == y.IsNil() + } + switch t := x.Interface().(type) { + // Skip fields of types potentially involved in cycles. + case *ast.Object, *ast.Scope, *ast.CommentGroup: + return true + case *ast.Ident: + return identical(t, y.Interface().(*ast.Ident)) + default: + return equal(x.Elem(), y.Elem(), identical) + } + + case reflect.Interface: + if x.IsNil() || y.IsNil() { + return x.IsNil() == y.IsNil() + } + return equal(x.Elem(), y.Elem(), identical) + + case reflect.Struct: + for i := range x.NumField() { + xf := x.Field(i) + yf := y.Field(i) + // Skip position fields. + if xpos, ok := xf.Interface().(token.Pos); ok { + ypos := yf.Interface().(token.Pos) + // Numeric value of a Pos is not significant but its "zeroness" is, + // because it is often significant, e.g. CallExpr.Variadic(Ellipsis), ChanType.Arrow. + if xpos.IsValid() != ypos.IsValid() { + return false + } + } else if !equal(xf, yf, identical) { + return false + } + } + return true + + case reflect.Slice: + if x.IsNil() || y.IsNil() { + return x.IsNil() == y.IsNil() + } + if x.Len() != y.Len() { + return false + } + for i := range x.Len() { + if !equal(x.Index(i), y.Index(i), identical) { + return false + } + } + return true + + case reflect.String: + return x.String() == y.String() + + case reflect.Bool: + return x.Bool() == y.Bool() + + case reflect.Int: + return x.Int() == y.Int() + + default: + panic(x) + } } diff --git a/gopls/internal/util/frob/frob.go b/gopls/internal/util/frob/frob.go index a5fa584215f..c297e2a1014 100644 --- a/gopls/internal/util/frob/frob.go +++ b/gopls/internal/util/frob/frob.go @@ -12,8 +12,7 @@ // - Interface values are not supported; this avoids the need for // the encoding to describe types. // -// - Types that recursively contain private struct fields are not -// permitted. +// - Private struct fields are ignored. // // - The encoding is unspecified and subject to change, so the encoder // and decoder must exactly agree on their implementation and on the @@ -104,7 +103,7 @@ func frobFor(t reflect.Type) *frob { for i := 0; i < fr.t.NumField(); i++ { field := fr.t.Field(i) if field.PkgPath != "" { - panic(fmt.Sprintf("unexported field %v", field)) + continue // skip unexported field } fr.addElem(field.Type) } diff --git a/gopls/internal/util/moremaps/maps.go b/gopls/internal/util/moremaps/maps.go index c8484d9fecd..00dd1e4210b 100644 --- a/gopls/internal/util/moremaps/maps.go +++ b/gopls/internal/util/moremaps/maps.go @@ -22,7 +22,7 @@ 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, like slices.Collect(maps.Keys(m)). +// KeySlice 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 { @@ -42,21 +42,30 @@ func ValueSlice[M ~map[K]V, K comparable, V any](m M) []V { // SameKeys reports whether x and y have equal sets of keys. func SameKeys[K comparable, V1, V2 any](x map[K]V1, y map[K]V2) bool { - if len(x) != len(y) { - return false - } - for k := range x { - if _, ok := y[k]; !ok { - return false - } - } - return true + ignoreValues := func(V1, V2) bool { return true } + return maps.EqualFunc(x, y, ignoreValues) } // 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] { + // TODO(adonovan): use maps.Sorted if proposal #68598 is accepted. + return func(yield func(K, V) bool) { + keys := KeySlice(m) + slices.Sort(keys) + for _, k := range keys { + if !yield(k, m[k]) { + break + } + } + } +} + +// SortedFunc returns an iterator over the entries of m in key order. +func SortedFunc[M ~map[K]V, K comparable, V any](m M, cmp func(x, y K) int) iter.Seq2[K, V] { + // TODO(adonovan): use maps.SortedFunc if proposal #68598 is accepted. return func(yield func(K, V) bool) { - keys := slices.Sorted(maps.Keys(m)) + keys := KeySlice(m) + slices.SortFunc(keys, cmp) for _, k := range keys { if !yield(k, m[k]) { break diff --git a/gopls/internal/util/typesutil/typesutil.go b/gopls/internal/util/typesutil/typesutil.go index 98f5605200e..79042a24901 100644 --- a/gopls/internal/util/typesutil/typesutil.go +++ b/gopls/internal/util/typesutil/typesutil.go @@ -12,32 +12,6 @@ import ( "strings" ) -// FileQualifier returns a [types.Qualifier] function that qualifies -// imported symbols appropriately based on the import environment of a -// given file. -func FileQualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qualifier { - // Construct mapping of import paths to their defined or implicit names. - imports := make(map[*types.Package]string) - for _, imp := range f.Imports { - if pkgname := info.PkgNameOf(imp); pkgname != nil { - imports[pkgname.Imported()] = pkgname.Name() - } - } - // Define qualifier to replace full package paths with names of the imports. - return func(p *types.Package) string { - if p == pkg { - return "" - } - if name, ok := imports[p]; ok { - if name == "." { - return "" - } - return name - } - return p.Name() - } -} - // FormatTypeParams turns TypeParamList into its Go representation, such as: // [T, Y]. Note that it does not print constraints as this is mainly used for // formatting type params in method receivers. diff --git a/gopls/main.go b/gopls/main.go index 083c4efd8de..a563ecfd8c1 100644 --- a/gopls/main.go +++ b/gopls/main.go @@ -13,10 +13,13 @@ package main // import "golang.org/x/tools/gopls" import ( "context" + "log" "os" "golang.org/x/telemetry" + "golang.org/x/telemetry/counter" "golang.org/x/tools/gopls/internal/cmd" + "golang.org/x/tools/gopls/internal/filecache" versionpkg "golang.org/x/tools/gopls/internal/version" "golang.org/x/tools/internal/tool" ) @@ -31,6 +34,24 @@ func main() { Upload: true, }) + // Force early creation of the filecache and refuse to start + // if there were unexpected errors such as ENOSPC. This + // minimizes the window of exposure to deletion of the + // executable, and ensures that all subsequent calls to + // filecache.Get cannot fail for these two reasons; + // see issue #67433. + // + // This leaves only one likely cause for later failures: + // deletion of the cache while gopls is running. If the + // problem continues, we could periodically stat the cache + // directory (for example at the start of every RPC) and + // either re-create it or just fail the RPC with an + // informative error and terminate the process. + if _, err := filecache.Get("nonesuch", [32]byte{}); err != nil && err != filecache.ErrNotFound { + counter.Inc("gopls/nocache") + log.Fatalf("gopls cannot access its persistent index (disk full?): %v", err) + } + ctx := context.Background() tool.Main(ctx, cmd.New(), os.Args[1:]) } diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go index f469821141b..54b5bc8731b 100644 --- a/internal/aliases/aliases_test.go +++ b/internal/aliases/aliases_test.go @@ -81,7 +81,7 @@ func TestNewAlias(t *testing.T) { } } -// TestNewAlias tests that alias.NewAlias can create a parameterized alias +// TestNewParameterizedAlias tests that alias.NewAlias can create a parameterized alias // A[T] of a type whose underlying and Unaliased type is *T. The test then // instantiates A[Named] and checks that the underlying and Unaliased type // of A[Named] is *Named. diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index fe67b0fa27a..58615232ff9 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -65,90 +65,6 @@ func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos return end } -// StmtToInsertVarBefore returns the ast.Stmt before which we can -// safely insert a new var declaration, or nil if the path denotes a -// node outside any statement. -// -// Basic Example: -// -// z := 1 -// y := z + x -// -// If x is undeclared, then this function would return `y := z + x`, so that we -// can insert `x := ` on the line before `y := z + x`. -// -// If stmt example: -// -// if z == 1 { -// } else if z == y {} -// -// If y is undeclared, then this function would return `if z == 1 {`, because we cannot -// insert a statement between an if and an else if statement. As a result, we need to find -// the top of the if chain to insert `y := ` before. -func StmtToInsertVarBefore(path []ast.Node) ast.Stmt { - enclosingIndex := -1 - for i, p := range path { - if _, ok := p.(ast.Stmt); ok { - enclosingIndex = i - break - } - } - if enclosingIndex == -1 { - return nil // no enclosing statement: outside function - } - enclosingStmt := path[enclosingIndex] - switch enclosingStmt.(type) { - case *ast.IfStmt: - // The enclosingStmt is inside of the if declaration, - // We need to check if we are in an else-if stmt and - // get the base if statement. - // TODO(adonovan): for non-constants, it may be preferable - // to add the decl as the Init field of the innermost - // enclosing ast.IfStmt. - return baseIfStmt(path, enclosingIndex) - case *ast.CaseClause: - // Get the enclosing switch stmt if the enclosingStmt is - // inside of the case statement. - for i := enclosingIndex + 1; i < len(path); i++ { - if node, ok := path[i].(*ast.SwitchStmt); ok { - return node - } else if node, ok := path[i].(*ast.TypeSwitchStmt); ok { - return node - } - } - } - if len(path) <= enclosingIndex+1 { - return enclosingStmt.(ast.Stmt) - } - // Check if the enclosing statement is inside another node. - switch expr := path[enclosingIndex+1].(type) { - case *ast.IfStmt: - // Get the base if statement. - return baseIfStmt(path, enclosingIndex+1) - case *ast.ForStmt: - if expr.Init == enclosingStmt || expr.Post == enclosingStmt { - return expr - } - case *ast.SwitchStmt, *ast.TypeSwitchStmt: - return expr.(ast.Stmt) - } - return enclosingStmt.(ast.Stmt) -} - -// baseIfStmt walks up the if/else-if chain until we get to -// the top of the current if chain. -func baseIfStmt(path []ast.Node, index int) ast.Stmt { - stmt := path[index] - for i := index + 1; i < len(path); i++ { - if node, ok := path[i].(*ast.IfStmt); ok && node.Else == stmt { - stmt = node - continue - } - break - } - return stmt.(ast.Stmt) -} - // WalkASTWithParent walks the AST rooted at n. The semantics are // similar to ast.Inspect except it does not call f(nil). func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) { diff --git a/internal/astutil/cursor/cursor.go b/internal/astutil/cursor/cursor.go new file mode 100644 index 00000000000..9f0b906f1c2 --- /dev/null +++ b/internal/astutil/cursor/cursor.go @@ -0,0 +1,352 @@ +// 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 cursor augments [inspector.Inspector] with [Cursor] +// functionality allowing more flexibility and control during +// inspection. +// +// This package is a temporary private extension of inspector until +// proposal #70859 is accepted, and which point it will be moved into +// the inspector package, and [Root] will become a method of +// Inspector. +package cursor + +import ( + "go/ast" + "go/token" + "iter" + "reflect" + "slices" + + "golang.org/x/tools/go/ast/inspector" +) + +// A Cursor represents an [ast.Node]. It is immutable. +// +// Two Cursors compare equal if they represent the same node. +// +// Call [Root] to obtain a valid cursor. +type Cursor struct { + in *inspector.Inspector + index int32 // index of push node; -1 for virtual root node +} + +// Root returns a cursor for the virtual root node, +// whose children are the files provided to [New]. +// +// Its [Cursor.Node] and [Cursor.Stack] methods return nil. +func Root(in *inspector.Inspector) Cursor { + return Cursor{in, -1} +} + +// Node returns the node at the current cursor position, +// or nil for the cursor returned by [Inspector.Root]. +func (c Cursor) Node() ast.Node { + if c.index < 0 { + return nil + } + return c.events()[c.index].node +} + +// String returns information about the cursor's node, if any. +func (c Cursor) String() string { + if c.in == nil { + return "(invalid)" + } + if c.index < 0 { + return "(root)" + } + return reflect.TypeOf(c.Node()).String() +} + +// indices return the [start, end) half-open interval of event indices. +func (c Cursor) indices() (int32, int32) { + if c.index < 0 { + return 0, int32(len(c.events())) // root: all events + } else { + return c.index, c.events()[c.index].index + 1 // just one subtree + } +} + +// Preorder returns an iterator over the nodes of the subtree +// represented by c in depth-first order. Each node in the sequence is +// represented by a Cursor that allows access to the Node, but may +// also be used to start a new traversal, or to obtain the stack of +// nodes enclosing the cursor. +// +// The traversal sequence is determined by [ast.Inspect]. The types +// argument, if non-empty, enables type-based filtering of events. The +// function f if is called only for nodes whose type matches an +// element of the types slice. +// +// If you need control over descent into subtrees, +// or need both pre- and post-order notifications, use [Cursor.Inspect] +func (c Cursor) Preorder(types ...ast.Node) iter.Seq[Cursor] { + mask := maskOf(types) + + return func(yield func(Cursor) bool) { + events := c.events() + + for i, limit := c.indices(); i < limit; { + ev := events[i] + if ev.index > i { // push? + if ev.typ&mask != 0 && !yield(Cursor{c.in, i}) { + break + } + pop := ev.index + if events[pop].typ&mask == 0 { + // Subtree does not contain types: skip. + i = pop + 1 + continue + } + } + i++ + } + } +} + +// Inspect visits the nodes of the subtree represented by c in +// depth-first order. It calls f(n, true) for each node n before it +// visits n's children. If f returns true, Inspect invokes f +// recursively for each of the non-nil children of the node, followed +// by a call of f(n, false). +// +// Each node is represented by a Cursor that allows access to the +// Node, but may also be used to start a new traversal, or to obtain +// the stack of nodes enclosing the cursor. +// +// The complete traversal sequence is determined by [ast.Inspect]. +// The types argument, if non-empty, enables type-based filtering of +// events. The function f if is called only for nodes whose type +// matches an element of the types slice. +func (c Cursor) Inspect(types []ast.Node, f func(c Cursor, push bool) (descend bool)) { + mask := maskOf(types) + events := c.events() + for i, limit := c.indices(); i < limit; { + ev := events[i] + if ev.index > i { + // push + pop := ev.index + if ev.typ&mask != 0 && !f(Cursor{c.in, i}, true) { + i = pop + 1 // past the pop + continue + } + if events[pop].typ&mask == 0 { + // Subtree does not contain types: skip to pop. + i = pop + continue + } + } else { + // pop + push := ev.index + if events[push].typ&mask != 0 { + f(Cursor{c.in, push}, false) + } + } + i++ + } +} + +// Stack returns the stack of enclosing nodes, outermost first: +// from the [ast.File] down to the current cursor's node. +// +// To amortize allocation, it appends to the provided slice, which +// must be empty. +// +// Stack must not be called on the Root node. +// +// TODO(adonovan): perhaps this should be replaced by: +// +// func (Cursor) Ancestors(filter []ast.Node) iter.Seq[Cursor] +// +// returning a filtering iterator up the parent chain. +// This finesses the question of allocation entirely. +func (c Cursor) Stack(stack []Cursor) []Cursor { + if len(stack) > 0 { + panic("stack is non-empty") + } + if c.index < 0 { + panic("Cursor.Stack called on Root node") + } + + events := c.events() + for i := c.index; i >= 0; i = events[i].parent { + stack = append(stack, Cursor{c.in, i}) + } + slices.Reverse(stack) + return stack +} + +// Parent returns the parent of the current node. +// +// Parent must not be called on the Root node (whose [Cursor.Node] returns nil). +func (c Cursor) Parent() Cursor { + if c.index < 0 { + panic("Cursor.Parent called on Root node") + } + + return Cursor{c.in, c.events()[c.index].parent} +} + +// NextSibling returns the cursor for the next sibling node in the +// same list (for example, of files, decls, specs, statements, fields, +// or expressions) as the current node. It returns zero if the node is +// the last node in the list, or is not part of a list. +// +// NextSibling must not be called on the Root node. +func (c Cursor) NextSibling() (Cursor, bool) { + if c.index < 0 { + panic("Cursor.NextSibling called on Root node") + } + + events := c.events() + i := events[c.index].index + 1 // after corresponding pop + if i < int32(len(events)) { + if events[i].index > i { // push? + return Cursor{c.in, i}, true + } + } + return Cursor{}, false +} + +// PrevSibling returns the cursor for the previous sibling node in the +// same list (for example, of files, decls, specs, statements, fields, +// or expressions) as the current node. It returns zero if the node is +// the first node in the list, or is not part of a list. +// +// It must not be called on the Root node. +func (c Cursor) PrevSibling() (Cursor, bool) { + if c.index < 0 { + panic("Cursor.PrevSibling called on Root node") + } + + events := c.events() + i := c.index - 1 + if i >= 0 { + if j := events[i].index; j < i { // pop? + return Cursor{c.in, j}, true + } + } + return Cursor{}, false +} + +// FirstChild returns the first direct child of the current node, +// or zero if it has no children. +func (c Cursor) FirstChild() (Cursor, bool) { + events := c.events() + i := c.index + 1 // i=0 if c is root + if i < int32(len(events)) && events[i].index > i { // push? + return Cursor{c.in, i}, true + } + return Cursor{}, false +} + +// LastChild returns the last direct child of the current node, +// or zero if it has no children. +func (c Cursor) LastChild() (Cursor, bool) { + events := c.events() + if c.index < 0 { // root? + if len(events) > 0 { + // return push of final event (a pop) + return Cursor{c.in, events[len(events)-1].index}, true + } + } else { + j := events[c.index].index - 1 // before corresponding pop + // Inv: j == c.index if c has no children + // or j is last child's pop. + if j > c.index { // c has children + return Cursor{c.in, events[j].index}, true + } + } + return Cursor{}, false +} + +// Children returns an iterator over the direct children of the +// current node, if any. +func (c Cursor) Children() iter.Seq[Cursor] { + return func(yield func(Cursor) bool) { + c, ok := c.FirstChild() + for ok && yield(c) { + c, ok = c.NextSibling() + } + } +} + +// FindNode returns the cursor for node n if it belongs to the subtree +// rooted at c. It returns zero if n is not found. +func (c Cursor) FindNode(n ast.Node) (Cursor, bool) { + + // FindNode is equivalent to this code, + // but more convenient and 15-20% faster: + if false { + for candidate := range c.Preorder(n) { + if candidate.Node() == n { + return candidate, true + } + } + return Cursor{}, false + } + + // TODO(adonovan): opt: should we assume Node.Pos is accurate + // and combine type-based filtering with position filtering + // like FindPos? + + mask := maskOf([]ast.Node{n}) + events := c.events() + + for i, limit := c.indices(); i < limit; i++ { + ev := events[i] + if ev.index > i { // push? + if ev.typ&mask != 0 && ev.node == n { + return Cursor{c.in, i}, true + } + pop := ev.index + if events[pop].typ&mask == 0 { + // Subtree does not contain type of n: skip. + i = pop + } + } + } + return Cursor{}, false +} + +// FindPos returns the cursor for the innermost node n in the tree +// rooted at c such that n.Pos() <= start && end <= n.End(). +// It returns zero if none is found. +// Precondition: start <= end. +// +// See also [astutil.PathEnclosingInterval], which +// tolerates adjoining whitespace. +func (c Cursor) FindPos(start, end token.Pos) (Cursor, bool) { + if end < start { + panic("end < start") + } + events := c.events() + + // This algorithm could be implemented using c.Inspect, + // but it is about 2.5x slower. + + best := int32(-1) // push index of latest (=innermost) node containing range + for i, limit := c.indices(); i < limit; i++ { + ev := events[i] + if ev.index > i { // push? + if ev.node.Pos() > start { + break // disjoint, after; stop + } + nodeEnd := ev.node.End() + if end <= nodeEnd { + // node fully contains target range + best = i + } else if nodeEnd < start { + i = ev.index // disjoint, before; skip forward + } + } + } + if best >= 0 { + return Cursor{c.in, best}, true + } + return Cursor{}, false +} diff --git a/internal/astutil/cursor/cursor_test.go b/internal/astutil/cursor/cursor_test.go new file mode 100644 index 00000000000..e578fa300a6 --- /dev/null +++ b/internal/astutil/cursor/cursor_test.go @@ -0,0 +1,460 @@ +// 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 cursor_test + +import ( + "fmt" + "go/ast" + "go/build" + "go/parser" + "go/token" + "iter" + "log" + "path/filepath" + "slices" + "strings" + "testing" + + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/astutil/cursor" +) + +// net/http package +var ( + netFset = token.NewFileSet() + netFiles []*ast.File + netInspect *inspector.Inspector +) + +func init() { + files, err := parseNetFiles() + if err != nil { + log.Fatal(err) + } + netFiles = files + netInspect = inspector.New(netFiles) +} + +func parseNetFiles() ([]*ast.File, error) { + pkg, err := build.Default.Import("net", "", 0) + if err != nil { + return nil, err + } + var files []*ast.File + for _, filename := range pkg.GoFiles { + filename = filepath.Join(pkg.Dir, filename) + f, err := parser.ParseFile(netFset, filename, nil, 0) + if err != nil { + return nil, err + } + files = append(files, f) + } + return files, nil +} + +// 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 { + for i := range nodesA { + if a, b := nodesA[i], nodesB[i]; a != b { + t.Errorf("node %d is inconsistent: %T, %T", i, a, b) + } + } + } +} + +// 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 +} + +func TestCursor_Preorder(t *testing.T) { + inspect := netInspect + + nodeFilter := []ast.Node{(*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)} + + // reference implementation + var want []ast.Node + for cur := range cursor.Root(inspect).Preorder(nodeFilter...) { + want = append(want, cur.Node()) + } + + // Check entire sequence. + got := slices.Collect(inspect.PreorderSeq(nodeFilter...)) + compare(t, got, want) + + // Check that break works. + got = got[:0] + for _, c := range firstN(10, cursor.Root(inspect).Preorder(nodeFilter...)) { + got = append(got, c.Node()) + } + compare(t, got, want[:10]) +} + +func TestCursor_nestedTraversal(t *testing.T) { + const src = `package a +func f() { + print("hello") +} +func g() { + print("goodbye") + panic("oops") +} +` + fset := token.NewFileSet() + f, _ := parser.ParseFile(fset, "a.go", src, 0) + inspect := inspector.New([]*ast.File{f}) + + var ( + funcDecls = []ast.Node{(*ast.FuncDecl)(nil)} + callExprs = []ast.Node{(*ast.CallExpr)(nil)} + nfuncs = 0 + ncalls = 0 + ) + + for curFunc := range cursor.Root(inspect).Preorder(funcDecls...) { + _ = curFunc.Node().(*ast.FuncDecl) + nfuncs++ + stack := curFunc.Stack(nil) + + // Stacks are convenient to print! + if got, want := fmt.Sprint(stack), "[*ast.File *ast.FuncDecl]"; got != want { + t.Errorf("curFunc.Stack() = %q, want %q", got, want) + } + + // Parent, iterated, is Stack. + i := 0 + for c := curFunc; c.Node() != nil; c = c.Parent() { + if got, want := stack[len(stack)-1-i], c; got != want { + t.Errorf("Stack[%d] = %v; Parent()^%d = %v", i, got, i, want) + } + i++ + } + + // nested Preorder traversal + preorderCount := 0 + for curCall := range curFunc.Preorder(callExprs...) { + _ = curCall.Node().(*ast.CallExpr) + preorderCount++ + stack := curCall.Stack(nil) + if got, want := fmt.Sprint(stack), "[*ast.File *ast.FuncDecl *ast.BlockStmt *ast.ExprStmt *ast.CallExpr]"; got != want { + t.Errorf("curCall.Stack() = %q, want %q", got, want) + } + } + + // nested Inspect traversal + inspectCount := 0 // pushes and pops + curFunc.Inspect(callExprs, func(curCall cursor.Cursor, push bool) (proceed bool) { + _ = curCall.Node().(*ast.CallExpr) + inspectCount++ + stack := curCall.Stack(nil) + if got, want := fmt.Sprint(stack), "[*ast.File *ast.FuncDecl *ast.BlockStmt *ast.ExprStmt *ast.CallExpr]"; got != want { + t.Errorf("curCall.Stack() = %q, want %q", got, want) + } + return true + }) + + if inspectCount != preorderCount*2 { + t.Errorf("Inspect (%d push/pop events) and Preorder (%d push events) are not consistent", inspectCount, preorderCount) + } + + ncalls += preorderCount + } + + if nfuncs != 2 { + t.Errorf("Found %d FuncDecls, want 2", nfuncs) + } + if ncalls != 3 { + t.Errorf("Found %d CallExprs, want 3", ncalls) + } +} + +func TestCursor_Children(t *testing.T) { + inspect := netInspect + + // Assert that Cursor.Children agrees with + // reference implementation for every node. + var want, got []ast.Node + for c := range cursor.Root(inspect).Preorder() { + + // reference implementation + want = want[:0] + { + parent := c.Node() + ast.Inspect(parent, func(n ast.Node) bool { + if n != nil && n != parent { + want = append(want, n) + } + return n == parent // descend only into parent + }) + } + + // Check cursor-based implementation + // (uses FirstChild+NextSibling). + got = got[:0] + for child := range c.Children() { + got = append(got, child.Node()) + } + + if !slices.Equal(got, want) { + t.Errorf("For %v\n"+ + "Using FirstChild+NextSibling: %v\n"+ + "Using ast.Inspect: %v", + c, sliceTypes(got), sliceTypes(want)) + } + + // Second cursor-based implementation + // using LastChild+PrevSibling+reverse. + got = got[:0] + for c, ok := c.LastChild(); ok; c, ok = c.PrevSibling() { + got = append(got, c.Node()) + } + slices.Reverse(got) + + if !slices.Equal(got, want) { + t.Errorf("For %v\n"+ + "Using LastChild+PrevSibling: %v\n"+ + "Using ast.Inspect: %v", + c, sliceTypes(got), sliceTypes(want)) + } + } +} + +func TestCursor_Inspect(t *testing.T) { + inspect := netInspect + + // In all three loops, we'll gather both kinds of type switches, + // but we'll prune the traversal from descending into (value) switches. + switches := []ast.Node{(*ast.SwitchStmt)(nil), (*ast.TypeSwitchStmt)(nil)} + + // reference implementation (ast.Inspect) + var nodesA []ast.Node + for _, f := range netFiles { + ast.Inspect(f, func(n ast.Node) (proceed bool) { + switch n.(type) { + case *ast.SwitchStmt, *ast.TypeSwitchStmt: + nodesA = append(nodesA, n) + return !is[*ast.SwitchStmt](n) // descend only into TypeSwitchStmt + } + return true + }) + } + + // Test Cursor.Inspect implementation. + var nodesB []ast.Node + cursor.Root(inspect).Inspect(switches, func(c cursor.Cursor, push bool) (proceed bool) { + if push { + n := c.Node() + nodesB = append(nodesB, n) + return !is[*ast.SwitchStmt](n) // descend only into TypeSwitchStmt + } + return false + }) + compare(t, nodesA, nodesB) + + // Test WithStack implementation. + var nodesC []ast.Node + inspect.WithStack(switches, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) { + if push { + nodesC = append(nodesC, n) + return !is[*ast.SwitchStmt](n) // descend only into TypeSwitchStmt + } + return false + }) + compare(t, nodesA, nodesC) +} + +func TestCursor_FindNode(t *testing.T) { + inspect := netInspect + + // Enumerate all nodes of a particular type, + // then check that FindPos can find them, + // starting at the root. + // + // (We use BasicLit because they are numerous.) + root := cursor.Root(inspect) + for c := range root.Preorder((*ast.BasicLit)(nil)) { + node := c.Node() + got, ok := root.FindNode(node) + if !ok { + t.Errorf("root.FindNode failed") + } else if got != c { + t.Errorf("root.FindNode returned %v, want %v", got, c) + } + } + + // Same thing, but searching only within subtrees (each FuncDecl). + for funcDecl := range root.Preorder((*ast.FuncDecl)(nil)) { + for c := range funcDecl.Preorder((*ast.BasicLit)(nil)) { + node := c.Node() + got, ok := funcDecl.FindNode(node) + if !ok { + t.Errorf("funcDecl.FindNode failed") + } else if got != c { + t.Errorf("funcDecl.FindNode returned %v, want %v", got, c) + } + + // Also, check that we cannot find the BasicLit + // beneath a different FuncDecl. + if prevFunc, ok := funcDecl.PrevSibling(); ok { + got, ok := prevFunc.FindNode(node) + if ok { + t.Errorf("prevFunc.FindNode succeeded unexpectedly: %v", got) + } + } + } + } + + // TODO(adonovan): FindPos needs a test (not just a benchmark). +} + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} + +// sliceTypes is a debugging helper that formats each slice element with %T. +func sliceTypes[T any](slice []T) string { + var buf strings.Builder + buf.WriteByte('[') + for i, elem := range slice { + if i > 0 { + buf.WriteByte(' ') + } + fmt.Fprintf(&buf, "%T", elem) + } + buf.WriteByte(']') + return buf.String() +} + +// (partially duplicates benchmark in go/ast/inspector) +func BenchmarkInspectCalls(b *testing.B) { + inspect := netInspect + b.ResetTimer() + + // Measure marginal cost of traversal. + + callExprs := []ast.Node{(*ast.CallExpr)(nil)} + + b.Run("Preorder", func(b *testing.B) { + var ncalls int + for range b.N { + inspect.Preorder(callExprs, func(n ast.Node) { + _ = n.(*ast.CallExpr) + ncalls++ + }) + } + }) + + b.Run("WithStack", func(b *testing.B) { + var ncalls int + for range b.N { + inspect.WithStack(callExprs, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) { + _ = n.(*ast.CallExpr) + if push { + ncalls++ + } + return true + }) + } + }) + + // Cursor.Stack(nil) is ~6x slower than WithStack. + // Even using Cursor.Stack(stack[:0]) to amortize the + // allocation, it's ~4x slower. + // + // But it depends on the selectivity of the nodeTypes + // filter: searching for *ast.InterfaceType, results in + // fewer calls to Stack, making it only 2x slower. + // And if the calls to Stack are very selective, + // or are replaced by 2 calls to Parent, it runs + // 27% faster than WithStack. + b.Run("CursorStack", func(b *testing.B) { + var ncalls int + for range b.N { + var stack []cursor.Cursor // recycle across calls + for cur := range cursor.Root(inspect).Preorder(callExprs...) { + _ = cur.Node().(*ast.CallExpr) + stack = cur.Stack(stack[:0]) + ncalls++ + } + } + }) +} + +// This benchmark compares methods for finding a known node in a tree. +func BenchmarkCursor_FindNode(b *testing.B) { + root := cursor.Root(netInspect) + + callExprs := []ast.Node{(*ast.CallExpr)(nil)} + + // Choose a needle in the haystack to use as the search target: + // a CallExpr not too near the start nor at too shallow a depth. + var needle cursor.Cursor + { + count := 0 + found := false + for c := range root.Preorder(callExprs...) { + count++ + if count >= 1000 && len(c.Stack(nil)) >= 6 { + needle = c + found = true + break + } + } + if !found { + b.Fatal("can't choose needle") + } + } + + b.ResetTimer() + + b.Run("Cursor.Preorder", func(b *testing.B) { + needleNode := needle.Node() + for range b.N { + var found cursor.Cursor + for c := range root.Preorder(callExprs...) { + if c.Node() == needleNode { + found = c + break + } + } + if found != needle { + b.Errorf("Preorder search failed: got %v, want %v", found, needle) + } + } + }) + + // This method is about 10-15% faster than Cursor.Preorder. + b.Run("Cursor.FindNode", func(b *testing.B) { + for range b.N { + found, ok := root.FindNode(needle.Node()) + if !ok || found != needle { + b.Errorf("FindNode search failed: got %v, want %v", found, needle) + } + } + }) + + // This method is about 100x (!) faster than Cursor.Preorder. + b.Run("Cursor.FindPos", func(b *testing.B) { + needleNode := needle.Node() + for range b.N { + found, ok := root.FindPos(needleNode.Pos(), needleNode.End()) + if !ok || found != needle { + b.Errorf("FindPos search failed: got %v, want %v", found, needle) + } + } + }) +} diff --git a/internal/astutil/cursor/hooks.go b/internal/astutil/cursor/hooks.go new file mode 100644 index 00000000000..47aaaae37e0 --- /dev/null +++ b/internal/astutil/cursor/hooks.go @@ -0,0 +1,33 @@ +// 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 cursor + +import ( + "go/ast" + _ "unsafe" // for go:linkname + + "golang.org/x/tools/go/ast/inspector" +) + +// This file defines backdoor access to inspector. + +// Copied from inspector.event; must remain in sync. +// (Note that the linkname effects a type coercion too.) +type event struct { + node ast.Node + typ uint64 // typeOf(node) on push event, or union of typ strictly between push and pop events on pop events + index int32 // index of corresponding push or pop event (relative to this event's index, +ve=push, -ve=pop) + parent int32 // index of parent's push node (defined for push nodes only) +} + +//go:linkname maskOf golang.org/x/tools/go/ast/inspector.maskOf +func maskOf(nodes []ast.Node) uint64 + +//go:linkname events golang.org/x/tools/go/ast/inspector.events +func events(in *inspector.Inspector) []event + +func (c Cursor) events() []event { return events(c.in) } diff --git a/internal/diff/lcs/old.go b/internal/diff/lcs/old.go index 4353da15ba9..7c74b47bb1c 100644 --- a/internal/diff/lcs/old.go +++ b/internal/diff/lcs/old.go @@ -199,6 +199,7 @@ func (e *editGraph) bdone(D, k int) (bool, lcs) { } // run the backward algorithm, until success or up to the limit on D. +// (used only by tests) func backward(e *editGraph) lcs { e.setBackward(0, 0, e.ux) if ok, ans := e.bdone(0, 0); ok { diff --git a/internal/event/export/metric/data.go b/internal/event/export/metric/data.go index f90fb804f28..4160df40680 100644 --- a/internal/event/export/metric/data.go +++ b/internal/event/export/metric/data.go @@ -34,7 +34,7 @@ type Int64Data struct { IsGauge bool // Rows holds the per group values for the metric. Rows []int64 - // End is the last time this metric was updated. + // EndTime is the last time this metric was updated. EndTime time.Time groups [][]label.Label @@ -49,7 +49,7 @@ type Float64Data struct { IsGauge bool // Rows holds the per group values for the metric. Rows []float64 - // End is the last time this metric was updated. + // EndTime is the last time this metric was updated. EndTime time.Time groups [][]label.Label @@ -62,7 +62,7 @@ type HistogramInt64Data struct { Info *HistogramInt64 // Rows holds the per group values for the metric. Rows []*HistogramInt64Row - // End is the last time this metric was updated. + // EndTime is the last time this metric was updated. EndTime time.Time groups [][]label.Label @@ -89,7 +89,7 @@ type HistogramFloat64Data struct { Info *HistogramFloat64 // Rows holds the per group values for the metric. Rows []*HistogramFloat64Row - // End is the last time this metric was updated. + // EndTime is the last time this metric was updated. EndTime time.Time groups [][]label.Label diff --git a/internal/gcimporter/exportdata.go b/internal/gcimporter/exportdata.go index 6f5d8a21391..5662a311dac 100644 --- a/internal/gcimporter/exportdata.go +++ b/internal/gcimporter/exportdata.go @@ -2,52 +2,183 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file is a copy of $GOROOT/src/go/internal/gcimporter/exportdata.go. - -// This file implements FindExportData. +// This file should be kept in sync with $GOROOT/src/internal/exportdata/exportdata.go. +// This file also additionally implements FindExportData for gcexportdata.NewReader. package gcimporter import ( "bufio" + "bytes" + "errors" "fmt" + "go/build" "io" - "strconv" + "os" + "os/exec" + "path/filepath" "strings" + "sync" ) -func readGopackHeader(r *bufio.Reader) (name string, size int64, err error) { - // See $GOROOT/include/ar.h. - hdr := make([]byte, 16+12+6+6+8+10+2) - _, err = io.ReadFull(r, hdr) +// FindExportData positions the reader r at the beginning of the +// export data section of an underlying cmd/compile created archive +// file by reading from it. The reader must be positioned at the +// start of the file before calling this function. +// This returns the length of the export data in bytes. +// +// This function is needed by [gcexportdata.Read], which must +// accept inputs produced by the last two releases of cmd/compile, +// plus tip. +func FindExportData(r *bufio.Reader) (size int64, err error) { + arsize, err := FindPackageDefinition(r) + if err != nil { + return + } + size = int64(arsize) + + objapi, headers, err := ReadObjectHeaders(r) + if err != nil { + return + } + size -= int64(len(objapi)) + for _, h := range headers { + size -= int64(len(h)) + } + + // Check for the binary export data section header "$$B\n". + // TODO(taking): Unify with ReadExportDataHeader so that it stops at the 'u' instead of reading + line, err := r.ReadSlice('\n') if err != nil { return } - // leave for debugging - if false { - fmt.Printf("header: %s", hdr) + hdr := string(line) + if hdr != "$$B\n" { + err = fmt.Errorf("unknown export data header: %q", hdr) + return } - s := strings.TrimSpace(string(hdr[16+12+6+6+8:][:10])) - length, err := strconv.Atoi(s) - size = int64(length) - if err != nil || hdr[len(hdr)-2] != '`' || hdr[len(hdr)-1] != '\n' { - err = fmt.Errorf("invalid archive header") + size -= int64(len(hdr)) + + // For files with a binary export data header "$$B\n", + // these are always terminated by an end-of-section marker "\n$$\n". + // So the last bytes must always be this constant. + // + // The end-of-section marker is not a part of the export data itself. + // Do not include these in size. + // + // It would be nice to have sanity check that the final bytes after + // the export data are indeed the end-of-section marker. The split + // of gcexportdata.NewReader and gcexportdata.Read make checking this + // ugly so gcimporter gives up enforcing this. The compiler and go/types + // importer do enforce this, which seems good enough. + const endofsection = "\n$$\n" + size -= int64(len(endofsection)) + + if size < 0 { + err = fmt.Errorf("invalid size (%d) in the archive file: %d bytes remain without section headers (recompile package)", arsize, size) return } - name = strings.TrimSpace(string(hdr[:16])) + return } -// FindExportData positions the reader r at the beginning of the -// export data section of an underlying cmd/compile created archive -// file by reading from it. The reader must be positioned at the -// start of the file before calling this function. -// The size result is the length of the export data in bytes. +// ReadUnified reads the contents of the unified export data from a reader r +// that contains the contents of a GC-created archive file. // -// This function is needed by [gcexportdata.Read], which must -// accept inputs produced by the last two releases of cmd/compile, -// plus tip. -func FindExportData(r *bufio.Reader) (size int64, err error) { +// On success, the reader will be positioned after the end-of-section marker "\n$$\n". +// +// Supported GC-created archive files have 4 layers of nesting: +// - An archive file containing a package definition file. +// - The package definition file contains headers followed by a data section. +// Headers are lines (≤ 4kb) that do not start with "$$". +// - The data section starts with "$$B\n" followed by export data followed +// by an end of section marker "\n$$\n". (The section start "$$\n" is no +// longer supported.) +// - The export data starts with a format byte ('u') followed by the in +// the given format. (See ReadExportDataHeader for older formats.) +// +// Putting this together, the bytes in a GC-created archive files are expected +// to look like the following. +// See cmd/internal/archive for more details on ar file headers. +// +// | \n | ar file signature +// | __.PKGDEF...size...\n | ar header for __.PKGDEF including size. +// | go object <...>\n | objabi header +// | \n | other headers such as build id +// | $$B\n | binary format marker +// | u\n | unified export +// | $$\n | end-of-section marker +// | [optional padding] | padding byte (0x0A) if size is odd +// | [ar file header] | other ar files +// | [ar file data] | +func ReadUnified(r *bufio.Reader) (data []byte, err error) { + // We historically guaranteed headers at the default buffer size (4096) work. + // This ensures we can use ReadSlice throughout. + const minBufferSize = 4096 + r = bufio.NewReaderSize(r, minBufferSize) + + size, err := FindPackageDefinition(r) + if err != nil { + return + } + n := size + + objapi, headers, err := ReadObjectHeaders(r) + if err != nil { + return + } + n -= len(objapi) + for _, h := range headers { + n -= len(h) + } + + hdrlen, err := ReadExportDataHeader(r) + if err != nil { + return + } + n -= hdrlen + + // size also includes the end of section marker. Remove that many bytes from the end. + const marker = "\n$$\n" + n -= len(marker) + + if n < 0 { + err = fmt.Errorf("invalid size (%d) in the archive file: %d bytes remain without section headers (recompile package)", size, n) + return + } + + // Read n bytes from buf. + data = make([]byte, n) + _, err = io.ReadFull(r, data) + if err != nil { + return + } + + // Check for marker at the end. + var suffix [len(marker)]byte + _, err = io.ReadFull(r, suffix[:]) + if err != nil { + return + } + if s := string(suffix[:]); s != marker { + err = fmt.Errorf("read %q instead of end-of-section marker (%q)", s, marker) + return + } + + return +} + +// FindPackageDefinition positions the reader r at the beginning of a package +// definition file ("__.PKGDEF") within a GC-created archive by reading +// from it, and returns the size of the package definition file in the archive. +// +// The reader must be positioned at the start of the archive file before calling +// this function, and "__.PKGDEF" is assumed to be the first file in the archive. +// +// See cmd/internal/archive for details on the archive format. +func FindPackageDefinition(r *bufio.Reader) (size int, err error) { + // Uses ReadSlice to limit risk of malformed inputs. + // Read first line to make sure this is an object file. line, err := r.ReadSlice('\n') if err != nil { @@ -61,56 +192,230 @@ func FindExportData(r *bufio.Reader) (size int64, err error) { return } - // Archive file. Scan to __.PKGDEF. - var name string - if name, size, err = readGopackHeader(r); err != nil { + // package export block should be first + size = readArchiveHeader(r, "__.PKGDEF") + if size <= 0 { + err = fmt.Errorf("not a package file") return } - arsize := size - // First entry should be __.PKGDEF. - if name != "__.PKGDEF" { - err = fmt.Errorf("go archive is missing __.PKGDEF") - return - } + return +} + +// ReadObjectHeaders reads object headers from the reader. Object headers are +// lines that do not start with an end-of-section marker "$$". The first header +// is the objabi header. On success, the reader will be positioned at the beginning +// of the end-of-section marker. +// +// It returns an error if any header does not fit in r.Size() bytes. +func ReadObjectHeaders(r *bufio.Reader) (objapi string, headers []string, err error) { + // line is a temporary buffer for headers. + // Use bounded reads (ReadSlice, Peek) to limit risk of malformed inputs. + var line []byte - // Read first line of __.PKGDEF data, so that line - // is once again the first line of the input. + // objapi header should be the first line if line, err = r.ReadSlice('\n'); err != nil { err = fmt.Errorf("can't find export data (%v)", err) return } - size -= int64(len(line)) + objapi = string(line) - // Now at __.PKGDEF in archive or still at beginning of file. - // Either way, line should begin with "go object ". - if !strings.HasPrefix(string(line), "go object ") { - err = fmt.Errorf("not a Go object file") + // objapi header begins with "go object ". + if !strings.HasPrefix(objapi, "go object ") { + err = fmt.Errorf("not a go object file: %s", objapi) return } - // Skip over object headers to get to the export data section header "$$B\n". - // Object headers are lines that do not start with '$'. - for line[0] != '$' { - if line, err = r.ReadSlice('\n'); err != nil { - err = fmt.Errorf("can't find export data (%v)", err) + // process remaining object header lines + for { + // check for an end of section marker "$$" + line, err = r.Peek(2) + if err != nil { + return + } + if string(line) == "$$" { + return // stop + } + + // read next header + line, err = r.ReadSlice('\n') + if err != nil { return } - size -= int64(len(line)) + headers = append(headers, string(line)) } +} - // Check for the binary export data section header "$$B\n". - hdr := string(line) - if hdr != "$$B\n" { - err = fmt.Errorf("unknown export data header: %q", hdr) +// ReadExportDataHeader reads the export data header and format from r. +// It returns the number of bytes read, or an error if the format is no longer +// supported or it failed to read. +// +// The only currently supported format is binary export data in the +// unified export format. +func ReadExportDataHeader(r *bufio.Reader) (n int, err error) { + // Read export data header. + line, err := r.ReadSlice('\n') + if err != nil { return } - // TODO(taking): Remove end-of-section marker "\n$$\n" from size. - if size < 0 { - err = fmt.Errorf("invalid size (%d) in the archive file: %d bytes remain without section headers (recompile package)", arsize, size) + hdr := string(line) + switch hdr { + case "$$\n": + err = fmt.Errorf("old textual export format no longer supported (recompile package)") + return + + case "$$B\n": + var format byte + format, err = r.ReadByte() + if err != nil { + return + } + // The unified export format starts with a 'u'. + switch format { + case 'u': + default: + // Older no longer supported export formats include: + // indexed export format which started with an 'i'; and + // the older binary export format which started with a 'c', + // 'd', or 'v' (from "version"). + err = fmt.Errorf("binary export format %q is no longer supported (recompile package)", format) + return + } + + default: + err = fmt.Errorf("unknown export data header: %q", hdr) return } + n = len(hdr) + 1 // + 1 is for 'u' return } + +// FindPkg returns the filename and unique package id for an import +// path based on package information provided by build.Import (using +// the build.Default build.Context). A relative srcDir is interpreted +// relative to the current working directory. +// +// FindPkg is only used in tests within x/tools. +func FindPkg(path, srcDir string) (filename, id string, err error) { + // TODO(taking): Move internal/exportdata.FindPkg into its own file, + // and then this copy into a _test package. + if path == "" { + return "", "", errors.New("path is empty") + } + + var noext string + switch { + default: + // "x" -> "$GOPATH/pkg/$GOOS_$GOARCH/x.ext", "x" + // Don't require the source files to be present. + if abs, err := filepath.Abs(srcDir); err == nil { // see issue 14282 + srcDir = abs + } + var bp *build.Package + bp, err = build.Import(path, srcDir, build.FindOnly|build.AllowBinary) + if bp.PkgObj == "" { + if bp.Goroot && bp.Dir != "" { + filename, err = lookupGorootExport(bp.Dir) + if err == nil { + _, err = os.Stat(filename) + } + if err == nil { + return filename, bp.ImportPath, nil + } + } + goto notfound + } else { + noext = strings.TrimSuffix(bp.PkgObj, ".a") + } + id = bp.ImportPath + + case build.IsLocalImport(path): + // "./x" -> "/this/directory/x.ext", "/this/directory/x" + noext = filepath.Join(srcDir, path) + id = noext + + case filepath.IsAbs(path): + // for completeness only - go/build.Import + // does not support absolute imports + // "/x" -> "/x.ext", "/x" + noext = path + id = path + } + + if false { // for debugging + if path != id { + fmt.Printf("%s -> %s\n", path, id) + } + } + + // try extensions + for _, ext := range pkgExts { + filename = noext + ext + f, statErr := os.Stat(filename) + if statErr == nil && !f.IsDir() { + return filename, id, nil + } + if err == nil { + err = statErr + } + } + +notfound: + if err == nil { + return "", path, fmt.Errorf("can't find import: %q", path) + } + return "", path, fmt.Errorf("can't find import: %q: %w", path, err) +} + +var pkgExts = [...]string{".a", ".o"} // a file from the build cache will have no extension + +var exportMap sync.Map // package dir → func() (string, error) + +// lookupGorootExport returns the location of the export data +// (normally found in the build cache, but located in GOROOT/pkg +// in prior Go releases) for the package located in pkgDir. +// +// (We use the package's directory instead of its import path +// mainly to simplify handling of the packages in src/vendor +// and cmd/vendor.) +// +// lookupGorootExport is only used in tests within x/tools. +func lookupGorootExport(pkgDir string) (string, error) { + f, ok := exportMap.Load(pkgDir) + if !ok { + var ( + listOnce sync.Once + exportPath string + err error + ) + f, _ = exportMap.LoadOrStore(pkgDir, func() (string, error) { + listOnce.Do(func() { + cmd := exec.Command(filepath.Join(build.Default.GOROOT, "bin", "go"), "list", "-export", "-f", "{{.Export}}", pkgDir) + cmd.Dir = build.Default.GOROOT + cmd.Env = append(os.Environ(), "PWD="+cmd.Dir, "GOROOT="+build.Default.GOROOT) + var output []byte + output, err = cmd.Output() + if err != nil { + if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { + err = errors.New(string(ee.Stderr)) + } + return + } + + exports := strings.Split(string(bytes.TrimSpace(output)), "\n") + if len(exports) != 1 { + err = fmt.Errorf("go list reported %d exports; expected 1", len(exports)) + return + } + + exportPath = exports[0] + }) + + return exportPath, err + }) + } + + return f.(func() (string, error))() +} diff --git a/internal/gcimporter/gcimporter.go b/internal/gcimporter/gcimporter.go index dbbca860432..3dbd21d1b90 100644 --- a/internal/gcimporter/gcimporter.go +++ b/internal/gcimporter/gcimporter.go @@ -23,17 +23,11 @@ package gcimporter // import "golang.org/x/tools/internal/gcimporter" import ( "bufio" - "bytes" "fmt" - "go/build" "go/token" "go/types" "io" "os" - "os/exec" - "path/filepath" - "strings" - "sync" ) const ( @@ -45,127 +39,14 @@ const ( trace = false ) -var exportMap sync.Map // package dir → func() (string, bool) - -// lookupGorootExport returns the location of the export data -// (normally found in the build cache, but located in GOROOT/pkg -// in prior Go releases) for the package located in pkgDir. -// -// (We use the package's directory instead of its import path -// mainly to simplify handling of the packages in src/vendor -// and cmd/vendor.) -func lookupGorootExport(pkgDir string) (string, bool) { - f, ok := exportMap.Load(pkgDir) - if !ok { - var ( - listOnce sync.Once - exportPath string - ) - f, _ = exportMap.LoadOrStore(pkgDir, func() (string, bool) { - listOnce.Do(func() { - cmd := exec.Command("go", "list", "-export", "-f", "{{.Export}}", pkgDir) - cmd.Dir = build.Default.GOROOT - var output []byte - output, err := cmd.Output() - if err != nil { - return - } - - exports := strings.Split(string(bytes.TrimSpace(output)), "\n") - if len(exports) != 1 { - return - } - - exportPath = exports[0] - }) - - return exportPath, exportPath != "" - }) - } - - return f.(func() (string, bool))() -} - -var pkgExts = [...]string{".a", ".o"} - -// FindPkg returns the filename and unique package id for an import -// path based on package information provided by build.Import (using -// the build.Default build.Context). A relative srcDir is interpreted -// relative to the current working directory. -// If no file was found, an empty filename is returned. -func FindPkg(path, srcDir string) (filename, id string) { - if path == "" { - return - } - - var noext string - switch { - default: - // "x" -> "$GOPATH/pkg/$GOOS_$GOARCH/x.ext", "x" - // Don't require the source files to be present. - if abs, err := filepath.Abs(srcDir); err == nil { // see issue 14282 - srcDir = abs - } - bp, _ := build.Import(path, srcDir, build.FindOnly|build.AllowBinary) - if bp.PkgObj == "" { - var ok bool - if bp.Goroot && bp.Dir != "" { - filename, ok = lookupGorootExport(bp.Dir) - } - if !ok { - id = path // make sure we have an id to print in error message - return - } - } else { - noext = strings.TrimSuffix(bp.PkgObj, ".a") - id = bp.ImportPath - } - - case build.IsLocalImport(path): - // "./x" -> "/this/directory/x.ext", "/this/directory/x" - noext = filepath.Join(srcDir, path) - id = noext - - case filepath.IsAbs(path): - // for completeness only - go/build.Import - // does not support absolute imports - // "/x" -> "/x.ext", "/x" - noext = path - id = path - } - - if false { // for debugging - if path != id { - fmt.Printf("%s -> %s\n", path, id) - } - } - - if filename != "" { - if f, err := os.Stat(filename); err == nil && !f.IsDir() { - return - } - } - - // try extensions - for _, ext := range pkgExts { - filename = noext + ext - if f, err := os.Stat(filename); err == nil && !f.IsDir() { - return - } - } - - filename = "" // not found - return -} - // Import imports a gc-generated package given its import path and srcDir, adds // the corresponding package object to the packages map, and returns the object. // The packages map must contain all packages already imported. // -// TODO(taking): Import is only used in tests. Move to gcimporter_test. -func Import(packages map[string]*types.Package, path, srcDir string, lookup func(path string) (io.ReadCloser, error)) (pkg *types.Package, err error) { +// Import is only used in tests. +func Import(fset *token.FileSet, packages map[string]*types.Package, path, srcDir string, lookup func(path string) (io.ReadCloser, error)) (pkg *types.Package, err error) { var rc io.ReadCloser - var filename, id string + var id string if lookup != nil { // With custom lookup specified, assume that caller has // converted path to a canonical import path for use in the map. @@ -184,12 +65,13 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func } rc = f } else { - filename, id = FindPkg(path, srcDir) + var filename string + filename, id, err = FindPkg(path, srcDir) if filename == "" { if path == "unsafe" { return types.Unsafe, nil } - return nil, fmt.Errorf("can't find import: %q", id) + return nil, err } // no need to re-import if the package was imported completely before @@ -212,54 +94,15 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func } defer rc.Close() - var size int64 buf := bufio.NewReader(rc) - if size, err = FindExportData(buf); err != nil { - return - } - - var data []byte - data, err = io.ReadAll(buf) + data, err := ReadUnified(buf) if err != nil { + err = fmt.Errorf("import %q: %v", path, err) return } - if len(data) == 0 { - return nil, fmt.Errorf("no data to load a package from for path %s", id) - } - - // TODO(gri): allow clients of go/importer to provide a FileSet. - // Or, define a new standard go/types/gcexportdata package. - fset := token.NewFileSet() - - // Select appropriate importer. - switch data[0] { - 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: 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 + // unified: emitted by cmd/compile since go1.20. + _, pkg, err = UImportData(fset, packages, data, id) - case 'u': - // unified: emitted by cmd/compile since go1.20. - _, pkg, err := UImportData(fset, packages, data[1:size], id) - return pkg, err - - default: - l := len(data) - if l > 10 { - l = 10 - } - return nil, fmt.Errorf("unexpected export data with prefix %q for path %s", string(data[:l]), id) - } + return } - -type byPath []*types.Package - -func (a byPath) Len() int { return len(a) } -func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a byPath) Less(i, j int) bool { return a[i].Path() < a[j].Path() } diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 7848f27e0c2..9b38a0e1e28 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -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 := gcimporter.Import(make(map[string]*types.Package), path, srcDir, nil) + pkg, err := gcimporter.Import(token.NewFileSet(), make(map[string]*types.Package), path, srcDir, nil) if err != nil { t.Errorf("testPath(%s): %s", path, err) return nil @@ -129,9 +129,9 @@ func testImportTestdata(t *testing.T) { packageFiles := map[string]string{} for _, pkg := range []string{"go/ast", "go/token"} { - export, _ := gcimporter.FindPkg(pkg, "testdata") + export, _, err := gcimporter.FindPkg(pkg, "testdata") if export == "" { - t.Fatalf("no export data found for %s", pkg) + t.Fatalf("no export data found for %s: %s", pkg, err) } packageFiles[pkg] = export } @@ -314,7 +314,7 @@ func TestVersionHandling(t *testing.T) { } // test that export data can be imported - _, err := gcimporter.Import(make(map[string]*types.Package), pkgpath, dir, nil) + _, err := gcimporter.Import(token.NewFileSet(), make(map[string]*types.Package), pkgpath, dir, nil) if err != nil { t.Errorf("import %q failed: %v", pkgpath, err) continue @@ -327,6 +327,7 @@ func TestVersionHandling(t *testing.T) { t.Fatal(err) } // 2) find export data + // Index is an incorrect but 'good enough for tests' way to find the end of the export data. i := bytes.Index(data, []byte("\n$$B\n")) + 5 j := bytes.Index(data[i:], []byte("\n$$\n")) + i if i < 0 || j < 0 || i > j { @@ -342,7 +343,7 @@ func TestVersionHandling(t *testing.T) { os.WriteFile(filename, data, 0666) // test that importing the corrupted file results in an error - _, err = gcimporter.Import(make(map[string]*types.Package), pkgpath, corruptdir, nil) + _, err = gcimporter.Import(token.NewFileSet(), 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") { @@ -472,7 +473,7 @@ func importObject(t *testing.T, name string) types.Object { importPath := s[0] objName := s[1] - pkg, err := gcimporter.Import(make(map[string]*types.Package), importPath, ".", nil) + pkg, err := gcimporter.Import(token.NewFileSet(), make(map[string]*types.Package), importPath, ".", nil) if err != nil { t.Error(err) return nil @@ -554,7 +555,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 := gcimporter.Import(imports, "net/http", ".", nil) + _, err := gcimporter.Import(token.NewFileSet(), imports, "net/http", ".", nil) if err != nil { t.Fatal(err) } @@ -586,9 +587,9 @@ func TestIssue13566(t *testing.T) { t.Fatal(err) } - jsonExport, _ := gcimporter.FindPkg("encoding/json", "testdata") + jsonExport, _, err := gcimporter.FindPkg("encoding/json", "testdata") if jsonExport == "" { - t.Fatalf("no export data found for encoding/json") + t.Fatalf("no export data found for encoding/json: %s", err) } compilePkg(t, "testdata", "a.go", testoutdir, map[string]string{"encoding/json": jsonExport}, apkg(testoutdir)) @@ -612,7 +613,7 @@ func TestIssue13898(t *testing.T) { // import go/internal/gcimporter which imports go/types partially imports := make(map[string]*types.Package) - _, err := gcimporter.Import(imports, "go/internal/gcimporter", ".", nil) + _, err := gcimporter.Import(token.NewFileSet(), imports, "go/internal/gcimporter", ".", nil) if err != nil { t.Fatal(err) } @@ -672,7 +673,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 := gcimporter.Import(imports, "./././testdata/p", tmpdir, nil); err != nil { + if _, err := gcimporter.Import(token.NewFileSet(), imports, "./././testdata/p", tmpdir, nil); err != nil { t.Fatal(err) } } @@ -908,7 +909,7 @@ func TestIssue58296(t *testing.T) { } // make sure a and b are both imported by c. - pkg, err := gcimporter.Import(imports, "./c", testoutdir, nil) + pkg, err := gcimporter.Import(token.NewFileSet(), imports, "./c", testoutdir, nil) if err != nil { t.Fatal(err) } @@ -951,7 +952,7 @@ func TestIssueAliases(t *testing.T) { ) // import c from gc export data using a and b. - pkg, err := gcimporter.Import(map[string]*types.Package{ + pkg, err := gcimporter.Import(token.NewFileSet(), map[string]*types.Package{ apkg: types.NewPackage(apkg, "a"), bpkg: types.NewPackage(bpkg, "b"), }, "./c", testoutdir, nil) @@ -995,7 +996,7 @@ func apkg(testoutdir string) string { } func importPkg(t *testing.T, path, srcDir string) *types.Package { - pkg, err := gcimporter.Import(make(map[string]*types.Package), path, srcDir, nil) + pkg, err := gcimporter.Import(token.NewFileSet(), make(map[string]*types.Package), path, srcDir, nil) if err != nil { t.Fatal(err) } diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index e260c0e8dbf..69b1d697cbe 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -5,8 +5,6 @@ // Indexed package import. // See iexport.go for the export data format. -// This file is a copy of $GOROOT/src/go/internal/gcimporter/iimport.go. - package gcimporter import ( @@ -1111,3 +1109,9 @@ func (r *importReader) byte() byte { } return x } + +type byPath []*types.Package + +func (a byPath) Len() int { return len(a) } +func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byPath) Less(i, j int) bool { return a[i].Path() < a[j].Path() } diff --git a/internal/gcimporter/support.go b/internal/gcimporter/support.go new file mode 100644 index 00000000000..4af810dc412 --- /dev/null +++ b/internal/gcimporter/support.go @@ -0,0 +1,30 @@ +// 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 ( + "bufio" + "io" + "strconv" + "strings" +) + +// Copy of $GOROOT/src/cmd/internal/archive.ReadHeader. +func readArchiveHeader(b *bufio.Reader, name string) int { + // architecture-independent object file output + const HeaderSize = 60 + + var buf [HeaderSize]byte + if _, err := io.ReadFull(b, buf[:]); err != nil { + return -1 + } + aname := strings.Trim(string(buf[0:16]), " ") + if !strings.HasPrefix(aname, name) { + return -1 + } + asize := strings.Trim(string(buf[48:58]), " ") + i, _ := strconv.Atoi(asize) + return i +} diff --git a/internal/gcimporter/testdata/versions/test_go1.16_i.a b/internal/gcimporter/testdata/versions/test_go1.16_i.a deleted file mode 100644 index 35dc863e81c..00000000000 Binary files a/internal/gcimporter/testdata/versions/test_go1.16_i.a and /dev/null differ diff --git a/internal/gcimporter/testdata/versions/test_go1.17_i.a b/internal/gcimporter/testdata/versions/test_go1.17_i.a deleted file mode 100644 index 7a8ecb75c7e..00000000000 Binary files a/internal/gcimporter/testdata/versions/test_go1.17_i.a and /dev/null differ diff --git a/internal/gcimporter/testdata/versions/test_go1.18.5_i.a b/internal/gcimporter/testdata/versions/test_go1.18.5_i.a deleted file mode 100644 index 6ed126f7e92..00000000000 Binary files a/internal/gcimporter/testdata/versions/test_go1.18.5_i.a and /dev/null differ diff --git a/internal/gcimporter/testdata/versions/test_go1.19_i.a b/internal/gcimporter/testdata/versions/test_go1.19_i.a deleted file mode 100644 index ff8f5995bb8..00000000000 Binary files a/internal/gcimporter/testdata/versions/test_go1.19_i.a and /dev/null differ diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index 1db408613c9..6cdab448eca 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -11,7 +11,6 @@ import ( "go/token" "go/types" "sort" - "strings" "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/pkgbits" @@ -71,7 +70,6 @@ func UImportData(fset *token.FileSet, imports map[string]*types.Package, data [] } s := string(data) - s = s[:strings.LastIndex(s, "\n$$\n")] input := pkgbits.NewPkgDecoder(path, s) pkg = readUnifiedPackage(fset, nil, imports, input) return @@ -266,7 +264,12 @@ func (pr *pkgReader) pkgIdx(idx pkgbits.Index) *types.Package { func (r *reader) doPkg() *types.Package { path := r.String() switch path { - case "": + // cmd/compile emits path="main" for main packages because + // that's the linker symbol prefix it used; but we need + // the package's path as it would be reported by go list, + // hence "main" below. + // See test at go/packages.TestMainPackagePathInModeTypes. + case "", "main": path = r.p.PkgPath() case "builtin": return nil // universe diff --git a/internal/imports/sourcex_test.go b/internal/imports/sourcex_test.go index e8a4d537f8f..0a2327ca300 100644 --- a/internal/imports/sourcex_test.go +++ b/internal/imports/sourcex_test.go @@ -93,7 +93,7 @@ type dirs struct { func testDirs(t *testing.T) dirs { t.Helper() dir := t.TempDir() - modindex.IndexDir = func() (string, error) { return dir, nil } + modindex.IndexDir = dir x := dirs{ tmpdir: dir, cachedir: filepath.Join(dir, "pkg", "mod"), diff --git a/internal/jsonrpc2/wire.go b/internal/jsonrpc2/wire.go index f2aa2d63e8c..7dcd262ec11 100644 --- a/internal/jsonrpc2/wire.go +++ b/internal/jsonrpc2/wire.go @@ -47,7 +47,7 @@ type wireRequest struct { ID *ID `json:"id,omitempty"` } -// WireResponse is a reply to a Request. +// wireResponse is a reply to a Request. // It will always have the ID field set to tie it back to a request, and will // have either the Result or Error fields set depending on whether it is a // success or failure response. diff --git a/internal/jsonrpc2_v2/jsonrpc2.go b/internal/jsonrpc2_v2/jsonrpc2.go index e9164b0bc95..9d775de0603 100644 --- a/internal/jsonrpc2_v2/jsonrpc2.go +++ b/internal/jsonrpc2_v2/jsonrpc2.go @@ -112,15 +112,6 @@ func (a *async) done() { close(a.ready) } -func (a *async) isDone() bool { - select { - case <-a.ready: - return true - default: - return false - } -} - func (a *async) wait() error { <-a.ready err := <-a.firstErr diff --git a/internal/jsonrpc2_v2/serve_go116.go b/internal/jsonrpc2_v2/serve_go116.go index 29549f1059d..2dac7413f31 100644 --- a/internal/jsonrpc2_v2/serve_go116.go +++ b/internal/jsonrpc2_v2/serve_go116.go @@ -8,12 +8,7 @@ package jsonrpc2 import ( - "errors" "net" ) var errClosed = net.ErrClosed - -func isErrClosed(err error) bool { - return errors.Is(err, errClosed) -} diff --git a/internal/jsonrpc2_v2/serve_pre116.go b/internal/jsonrpc2_v2/serve_pre116.go index a1801d8a200..ef5477fecb9 100644 --- a/internal/jsonrpc2_v2/serve_pre116.go +++ b/internal/jsonrpc2_v2/serve_pre116.go @@ -9,22 +9,8 @@ package jsonrpc2 import ( "errors" - "strings" ) // errClosed is an error with the same string as net.ErrClosed, // which was added in Go 1.16. var errClosed = errors.New("use of closed network connection") - -// isErrClosed reports whether err ends in the same string as errClosed. -func isErrClosed(err error) bool { - // As of Go 1.16, this could be 'errors.Is(err, net.ErrClosing)', but - // unfortunately gopls still requires compatibility with - // (otherwise-unsupported) older Go versions. - // - // In the meantime, this error string has not changed on any supported Go - // version, and is not expected to change in the future. - // This is not ideal, but since the worst that could happen here is some - // superfluous logging, it is acceptable. - return strings.HasSuffix(err.Error(), "use of closed network connection") -} diff --git a/internal/modindex/dir_test.go b/internal/modindex/dir_test.go index 6e76f825116..e0919e4c4bf 100644 --- a/internal/modindex/dir_test.go +++ b/internal/modindex/dir_test.go @@ -47,10 +47,8 @@ var idtests = []id{ } func testModCache(t *testing.T) string { - t.Helper() - dir := t.TempDir() - IndexDir = func() (string, error) { return dir, nil } - return dir + IndexDir = t.TempDir() + return IndexDir } // add a trivial package to the test module cache @@ -211,11 +209,7 @@ func TestMissingCachedir(t *testing.T) { if err := Create(dir); err != nil { t.Fatal(err) } - ixd, err := IndexDir() - if err != nil { - t.Fatal(err) - } - des, err := os.ReadDir(ixd) + des, err := os.ReadDir(IndexDir) if err != nil { t.Fatal(err) } @@ -232,11 +226,7 @@ func TestMissingIndex(t *testing.T) { } else if !ok { t.Error("Update returned !ok") } - ixd, err := IndexDir() - if err != nil { - t.Fatal(err) - } - des, err := os.ReadDir(ixd) + des, err := os.ReadDir(IndexDir) if err != nil { t.Fatal(err) } diff --git a/internal/modindex/gomodindex/cmd.go b/internal/modindex/gomodindex/cmd.go index 06314826422..4fc0caf400e 100644 --- a/internal/modindex/gomodindex/cmd.go +++ b/internal/modindex/gomodindex/cmd.go @@ -93,10 +93,7 @@ func query(dir string) { panic("implement") } func clean(_ string) { - des, err := modindex.IndexDir() - if err != nil { - log.Fatal(err) - } + des := modindex.IndexDir // look at the files starting with 'index' // the current ones of each version are pointed to by // index-name-%d files. Any others more than an hour old diff --git a/internal/modindex/index.go b/internal/modindex/index.go index 27b6dd832d7..9665356c01b 100644 --- a/internal/modindex/index.go +++ b/internal/modindex/index.go @@ -17,6 +17,7 @@ import ( "path/filepath" "strconv" "strings" + "testing" "time" ) @@ -85,6 +86,28 @@ type Entry struct { Names []string // exported names and information } +// IndexDir is where the module index is stored. +var IndexDir string + +// Set IndexDir +func init() { + var dir string + var err error + if testing.Testing() { + dir = os.TempDir() + } else { + dir, err = os.UserCacheDir() + // shouldn't happen, but TempDir is better than + // creating ./go/imports + if err != nil { + dir = os.TempDir() + } + } + dir = filepath.Join(dir, "go", "imports") + os.MkdirAll(dir, 0777) + IndexDir = dir +} + // ReadIndex reads the latest version of the on-disk index // for the cache directory cd. // It returns (nil, nil) if there is no index, but returns @@ -95,10 +118,7 @@ func ReadIndex(cachedir string) (*Index, error) { return nil, err } cd := Abspath(cachedir) - dir, err := IndexDir() - if err != nil { - return nil, err - } + dir := IndexDir base := indexNameBase(cd) iname := filepath.Join(dir, base) buf, err := os.ReadFile(iname) @@ -185,12 +205,8 @@ func readIndexFrom(cd Abspath, bx io.Reader) (*Index, error) { // write the index as a text file func writeIndex(cachedir Abspath, ix *Index) error { - dir, err := IndexDir() - if err != nil { - return err - } ipat := fmt.Sprintf("index-%d-*", CurrentVersion) - fd, err := os.CreateTemp(dir, ipat) + fd, err := os.CreateTemp(IndexDir, ipat) if err != nil { return err // can this happen? } @@ -201,7 +217,7 @@ func writeIndex(cachedir Abspath, ix *Index) error { content := fd.Name() content = filepath.Base(content) base := indexNameBase(cachedir) - nm := filepath.Join(dir, base) + nm := filepath.Join(IndexDir, base) err = os.WriteFile(nm, []byte(content), 0666) if err != nil { return err @@ -241,18 +257,6 @@ func writeIndexToFile(x *Index, fd *os.File) error { return nil } -// tests can override this -var IndexDir = indexDir - -// IndexDir computes the directory containing the index -func indexDir() (string, error) { - dir, err := os.UserCacheDir() - if err != nil { - return "", fmt.Errorf("cannot open UserCacheDir, %w", err) - } - return filepath.Join(dir, "go", "imports"), nil -} - // return the base name of the file containing the name of the current index func indexNameBase(cachedir Abspath) string { // crc64 is a way to convert path names into 16 hex digits. diff --git a/internal/modindex/lookup.go b/internal/modindex/lookup.go index 29d4e3d7a39..012fdd7134c 100644 --- a/internal/modindex/lookup.go +++ b/internal/modindex/lookup.go @@ -16,6 +16,7 @@ type Candidate struct { Dir string ImportPath string Type LexType + Deprecated bool // information for Funcs Results int16 // how many results Sig []Field // arg names and types @@ -79,8 +80,9 @@ func (ix *Index) Lookup(pkg, name string, prefix bool) []Candidate { Dir: string(e.Dir), ImportPath: e.ImportPath, Type: asLexType(flds[1][0]), + Deprecated: len(flds[1]) > 1 && flds[1][1] == 'D', } - if flds[1] == "F" { + if px.Type == Func { n, err := strconv.Atoi(flds[2]) if err != nil { continue // should never happen @@ -111,6 +113,7 @@ func toFields(sig []string) []Field { } // benchmarks show this is measurably better than strings.Split +// split into first 4 fields separated by single space func fastSplit(x string) []string { ans := make([]string, 0, 4) nxt := 0 diff --git a/internal/modindex/lookup_test.go b/internal/modindex/lookup_test.go index 6a663554d73..4c5ae35695d 100644 --- a/internal/modindex/lookup_test.go +++ b/internal/modindex/lookup_test.go @@ -28,28 +28,34 @@ var thedata = tdata{ fname: "cloud.google.com/go/longrunning@v0.4.1/foo.go", pkg: "foo", items: []titem{ - // these need to be in alphabetical order by symbol - {"func Foo() {}", result{"Foo", Func, 0, nil}}, - {"const FooC = 23", result{"FooC", Const, 0, nil}}, - {"func FooF(int, float) error {return nil}", result{"FooF", Func, 1, + // these need to be in alphabetical order + {"func Foo() {}", result{"Foo", Func, false, 0, nil}}, + {"const FooC = 23", result{"FooC", Const, false, 0, nil}}, + {"func FooF(int, float) error {return nil}", result{"FooF", Func, false, 1, []Field{{"_", "int"}, {"_", "float"}}}}, - {"type FooT struct{}", result{"FooT", Type, 0, nil}}, - {"var FooV int", result{"FooV", Var, 0, nil}}, - {"func Ⱋoox(x int) {}", result{"Ⱋoox", Func, 0, []Field{{"x", "int"}}}}, + {"type FooT struct{}", result{"FooT", Type, false, 0, nil}}, + {"var FooV int", result{"FooV", Var, false, 0, nil}}, + {"func Goo() {}", result{"Goo", Func, false, 0, nil}}, + {"/*Deprecated: too weird\n*/\n// Another Goo\nvar GooVV int", result{"GooVV", Var, true, 0, nil}}, + {"func Ⱋoox(x int) {}", result{"Ⱋoox", Func, false, 0, []Field{{"x", "int"}}}}, }, } type result struct { - name string - typ LexType - result int - sig []Field + name string + typ LexType + deprecated bool + result int + sig []Field } func okresult(r result, p Candidate) bool { if r.name != p.Name || r.typ != p.Type || r.result != int(p.Results) { return false } + if r.deprecated != p.Deprecated { + return false + } if len(r.sig) != len(p.Sig) { return false } @@ -78,7 +84,6 @@ func TestLookup(t *testing.T) { // get all the symbols p := ix.Lookup("foo", "", true) if len(p) != len(thedata.items) { - // we should have gotten them all t.Errorf("got %d possibilities for pkg foo, expected %d", len(p), len(thedata.items)) } for i, r := range thedata.items { diff --git a/internal/modindex/symbols.go b/internal/modindex/symbols.go index 2e285ed996a..33bf2641f7b 100644 --- a/internal/modindex/symbols.go +++ b/internal/modindex/symbols.go @@ -19,12 +19,13 @@ import ( ) // The name of a symbol contains information about the symbol: -// T for types -// C for consts -// V for vars +// T for types, TD if the type is deprecated +// C for consts, CD if the const is deprecated +// V for vars, VD if the var is deprecated // and for funcs: F ( )* // any spaces in are replaced by $s so that the fields -// of the name are space separated +// of the name are space separated. F is replaced by FD if the func +// is deprecated. type symbol struct { pkg string // name of the symbols's package name string // declared name @@ -41,7 +42,7 @@ func getSymbols(cd Abspath, dirs map[string][]*directory) { d := vv[0] g.Go(func() error { thedir := filepath.Join(string(cd), string(d.path)) - mode := parser.SkipObjectResolution + mode := parser.SkipObjectResolution | parser.ParseComments fi, err := os.ReadDir(thedir) if err != nil { @@ -84,6 +85,9 @@ func getFileExports(f *ast.File) []symbol { // generic functions just like non-generic ones. sig := dtype.Params kind := "F" + if isDeprecated(decl.Doc) { + kind += "D" + } result := []string{fmt.Sprintf("%d", dtype.Results.NumFields())} for _, x := range sig.List { // This code creates a string representing the type. @@ -127,12 +131,16 @@ func getFileExports(f *ast.File) []symbol { ans = append(ans, *s) } case *ast.GenDecl: + depr := isDeprecated(decl.Doc) switch decl.Tok { case token.CONST, token.VAR: tp := "V" if decl.Tok == token.CONST { tp = "C" } + if depr { + tp += "D" + } for _, sp := range decl.Specs { for _, x := range sp.(*ast.ValueSpec).Names { if s := newsym(pkg, x.Name, tp, ""); s != nil { @@ -141,8 +149,12 @@ func getFileExports(f *ast.File) []symbol { } } case token.TYPE: + tp := "T" + if depr { + tp += "D" + } for _, sp := range decl.Specs { - if s := newsym(pkg, sp.(*ast.TypeSpec).Name.Name, "T", ""); s != nil { + if s := newsym(pkg, sp.(*ast.TypeSpec).Name.Name, tp, ""); s != nil { ans = append(ans, *s) } } @@ -160,6 +172,22 @@ func newsym(pkg, name, kind, sig string) *symbol { return &sym } +func isDeprecated(doc *ast.CommentGroup) bool { + if doc == nil { + return false + } + // go.dev/wiki/Deprecated Paragraph starting 'Deprecated:' + // This code fails for /* Deprecated: */, but it's the code from + // gopls/internal/analysis/deprecated + lines := strings.Split(doc.Text(), "\n\n") + for _, line := range lines { + if strings.HasPrefix(line, "Deprecated:") { + return true + } + } + return false +} + // return the package name and the value for the symbols. // if there are multiple packages, choose one arbitrarily // the returned slice is sorted lexicographically diff --git a/internal/refactor/inline/analyzer/analyzer.go b/internal/refactor/inline/analyzer/analyzer.go index f0519469b5e..0e3fec82f95 100644 --- a/internal/refactor/inline/analyzer/analyzer.go +++ b/internal/refactor/inline/analyzer/analyzer.go @@ -9,8 +9,7 @@ import ( "go/ast" "go/token" "go/types" - "os" - "strings" + "slices" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -20,28 +19,26 @@ import ( "golang.org/x/tools/internal/refactor/inline" ) -const Doc = `inline calls to functions with "inlineme" doc comment` +const Doc = `inline calls to functions with "//go:fix inline" doc comment` var Analyzer = &analysis.Analyzer{ Name: "inline", Doc: Doc, URL: "https://pkg.go.dev/golang.org/x/tools/internal/refactor/inline/analyzer", Run: run, - FactTypes: []analysis.Fact{new(inlineMeFact)}, + FactTypes: []analysis.Fact{new(goFixInlineFact)}, Requires: []*analysis.Analyzer{inspect.Analyzer}, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { // Memoize repeated calls for same file. - // TODO(adonovan): the analysis.Pass should abstract this (#62292) - // as the driver may not be reading directly from the file system. fileContent := make(map[string][]byte) readFile := func(node ast.Node) ([]byte, error) { filename := pass.Fset.File(node.Pos()).Name() content, ok := fileContent[filename] if !ok { var err error - content, err = os.ReadFile(filename) + content, err = pass.ReadFile(filename) if err != nil { return nil, err } @@ -50,40 +47,37 @@ func run(pass *analysis.Pass) (interface{}, error) { return content, nil } - // Pass 1: find functions annotated with an "inlineme" - // comment, and export a fact for each one. + // Pass 1: find functions annotated with a "//go:fix inline" + // comment (the syntax proposed by #32816), + // and export a fact for each one. inlinable := make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact) for _, file := range pass.Files { for _, decl := range file.Decls { - if decl, ok := decl.(*ast.FuncDecl); ok { - // TODO(adonovan): this is just a placeholder. - // Use the precise go:fix syntax in the proposal. - // Beware that //go: comments are treated specially - // by (*ast.CommentGroup).Text(). - // TODO(adonovan): alternatively, consider using - // the universal annotation mechanism sketched in - // https://go.dev/cl/489835 (which doesn't yet have - // a proper proposal). - if strings.Contains(decl.Doc.Text(), "inlineme") { - content, err := readFile(file) - if err != nil { - pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err) - continue - } - callee, err := inline.AnalyzeCallee(discard, pass.Fset, pass.Pkg, pass.TypesInfo, decl, content) - if err != nil { - pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err) - continue - } - fn := pass.TypesInfo.Defs[decl.Name].(*types.Func) - pass.ExportObjectFact(fn, &inlineMeFact{callee}) - inlinable[fn] = callee + if decl, ok := decl.(*ast.FuncDecl); ok && + slices.ContainsFunc(directives(decl.Doc), func(d *directive) bool { + return d.Tool == "go" && d.Name == "fix" && d.Args == "inline" + }) { + + content, err := readFile(decl) + if err != nil { + pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err) + continue + } + callee, err := inline.AnalyzeCallee(discard, pass.Fset, pass.Pkg, pass.TypesInfo, decl, content) + if err != nil { + pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err) + continue } + fn := pass.TypesInfo.Defs[decl.Name].(*types.Func) + pass.ExportObjectFact(fn, &goFixInlineFact{callee}) + inlinable[fn] = callee } } } // Pass 2. Inline each static call to an inlinable function. + // + // TODO(adonovan): handle multiple diffs that each add the same import. inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.File)(nil), @@ -100,7 +94,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // Inlinable? callee, ok := inlinable[fn] if !ok { - var fact inlineMeFact + var fact goFixInlineFact if pass.ImportObjectFact(fn, &fact) { callee = fact.Callee inlinable[fn] = callee @@ -129,6 +123,16 @@ func run(pass *analysis.Pass) (interface{}, error) { pass.Reportf(call.Lparen, "%v", err) return } + if res.Literalized { + // Users are not fond of inlinings that literalize + // f(x) to func() { ... }(), so avoid them. + // + // (Unfortunately the inliner is very timid, + // and often literalizes when it cannot prove that + // reducing the call is safe; the user of this tool + // has no indication of what the problem is.) + return + } got := res.Content // Suggest the "fix". @@ -156,9 +160,11 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } -type inlineMeFact struct{ Callee *inline.Callee } +// A goFixInlineFact is exported for each function marked "//go:fix inline". +// It holds information about the callee to support inlining. +type goFixInlineFact struct{ Callee *inline.Callee } -func (f *inlineMeFact) String() string { return "inlineme " + f.Callee.String() } -func (*inlineMeFact) AFact() {} +func (f *goFixInlineFact) String() string { return "goFixInline " + f.Callee.String() } +func (*goFixInlineFact) AFact() {} func discard(string, ...any) {} diff --git a/internal/refactor/inline/analyzer/directive.go b/internal/refactor/inline/analyzer/directive.go new file mode 100644 index 00000000000..f4426c5ffa8 --- /dev/null +++ b/internal/refactor/inline/analyzer/directive.go @@ -0,0 +1,90 @@ +// 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 analyzer + +import ( + "go/ast" + "go/token" + "strings" +) + +// -- plundered from the future (CL 605517, issue #68021) -- + +// TODO(adonovan): replace with ast.Directive after go1.24 (#68021). + +// A directive is a comment line with special meaning to the Go +// toolchain or another tool. It has the form: +// +// //tool:name args +// +// The "tool:" portion is missing for the three directives named +// line, extern, and export. +// +// See https://go.dev/doc/comment#Syntax for details of Go comment +// syntax and https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives +// for details of directives used by the Go compiler. +type directive struct { + Pos token.Pos // of preceding "//" + Tool string + Name string + Args string // may contain internal spaces +} + +// directives returns the directives within the comment. +func directives(g *ast.CommentGroup) (res []*directive) { + if g != nil { + // Avoid (*ast.CommentGroup).Text() as it swallows directives. + for _, c := range g.List { + if len(c.Text) > 2 && + c.Text[1] == '/' && + c.Text[2] != ' ' && + isDirective(c.Text[2:]) { + + tool, nameargs, ok := strings.Cut(c.Text[2:], ":") + if !ok { + // Must be one of {line,extern,export}. + tool, nameargs = "", tool + } + name, args, _ := strings.Cut(nameargs, " ") // tab?? + res = append(res, &directive{ + Pos: c.Slash, + Tool: tool, + Name: name, + Args: strings.TrimSpace(args), + }) + } + } + } + return +} + +// isDirective reports whether c is a comment directive. +// This code is also in go/printer. +func isDirective(c string) bool { + // "//line " is a line directive. + // "//extern " is for gccgo. + // "//export " is for cgo. + // (The // has been removed.) + if strings.HasPrefix(c, "line ") || strings.HasPrefix(c, "extern ") || strings.HasPrefix(c, "export ") { + return true + } + + // "//[a-z0-9]+:[a-z0-9]" + // (The // has been removed.) + colon := strings.Index(c, ":") + if colon <= 0 || colon+1 >= len(c) { + return false + } + for i := 0; i <= colon+1; i++ { + if i == colon { + continue + } + b := c[i] + if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') { + return false + } + } + return true +} diff --git a/internal/refactor/inline/analyzer/testdata/src/a/a.go b/internal/refactor/inline/analyzer/testdata/src/a/a.go index 294278670f2..6e159a36894 100644 --- a/internal/refactor/inline/analyzer/testdata/src/a/a.go +++ b/internal/refactor/inline/analyzer/testdata/src/a/a.go @@ -8,10 +8,10 @@ func f() { type T struct{} -// inlineme -func One() int { return one } // want One:`inlineme a.One` +//go:fix inline +func One() int { return one } // want One:`goFixInline a.One` const one = 1 -// inlineme -func (T) Two() int { return 2 } // want Two:`inlineme \(a.T\).Two` +//go:fix inline +func (T) Two() int { return 2 } // want Two:`goFixInline \(a.T\).Two` diff --git a/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden b/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden index 1a214fc9148..ea94f3b0175 100644 --- a/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden +++ b/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden @@ -8,10 +8,10 @@ func f() { type T struct{} -// inlineme -func One() int { return one } // want One:`inlineme a.One` +//go:fix inline +func One() int { return one } // want One:`goFixInline a.One` const one = 1 -// inlineme -func (T) Two() int { return 2 } // want Two:`inlineme \(a.T\).Two` +//go:fix inline +func (T) Two() int { return 2 } // want Two:`goFixInline \(a.T\).Two` diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go index ab1cbcb0070..b4ec43d551c 100644 --- a/internal/refactor/inline/callee.go +++ b/internal/refactor/inline/callee.go @@ -769,7 +769,7 @@ func isSelectionOperand(stack []ast.Node) bool { // use of b. type shadowMap map[string]int -// addShadows returns the [shadowMap] augmented by the set of names +// add returns the [shadowMap] augmented by the set of names // locally shadowed at the location of the reference in the callee // (identified by the stack). The name of the reference itself is // excluded. diff --git a/internal/refactor/inline/util.go b/internal/refactor/inline/util.go index 0ed33ba7353..591dc4265c0 100644 --- a/internal/refactor/inline/util.go +++ b/internal/refactor/inline/util.go @@ -22,9 +22,6 @@ func is[T any](x any) bool { return ok } -// TODO(adonovan): use go1.21's slices.Clone. -func clone[T any](slice []T) []T { return append([]T{}, slice...) } - // TODO(adonovan): use go1.21's slices.Index. func index[T comparable](slice []T, x T) int { for i, elem := range slice { diff --git a/internal/stdlib/manifest.go b/internal/stdlib/manifest.go index cdaac9ab34d..9f0b871ff6b 100644 --- a/internal/stdlib/manifest.go +++ b/internal/stdlib/manifest.go @@ -268,6 +268,8 @@ var PackageSymbols = map[string][]Symbol{ {"ErrTooLarge", Var, 0}, {"Fields", Func, 0}, {"FieldsFunc", Func, 0}, + {"FieldsFuncSeq", Func, 24}, + {"FieldsSeq", Func, 24}, {"HasPrefix", Func, 0}, {"HasSuffix", Func, 0}, {"Index", Func, 0}, @@ -280,6 +282,7 @@ var PackageSymbols = map[string][]Symbol{ {"LastIndexAny", Func, 0}, {"LastIndexByte", Func, 5}, {"LastIndexFunc", Func, 0}, + {"Lines", Func, 24}, {"Map", Func, 0}, {"MinRead", Const, 0}, {"NewBuffer", Func, 0}, @@ -293,7 +296,9 @@ var PackageSymbols = map[string][]Symbol{ {"Split", Func, 0}, {"SplitAfter", Func, 0}, {"SplitAfterN", Func, 0}, + {"SplitAfterSeq", Func, 24}, {"SplitN", Func, 0}, + {"SplitSeq", Func, 24}, {"Title", Func, 0}, {"ToLower", Func, 0}, {"ToLowerSpecial", Func, 0}, @@ -535,6 +540,7 @@ var PackageSymbols = map[string][]Symbol{ {"NewCTR", Func, 0}, {"NewGCM", Func, 2}, {"NewGCMWithNonceSize", Func, 5}, + {"NewGCMWithRandomNonce", Func, 24}, {"NewGCMWithTagSize", Func, 11}, {"NewOFB", Func, 0}, {"Stream", Type, 0}, @@ -673,6 +679,14 @@ var PackageSymbols = map[string][]Symbol{ {"Unmarshal", Func, 0}, {"UnmarshalCompressed", Func, 15}, }, + "crypto/fips140": { + {"Enabled", Func, 24}, + }, + "crypto/hkdf": { + {"Expand", Func, 24}, + {"Extract", Func, 24}, + {"Key", Func, 24}, + }, "crypto/hmac": { {"Equal", Func, 1}, {"New", Func, 0}, @@ -683,11 +697,43 @@ var PackageSymbols = map[string][]Symbol{ {"Size", Const, 0}, {"Sum", Func, 2}, }, + "crypto/mlkem": { + {"(*DecapsulationKey1024).Bytes", Method, 24}, + {"(*DecapsulationKey1024).Decapsulate", Method, 24}, + {"(*DecapsulationKey1024).EncapsulationKey", Method, 24}, + {"(*DecapsulationKey768).Bytes", Method, 24}, + {"(*DecapsulationKey768).Decapsulate", Method, 24}, + {"(*DecapsulationKey768).EncapsulationKey", Method, 24}, + {"(*EncapsulationKey1024).Bytes", Method, 24}, + {"(*EncapsulationKey1024).Encapsulate", Method, 24}, + {"(*EncapsulationKey768).Bytes", Method, 24}, + {"(*EncapsulationKey768).Encapsulate", Method, 24}, + {"CiphertextSize1024", Const, 24}, + {"CiphertextSize768", Const, 24}, + {"DecapsulationKey1024", Type, 24}, + {"DecapsulationKey768", Type, 24}, + {"EncapsulationKey1024", Type, 24}, + {"EncapsulationKey768", Type, 24}, + {"EncapsulationKeySize1024", Const, 24}, + {"EncapsulationKeySize768", Const, 24}, + {"GenerateKey1024", Func, 24}, + {"GenerateKey768", Func, 24}, + {"NewDecapsulationKey1024", Func, 24}, + {"NewDecapsulationKey768", Func, 24}, + {"NewEncapsulationKey1024", Func, 24}, + {"NewEncapsulationKey768", Func, 24}, + {"SeedSize", Const, 24}, + {"SharedKeySize", Const, 24}, + }, + "crypto/pbkdf2": { + {"Key", Func, 24}, + }, "crypto/rand": { {"Int", Func, 0}, {"Prime", Func, 0}, {"Read", Func, 0}, {"Reader", Var, 0}, + {"Text", Func, 24}, }, "crypto/rc4": { {"(*Cipher).Reset", Method, 0}, @@ -766,6 +812,39 @@ var PackageSymbols = map[string][]Symbol{ {"Sum224", Func, 2}, {"Sum256", Func, 2}, }, + "crypto/sha3": { + {"(*SHA3).AppendBinary", Method, 24}, + {"(*SHA3).BlockSize", Method, 24}, + {"(*SHA3).MarshalBinary", Method, 24}, + {"(*SHA3).Reset", Method, 24}, + {"(*SHA3).Size", Method, 24}, + {"(*SHA3).Sum", Method, 24}, + {"(*SHA3).UnmarshalBinary", Method, 24}, + {"(*SHA3).Write", Method, 24}, + {"(*SHAKE).AppendBinary", Method, 24}, + {"(*SHAKE).BlockSize", Method, 24}, + {"(*SHAKE).MarshalBinary", Method, 24}, + {"(*SHAKE).Read", Method, 24}, + {"(*SHAKE).Reset", Method, 24}, + {"(*SHAKE).UnmarshalBinary", Method, 24}, + {"(*SHAKE).Write", Method, 24}, + {"New224", Func, 24}, + {"New256", Func, 24}, + {"New384", Func, 24}, + {"New512", Func, 24}, + {"NewCSHAKE128", Func, 24}, + {"NewCSHAKE256", Func, 24}, + {"NewSHAKE128", Func, 24}, + {"NewSHAKE256", Func, 24}, + {"SHA3", Type, 24}, + {"SHAKE", Type, 24}, + {"Sum224", Func, 24}, + {"Sum256", Func, 24}, + {"Sum384", Func, 24}, + {"Sum512", Func, 24}, + {"SumSHAKE128", Func, 24}, + {"SumSHAKE256", Func, 24}, + }, "crypto/sha512": { {"BlockSize", Const, 0}, {"New", Func, 0}, @@ -788,6 +867,7 @@ var PackageSymbols = map[string][]Symbol{ {"ConstantTimeEq", Func, 0}, {"ConstantTimeLessOrEq", Func, 2}, {"ConstantTimeSelect", Func, 0}, + {"WithDataIndependentTiming", Func, 24}, {"XORBytes", Func, 20}, }, "crypto/tls": { @@ -864,6 +944,7 @@ var PackageSymbols = map[string][]Symbol{ {"ClientHelloInfo", Type, 4}, {"ClientHelloInfo.CipherSuites", Field, 4}, {"ClientHelloInfo.Conn", Field, 8}, + {"ClientHelloInfo.Extensions", Field, 24}, {"ClientHelloInfo.ServerName", Field, 4}, {"ClientHelloInfo.SignatureSchemes", Field, 8}, {"ClientHelloInfo.SupportedCurves", Field, 4}, @@ -881,6 +962,7 @@ var PackageSymbols = map[string][]Symbol{ {"Config.CurvePreferences", Field, 3}, {"Config.DynamicRecordSizingDisabled", Field, 7}, {"Config.EncryptedClientHelloConfigList", Field, 23}, + {"Config.EncryptedClientHelloKeys", Field, 24}, {"Config.EncryptedClientHelloRejectionVerify", Field, 23}, {"Config.GetCertificate", Field, 4}, {"Config.GetClientCertificate", Field, 8}, @@ -934,6 +1016,10 @@ var PackageSymbols = map[string][]Symbol{ {"ECHRejectionError", Type, 23}, {"ECHRejectionError.RetryConfigList", Field, 23}, {"Ed25519", Const, 13}, + {"EncryptedClientHelloKey", Type, 24}, + {"EncryptedClientHelloKey.Config", Field, 24}, + {"EncryptedClientHelloKey.PrivateKey", Field, 24}, + {"EncryptedClientHelloKey.SendAsRetry", Field, 24}, {"InsecureCipherSuites", Func, 14}, {"Listen", Func, 0}, {"LoadX509KeyPair", Func, 0}, @@ -1032,6 +1118,7 @@ var PackageSymbols = map[string][]Symbol{ {"VersionTLS12", Const, 2}, {"VersionTLS13", Const, 12}, {"X25519", Const, 8}, + {"X25519MLKEM768", Const, 24}, {"X509KeyPair", Func, 0}, }, "crypto/x509": { @@ -1056,6 +1143,8 @@ var PackageSymbols = map[string][]Symbol{ {"(ConstraintViolationError).Error", Method, 0}, {"(HostnameError).Error", Method, 0}, {"(InsecureAlgorithmError).Error", Method, 6}, + {"(OID).AppendBinary", Method, 24}, + {"(OID).AppendText", Method, 24}, {"(OID).Equal", Method, 22}, {"(OID).EqualASN1OID", Method, 22}, {"(OID).MarshalBinary", Method, 23}, @@ -1084,6 +1173,10 @@ var PackageSymbols = map[string][]Symbol{ {"Certificate.Extensions", Field, 2}, {"Certificate.ExtraExtensions", Field, 2}, {"Certificate.IPAddresses", Field, 1}, + {"Certificate.InhibitAnyPolicy", Field, 24}, + {"Certificate.InhibitAnyPolicyZero", Field, 24}, + {"Certificate.InhibitPolicyMapping", Field, 24}, + {"Certificate.InhibitPolicyMappingZero", Field, 24}, {"Certificate.IsCA", Field, 0}, {"Certificate.Issuer", Field, 0}, {"Certificate.IssuingCertificateURL", Field, 2}, @@ -1100,6 +1193,7 @@ var PackageSymbols = map[string][]Symbol{ {"Certificate.PermittedURIDomains", Field, 10}, {"Certificate.Policies", Field, 22}, {"Certificate.PolicyIdentifiers", Field, 0}, + {"Certificate.PolicyMappings", Field, 24}, {"Certificate.PublicKey", Field, 0}, {"Certificate.PublicKeyAlgorithm", Field, 0}, {"Certificate.Raw", Field, 0}, @@ -1107,6 +1201,8 @@ var PackageSymbols = map[string][]Symbol{ {"Certificate.RawSubject", Field, 0}, {"Certificate.RawSubjectPublicKeyInfo", Field, 0}, {"Certificate.RawTBSCertificate", Field, 0}, + {"Certificate.RequireExplicitPolicy", Field, 24}, + {"Certificate.RequireExplicitPolicyZero", Field, 24}, {"Certificate.SerialNumber", Field, 0}, {"Certificate.Signature", Field, 0}, {"Certificate.SignatureAlgorithm", Field, 0}, @@ -1198,6 +1294,7 @@ var PackageSymbols = map[string][]Symbol{ {"NameConstraintsWithoutSANs", Const, 10}, {"NameMismatch", Const, 8}, {"NewCertPool", Func, 0}, + {"NoValidChains", Const, 24}, {"NotAuthorizedToSign", Const, 0}, {"OID", Type, 22}, {"OIDFromInts", Func, 22}, @@ -1219,6 +1316,9 @@ var PackageSymbols = map[string][]Symbol{ {"ParsePKCS8PrivateKey", Func, 0}, {"ParsePKIXPublicKey", Func, 0}, {"ParseRevocationList", Func, 19}, + {"PolicyMapping", Type, 24}, + {"PolicyMapping.IssuerDomainPolicy", Field, 24}, + {"PolicyMapping.SubjectDomainPolicy", Field, 24}, {"PublicKeyAlgorithm", Type, 0}, {"PureEd25519", Const, 13}, {"RSA", Const, 0}, @@ -1265,6 +1365,7 @@ var PackageSymbols = map[string][]Symbol{ {"UnknownPublicKeyAlgorithm", Const, 0}, {"UnknownSignatureAlgorithm", Const, 0}, {"VerifyOptions", Type, 0}, + {"VerifyOptions.CertificatePolicies", Field, 24}, {"VerifyOptions.CurrentTime", Field, 0}, {"VerifyOptions.DNSName", Field, 0}, {"VerifyOptions.Intermediates", Field, 0}, @@ -1975,6 +2076,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*File).DynString", Method, 1}, {"(*File).DynValue", Method, 21}, {"(*File).DynamicSymbols", Method, 4}, + {"(*File).DynamicVersionNeeds", Method, 24}, + {"(*File).DynamicVersions", Method, 24}, {"(*File).ImportedLibraries", Method, 0}, {"(*File).ImportedSymbols", Method, 0}, {"(*File).Section", Method, 0}, @@ -2240,6 +2343,19 @@ var PackageSymbols = map[string][]Symbol{ {"DynFlag", Type, 0}, {"DynFlag1", Type, 21}, {"DynTag", Type, 0}, + {"DynamicVersion", Type, 24}, + {"DynamicVersion.Deps", Field, 24}, + {"DynamicVersion.Flags", Field, 24}, + {"DynamicVersion.Index", Field, 24}, + {"DynamicVersion.Name", Field, 24}, + {"DynamicVersionDep", Type, 24}, + {"DynamicVersionDep.Dep", Field, 24}, + {"DynamicVersionDep.Flags", Field, 24}, + {"DynamicVersionDep.Index", Field, 24}, + {"DynamicVersionFlag", Type, 24}, + {"DynamicVersionNeed", Type, 24}, + {"DynamicVersionNeed.Name", Field, 24}, + {"DynamicVersionNeed.Needs", Field, 24}, {"EI_ABIVERSION", Const, 0}, {"EI_CLASS", Const, 0}, {"EI_DATA", Const, 0}, @@ -3726,8 +3842,19 @@ var PackageSymbols = map[string][]Symbol{ {"Symbol.Size", Field, 0}, {"Symbol.Value", Field, 0}, {"Symbol.Version", Field, 13}, + {"Symbol.VersionIndex", Field, 24}, + {"Symbol.VersionScope", Field, 24}, + {"SymbolVersionScope", Type, 24}, {"Type", Type, 0}, + {"VER_FLG_BASE", Const, 24}, + {"VER_FLG_INFO", Const, 24}, + {"VER_FLG_WEAK", Const, 24}, {"Version", Type, 0}, + {"VersionScopeGlobal", Const, 24}, + {"VersionScopeHidden", Const, 24}, + {"VersionScopeLocal", Const, 24}, + {"VersionScopeNone", Const, 24}, + {"VersionScopeSpecific", Const, 24}, }, "debug/gosym": { {"(*DecodingError).Error", Method, 0}, @@ -4453,8 +4580,10 @@ var PackageSymbols = map[string][]Symbol{ {"FS", Type, 16}, }, "encoding": { + {"BinaryAppender", Type, 24}, {"BinaryMarshaler", Type, 2}, {"BinaryUnmarshaler", Type, 2}, + {"TextAppender", Type, 24}, {"TextMarshaler", Type, 2}, {"TextUnmarshaler", Type, 2}, }, @@ -5984,13 +6113,16 @@ var PackageSymbols = map[string][]Symbol{ {"(*Interface).Complete", Method, 5}, {"(*Interface).Embedded", Method, 5}, {"(*Interface).EmbeddedType", Method, 11}, + {"(*Interface).EmbeddedTypes", Method, 24}, {"(*Interface).Empty", Method, 5}, {"(*Interface).ExplicitMethod", Method, 5}, + {"(*Interface).ExplicitMethods", Method, 24}, {"(*Interface).IsComparable", Method, 18}, {"(*Interface).IsImplicit", Method, 18}, {"(*Interface).IsMethodSet", Method, 18}, {"(*Interface).MarkImplicit", Method, 18}, {"(*Interface).Method", Method, 5}, + {"(*Interface).Methods", Method, 24}, {"(*Interface).NumEmbeddeds", Method, 5}, {"(*Interface).NumExplicitMethods", Method, 5}, {"(*Interface).NumMethods", Method, 5}, @@ -6011,9 +6143,11 @@ var PackageSymbols = map[string][]Symbol{ {"(*MethodSet).At", Method, 5}, {"(*MethodSet).Len", Method, 5}, {"(*MethodSet).Lookup", Method, 5}, + {"(*MethodSet).Methods", Method, 24}, {"(*MethodSet).String", Method, 5}, {"(*Named).AddMethod", Method, 5}, {"(*Named).Method", Method, 5}, + {"(*Named).Methods", Method, 24}, {"(*Named).NumMethods", Method, 5}, {"(*Named).Obj", Method, 5}, {"(*Named).Origin", Method, 18}, @@ -6054,6 +6188,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Pointer).String", Method, 5}, {"(*Pointer).Underlying", Method, 5}, {"(*Scope).Child", Method, 5}, + {"(*Scope).Children", Method, 24}, {"(*Scope).Contains", Method, 5}, {"(*Scope).End", Method, 5}, {"(*Scope).Innermost", Method, 5}, @@ -6089,6 +6224,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*StdSizes).Offsetsof", Method, 5}, {"(*StdSizes).Sizeof", Method, 5}, {"(*Struct).Field", Method, 5}, + {"(*Struct).Fields", Method, 24}, {"(*Struct).NumFields", Method, 5}, {"(*Struct).String", Method, 5}, {"(*Struct).Tag", Method, 5}, @@ -6100,8 +6236,10 @@ var PackageSymbols = map[string][]Symbol{ {"(*Tuple).Len", Method, 5}, {"(*Tuple).String", Method, 5}, {"(*Tuple).Underlying", Method, 5}, + {"(*Tuple).Variables", Method, 24}, {"(*TypeList).At", Method, 18}, {"(*TypeList).Len", Method, 18}, + {"(*TypeList).Types", Method, 24}, {"(*TypeName).Exported", Method, 5}, {"(*TypeName).Id", Method, 5}, {"(*TypeName).IsAlias", Method, 9}, @@ -6119,9 +6257,11 @@ var PackageSymbols = map[string][]Symbol{ {"(*TypeParam).Underlying", Method, 18}, {"(*TypeParamList).At", Method, 18}, {"(*TypeParamList).Len", Method, 18}, + {"(*TypeParamList).TypeParams", Method, 24}, {"(*Union).Len", Method, 18}, {"(*Union).String", Method, 18}, {"(*Union).Term", Method, 18}, + {"(*Union).Terms", Method, 24}, {"(*Union).Underlying", Method, 18}, {"(*Var).Anonymous", Method, 5}, {"(*Var).Embedded", Method, 11}, @@ -6392,10 +6532,12 @@ var PackageSymbols = map[string][]Symbol{ {"(*Hash).WriteByte", Method, 14}, {"(*Hash).WriteString", Method, 14}, {"Bytes", Func, 19}, + {"Comparable", Func, 24}, {"Hash", Type, 14}, {"MakeSeed", Func, 14}, {"Seed", Type, 14}, {"String", Func, 19}, + {"WriteComparable", Func, 24}, }, "html": { {"EscapeString", Func, 0}, @@ -7082,6 +7224,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*JSONHandler).WithGroup", Method, 21}, {"(*Level).UnmarshalJSON", Method, 21}, {"(*Level).UnmarshalText", Method, 21}, + {"(*LevelVar).AppendText", Method, 24}, {"(*LevelVar).Level", Method, 21}, {"(*LevelVar).MarshalText", Method, 21}, {"(*LevelVar).Set", Method, 21}, @@ -7110,6 +7253,7 @@ var PackageSymbols = map[string][]Symbol{ {"(Attr).Equal", Method, 21}, {"(Attr).String", Method, 21}, {"(Kind).String", Method, 21}, + {"(Level).AppendText", Method, 24}, {"(Level).Level", Method, 21}, {"(Level).MarshalJSON", Method, 21}, {"(Level).MarshalText", Method, 21}, @@ -7140,6 +7284,7 @@ var PackageSymbols = map[string][]Symbol{ {"Debug", Func, 21}, {"DebugContext", Func, 21}, {"Default", Func, 21}, + {"DiscardHandler", Var, 24}, {"Duration", Func, 21}, {"DurationValue", Func, 21}, {"Error", Func, 21}, @@ -7375,6 +7520,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Float).Acc", Method, 5}, {"(*Float).Add", Method, 5}, {"(*Float).Append", Method, 5}, + {"(*Float).AppendText", Method, 24}, {"(*Float).Cmp", Method, 5}, {"(*Float).Copy", Method, 5}, {"(*Float).Float32", Method, 5}, @@ -7421,6 +7567,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Int).And", Method, 0}, {"(*Int).AndNot", Method, 0}, {"(*Int).Append", Method, 6}, + {"(*Int).AppendText", Method, 24}, {"(*Int).Binomial", Method, 0}, {"(*Int).Bit", Method, 0}, {"(*Int).BitLen", Method, 0}, @@ -7477,6 +7624,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Int).Xor", Method, 0}, {"(*Rat).Abs", Method, 0}, {"(*Rat).Add", Method, 0}, + {"(*Rat).AppendText", Method, 24}, {"(*Rat).Cmp", Method, 0}, {"(*Rat).Denom", Method, 0}, {"(*Rat).Float32", Method, 4}, @@ -7659,11 +7807,13 @@ var PackageSymbols = map[string][]Symbol{ {"Zipf", Type, 0}, }, "math/rand/v2": { + {"(*ChaCha8).AppendBinary", Method, 24}, {"(*ChaCha8).MarshalBinary", Method, 22}, {"(*ChaCha8).Read", Method, 23}, {"(*ChaCha8).Seed", Method, 22}, {"(*ChaCha8).Uint64", Method, 22}, {"(*ChaCha8).UnmarshalBinary", Method, 22}, + {"(*PCG).AppendBinary", Method, 24}, {"(*PCG).MarshalBinary", Method, 22}, {"(*PCG).Seed", Method, 22}, {"(*PCG).Uint64", Method, 22}, @@ -7931,6 +8081,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*UnixListener).SyscallConn", Method, 10}, {"(Flags).String", Method, 0}, {"(HardwareAddr).String", Method, 0}, + {"(IP).AppendText", Method, 24}, {"(IP).DefaultMask", Method, 0}, {"(IP).Equal", Method, 0}, {"(IP).IsGlobalUnicast", Method, 0}, @@ -8131,6 +8282,9 @@ var PackageSymbols = map[string][]Symbol{ {"(*MaxBytesError).Error", Method, 19}, {"(*ProtocolError).Error", Method, 0}, {"(*ProtocolError).Is", Method, 21}, + {"(*Protocols).SetHTTP1", Method, 24}, + {"(*Protocols).SetHTTP2", Method, 24}, + {"(*Protocols).SetUnencryptedHTTP2", Method, 24}, {"(*Request).AddCookie", Method, 0}, {"(*Request).BasicAuth", Method, 4}, {"(*Request).Clone", Method, 13}, @@ -8190,6 +8344,10 @@ var PackageSymbols = map[string][]Symbol{ {"(Header).Values", Method, 14}, {"(Header).Write", Method, 0}, {"(Header).WriteSubset", Method, 0}, + {"(Protocols).HTTP1", Method, 24}, + {"(Protocols).HTTP2", Method, 24}, + {"(Protocols).String", Method, 24}, + {"(Protocols).UnencryptedHTTP2", Method, 24}, {"AllowQuerySemicolons", Func, 17}, {"CanonicalHeaderKey", Func, 0}, {"Client", Type, 0}, @@ -8252,6 +8410,18 @@ var PackageSymbols = map[string][]Symbol{ {"FileSystem", Type, 0}, {"Flusher", Type, 0}, {"Get", Func, 0}, + {"HTTP2Config", Type, 24}, + {"HTTP2Config.CountError", Field, 24}, + {"HTTP2Config.MaxConcurrentStreams", Field, 24}, + {"HTTP2Config.MaxDecoderHeaderTableSize", Field, 24}, + {"HTTP2Config.MaxEncoderHeaderTableSize", Field, 24}, + {"HTTP2Config.MaxReadFrameSize", Field, 24}, + {"HTTP2Config.MaxReceiveBufferPerConnection", Field, 24}, + {"HTTP2Config.MaxReceiveBufferPerStream", Field, 24}, + {"HTTP2Config.PermitProhibitedCipherSuites", Field, 24}, + {"HTTP2Config.PingTimeout", Field, 24}, + {"HTTP2Config.SendPingTimeout", Field, 24}, + {"HTTP2Config.WriteByteTimeout", Field, 24}, {"Handle", Func, 0}, {"HandleFunc", Func, 0}, {"Handler", Type, 0}, @@ -8292,6 +8462,7 @@ var PackageSymbols = map[string][]Symbol{ {"PostForm", Func, 0}, {"ProtocolError", Type, 0}, {"ProtocolError.ErrorString", Field, 0}, + {"Protocols", Type, 24}, {"ProxyFromEnvironment", Func, 0}, {"ProxyURL", Func, 0}, {"PushOptions", Type, 8}, @@ -8361,9 +8532,11 @@ var PackageSymbols = map[string][]Symbol{ {"Server.ConnState", Field, 3}, {"Server.DisableGeneralOptionsHandler", Field, 20}, {"Server.ErrorLog", Field, 3}, + {"Server.HTTP2", Field, 24}, {"Server.Handler", Field, 0}, {"Server.IdleTimeout", Field, 8}, {"Server.MaxHeaderBytes", Field, 0}, + {"Server.Protocols", Field, 24}, {"Server.ReadHeaderTimeout", Field, 8}, {"Server.ReadTimeout", Field, 0}, {"Server.TLSConfig", Field, 0}, @@ -8453,12 +8626,14 @@ var PackageSymbols = map[string][]Symbol{ {"Transport.ExpectContinueTimeout", Field, 6}, {"Transport.ForceAttemptHTTP2", Field, 13}, {"Transport.GetProxyConnectHeader", Field, 16}, + {"Transport.HTTP2", Field, 24}, {"Transport.IdleConnTimeout", Field, 7}, {"Transport.MaxConnsPerHost", Field, 11}, {"Transport.MaxIdleConns", Field, 7}, {"Transport.MaxIdleConnsPerHost", Field, 0}, {"Transport.MaxResponseHeaderBytes", Field, 7}, {"Transport.OnProxyConnectResponse", Field, 20}, + {"Transport.Protocols", Field, 24}, {"Transport.Proxy", Field, 0}, {"Transport.ProxyConnectHeader", Field, 8}, {"Transport.ReadBufferSize", Field, 13}, @@ -8646,6 +8821,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*AddrPort).UnmarshalText", Method, 18}, {"(*Prefix).UnmarshalBinary", Method, 18}, {"(*Prefix).UnmarshalText", Method, 18}, + {"(Addr).AppendBinary", Method, 24}, + {"(Addr).AppendText", Method, 24}, {"(Addr).AppendTo", Method, 18}, {"(Addr).As16", Method, 18}, {"(Addr).As4", Method, 18}, @@ -8676,6 +8853,8 @@ var PackageSymbols = map[string][]Symbol{ {"(Addr).WithZone", Method, 18}, {"(Addr).Zone", Method, 18}, {"(AddrPort).Addr", Method, 18}, + {"(AddrPort).AppendBinary", Method, 24}, + {"(AddrPort).AppendText", Method, 24}, {"(AddrPort).AppendTo", Method, 18}, {"(AddrPort).Compare", Method, 22}, {"(AddrPort).IsValid", Method, 18}, @@ -8684,6 +8863,8 @@ var PackageSymbols = map[string][]Symbol{ {"(AddrPort).Port", Method, 18}, {"(AddrPort).String", Method, 18}, {"(Prefix).Addr", Method, 18}, + {"(Prefix).AppendBinary", Method, 24}, + {"(Prefix).AppendText", Method, 24}, {"(Prefix).AppendTo", Method, 18}, {"(Prefix).Bits", Method, 18}, {"(Prefix).Contains", Method, 18}, @@ -8868,6 +9049,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Error).Temporary", Method, 6}, {"(*Error).Timeout", Method, 6}, {"(*Error).Unwrap", Method, 13}, + {"(*URL).AppendBinary", Method, 24}, {"(*URL).EscapedFragment", Method, 15}, {"(*URL).EscapedPath", Method, 5}, {"(*URL).Hostname", Method, 8}, @@ -8967,6 +9149,17 @@ var PackageSymbols = map[string][]Symbol{ {"(*ProcessState).SysUsage", Method, 0}, {"(*ProcessState).SystemTime", Method, 0}, {"(*ProcessState).UserTime", Method, 0}, + {"(*Root).Close", Method, 24}, + {"(*Root).Create", Method, 24}, + {"(*Root).FS", Method, 24}, + {"(*Root).Lstat", Method, 24}, + {"(*Root).Mkdir", Method, 24}, + {"(*Root).Name", Method, 24}, + {"(*Root).Open", Method, 24}, + {"(*Root).OpenFile", Method, 24}, + {"(*Root).OpenRoot", Method, 24}, + {"(*Root).Remove", Method, 24}, + {"(*Root).Stat", Method, 24}, {"(*SyscallError).Error", Method, 0}, {"(*SyscallError).Timeout", Method, 10}, {"(*SyscallError).Unwrap", Method, 13}, @@ -9060,6 +9253,8 @@ var PackageSymbols = map[string][]Symbol{ {"O_WRONLY", Const, 0}, {"Open", Func, 0}, {"OpenFile", Func, 0}, + {"OpenInRoot", Func, 24}, + {"OpenRoot", Func, 24}, {"PathError", Type, 0}, {"PathError.Err", Field, 0}, {"PathError.Op", Field, 0}, @@ -9081,6 +9276,7 @@ var PackageSymbols = map[string][]Symbol{ {"Remove", Func, 0}, {"RemoveAll", Func, 0}, {"Rename", Func, 0}, + {"Root", Type, 24}, {"SEEK_CUR", Const, 0}, {"SEEK_END", Const, 0}, {"SEEK_SET", Const, 0}, @@ -9422,6 +9618,7 @@ var PackageSymbols = map[string][]Symbol{ {"Zero", Func, 0}, }, "regexp": { + {"(*Regexp).AppendText", Method, 24}, {"(*Regexp).Copy", Method, 6}, {"(*Regexp).Expand", Method, 0}, {"(*Regexp).ExpandString", Method, 0}, @@ -9602,6 +9799,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*StackRecord).Stack", Method, 0}, {"(*TypeAssertionError).Error", Method, 0}, {"(*TypeAssertionError).RuntimeError", Method, 0}, + {"(Cleanup).Stop", Method, 24}, + {"AddCleanup", Func, 24}, {"BlockProfile", Func, 1}, {"BlockProfileRecord", Type, 1}, {"BlockProfileRecord.Count", Field, 1}, @@ -9612,6 +9811,7 @@ var PackageSymbols = map[string][]Symbol{ {"Caller", Func, 0}, {"Callers", Func, 0}, {"CallersFrames", Func, 7}, + {"Cleanup", Type, 24}, {"Compiler", Const, 0}, {"Error", Type, 0}, {"Frame", Type, 7}, @@ -9974,6 +10174,8 @@ var PackageSymbols = map[string][]Symbol{ {"EqualFold", Func, 0}, {"Fields", Func, 0}, {"FieldsFunc", Func, 0}, + {"FieldsFuncSeq", Func, 24}, + {"FieldsSeq", Func, 24}, {"HasPrefix", Func, 0}, {"HasSuffix", Func, 0}, {"Index", Func, 0}, @@ -9986,6 +10188,7 @@ var PackageSymbols = map[string][]Symbol{ {"LastIndexAny", Func, 0}, {"LastIndexByte", Func, 5}, {"LastIndexFunc", Func, 0}, + {"Lines", Func, 24}, {"Map", Func, 0}, {"NewReader", Func, 0}, {"NewReplacer", Func, 0}, @@ -9997,7 +10200,9 @@ var PackageSymbols = map[string][]Symbol{ {"Split", Func, 0}, {"SplitAfter", Func, 0}, {"SplitAfterN", Func, 0}, + {"SplitAfterSeq", Func, 24}, {"SplitN", Func, 0}, + {"SplitSeq", Func, 24}, {"Title", Func, 0}, {"ToLower", Func, 0}, {"ToLowerSpecial", Func, 0}, @@ -16413,7 +16618,9 @@ var PackageSymbols = map[string][]Symbol{ {"ValueOf", Func, 0}, }, "testing": { + {"(*B).Chdir", Method, 24}, {"(*B).Cleanup", Method, 14}, + {"(*B).Context", Method, 24}, {"(*B).Elapsed", Method, 20}, {"(*B).Error", Method, 0}, {"(*B).Errorf", Method, 0}, @@ -16425,6 +16632,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*B).Helper", Method, 9}, {"(*B).Log", Method, 0}, {"(*B).Logf", Method, 0}, + {"(*B).Loop", Method, 24}, {"(*B).Name", Method, 8}, {"(*B).ReportAllocs", Method, 1}, {"(*B).ReportMetric", Method, 13}, @@ -16442,7 +16650,9 @@ var PackageSymbols = map[string][]Symbol{ {"(*B).StopTimer", Method, 0}, {"(*B).TempDir", Method, 15}, {"(*F).Add", Method, 18}, + {"(*F).Chdir", Method, 24}, {"(*F).Cleanup", Method, 18}, + {"(*F).Context", Method, 24}, {"(*F).Error", Method, 18}, {"(*F).Errorf", Method, 18}, {"(*F).Fail", Method, 18}, @@ -16463,7 +16673,9 @@ var PackageSymbols = map[string][]Symbol{ {"(*F).TempDir", Method, 18}, {"(*M).Run", Method, 4}, {"(*PB).Next", Method, 3}, + {"(*T).Chdir", Method, 24}, {"(*T).Cleanup", Method, 14}, + {"(*T).Context", Method, 24}, {"(*T).Deadline", Method, 15}, {"(*T).Error", Method, 0}, {"(*T).Errorf", Method, 0}, @@ -16954,7 +17166,9 @@ var PackageSymbols = map[string][]Symbol{ {"(Time).Add", Method, 0}, {"(Time).AddDate", Method, 0}, {"(Time).After", Method, 0}, + {"(Time).AppendBinary", Method, 24}, {"(Time).AppendFormat", Method, 5}, + {"(Time).AppendText", Method, 24}, {"(Time).Before", Method, 0}, {"(Time).Clock", Method, 0}, {"(Time).Compare", Method, 20}, @@ -17428,4 +17642,9 @@ var PackageSymbols = map[string][]Symbol{ {"String", Func, 0}, {"StringData", Func, 0}, }, + "weak": { + {"(Pointer).Value", Method, 24}, + {"Make", Func, 24}, + {"Pointer", Type, 24}, + }, } diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index 70c186b13b5..d217e28462c 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -364,7 +364,7 @@ func NeedsGo1Point(t testing.TB, x int) { } } -// SkipAfterGo1Point skips t if the ambient go command version in the PATH of +// SkipAfterGoCommand1Point skips t if the ambient go command version in the PATH of // the current process is newer than 1.x. // // SkipAfterGoCommand1Point memoizes the result of running the go command, so diff --git a/internal/typeparams/common.go b/internal/typeparams/common.go index 0b84acc5c7f..cdae2b8e818 100644 --- a/internal/typeparams/common.go +++ b/internal/typeparams/common.go @@ -66,75 +66,3 @@ func IsTypeParam(t types.Type) bool { _, ok := types.Unalias(t).(*types.TypeParam) return ok } - -// GenericAssignableTo is a generalization of types.AssignableTo that -// implements the following rule for uninstantiated generic types: -// -// If V and T are generic named types, then V is considered assignable to T if, -// for every possible instantiation of V[A_1, ..., A_N], the instantiation -// T[A_1, ..., A_N] is valid and V[A_1, ..., A_N] implements T[A_1, ..., A_N]. -// -// If T has structural constraints, they must be satisfied by V. -// -// For example, consider the following type declarations: -// -// type Interface[T any] interface { -// Accept(T) -// } -// -// type Container[T any] struct { -// Element T -// } -// -// func (c Container[T]) Accept(t T) { c.Element = t } -// -// 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 = 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. - - VN, Vnamed := V.(*types.Named) - TN, Tnamed := T.(*types.Named) - if !Vnamed || !Tnamed { - return types.AssignableTo(V, T) - } - - vtparams := VN.TypeParams() - ttparams := TN.TypeParams() - if vtparams.Len() == 0 || vtparams.Len() != ttparams.Len() || VN.TypeArgs().Len() != 0 || TN.TypeArgs().Len() != 0 { - return types.AssignableTo(V, T) - } - - // V and T have the same (non-zero) number of type params. Instantiate both - // with the type parameters of V. This must always succeed for V, and will - // succeed for T if and only if the type set of each type parameter of V is a - // subset of the type set of the corresponding type parameter of T, meaning - // that every instantiation of V corresponds to a valid instantiation of T. - - // Minor optimization: ensure we share a context across the two - // instantiations below. - if ctxt == nil { - ctxt = types.NewContext() - } - - var targs []types.Type - for i := 0; i < vtparams.Len(); i++ { - targs = append(targs, vtparams.At(i)) - } - - vinst, err := types.Instantiate(ctxt, V, targs, true) - if err != nil { - panic("type parameters should satisfy their own constraints") - } - - tinst, err := types.Instantiate(ctxt, T, targs, true) - if err != nil { - return false - } - - return types.AssignableTo(vinst, tinst) -} diff --git a/internal/typeparams/common_test.go b/internal/typeparams/common_test.go index 779a942d59e..3cbd741360c 100644 --- a/internal/typeparams/common_test.go +++ b/internal/typeparams/common_test.go @@ -204,74 +204,3 @@ func TestFuncOrigin60628(t *testing.T) { } } } - -func TestGenericAssignableTo(t *testing.T) { - tests := []struct { - src string - want bool - }{ - // The inciting issue: golang/go#50887. - {` - type T[P any] interface { - Accept(P) - } - - type V[Q any] struct { - Element Q - } - - func (c V[Q]) Accept(q Q) { c.Element = q } - `, true}, - - // Various permutations on constraints and signatures. - {`type T[P ~int] interface{ A(P) }; type V[Q int] int; func (V[Q]) A(Q) {}`, true}, - {`type T[P int] interface{ A(P) }; type V[Q ~int] int; func (V[Q]) A(Q) {}`, false}, - {`type T[P int|string] interface{ A(P) }; type V[Q int] int; func (V[Q]) A(Q) {}`, true}, - {`type T[P any] interface{ A(P) }; type V[Q any] int; func (V[Q]) A(Q, Q) {}`, false}, - {`type T[P any] interface{ int; A(P) }; type V[Q any] int; func (V[Q]) A(Q) {}`, false}, - - // Various structural restrictions on T. - {`type T[P any] interface{ ~int; A(P) }; type V[Q any] int; func (V[Q]) A(Q) {}`, true}, - {`type T[P any] interface{ ~int|string; A(P) }; type V[Q any] int; func (V[Q]) A(Q) {}`, true}, - {`type T[P any] interface{ int; A(P) }; type V[Q int] int; func (V[Q]) A(Q) {}`, false}, - - // Various recursive constraints. - {`type T[P ~struct{ f *P }] interface{ A(P) }; type V[Q ~struct{ f *Q }] int; func (V[Q]) A(Q) {}`, true}, - {`type T[P ~struct{ f *P }] interface{ A(P) }; type V[Q ~struct{ g *Q }] int; func (V[Q]) A(Q) {}`, false}, - {`type T[P ~*X, X any] interface{ A(P) X }; type V[Q ~*Y, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, true}, - {`type T[P ~*X, X any] interface{ A(P) X }; type V[Q ~**Y, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, false}, - {`type T[P, X any] interface{ A(P) X }; type V[Q ~*Y, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, true}, - {`type T[P ~*X, X any] interface{ A(P) X }; type V[Q, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, false}, - {`type T[P, X any] interface{ A(P) X }; type V[Q, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, true}, - - // In this test case, we reverse the type parameters in the signature of V.A - {`type T[P, X any] interface{ A(P) X }; type V[Q, Y any] int; func (V[Q, Y]) A(Y) (y Q) { return }`, false}, - // It would be nice to return true here: V can only be instantiated with - // [int, int], so the identity of the type parameters should not matter. - {`type T[P, X any] interface{ A(P) X }; type V[Q, Y int] int; func (V[Q, Y]) A(Y) (y Q) { return }`, false}, - } - - for _, test := range tests { - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "p.go", "package p; "+test.src, 0) - if err != nil { - t.Fatalf("%s:\n%v", test.src, err) - } - var conf types.Config - pkg, err := conf.Check("p", fset, []*ast.File{f}, nil) - if err != nil { - t.Fatalf("%s:\n%v", test.src, err) - } - - V := pkg.Scope().Lookup("V").Type() - T := pkg.Scope().Lookup("T").Type() - - if types.AssignableTo(V, T) { - t.Fatal("AssignableTo") - } - - if got := GenericAssignableTo(nil, V, T); got != test.want { - t.Fatalf("%s:\nGenericAssignableTo(%v, %v) = %v, want %v", test.src, V, T, got, test.want) - } - } -} diff --git a/internal/typeparams/normalize_test.go b/internal/typeparams/normalize_test.go index d2c678c90ff..f78826225c6 100644 --- a/internal/typeparams/normalize_test.go +++ b/internal/typeparams/normalize_test.go @@ -89,8 +89,8 @@ type T[P interface{ A|B; C }] int if len(terms) == 0 { got = "all" } else { - qf := types.RelativeTo(pkg) - got = types.TypeString(types.NewUnion(terms), qf) + qual := types.RelativeTo(pkg) + got = types.TypeString(types.NewUnion(terms), qual) } want := regexp.MustCompile(test.want) if !want.MatchString(got) { diff --git a/internal/typesinternal/qualifier.go b/internal/typesinternal/qualifier.go new file mode 100644 index 00000000000..b64f714eb30 --- /dev/null +++ b/internal/typesinternal/qualifier.go @@ -0,0 +1,46 @@ +// 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 ( + "go/ast" + "go/types" + "strconv" +) + +// FileQualifier returns a [types.Qualifier] function that qualifies +// imported symbols appropriately based on the import environment of a given +// file. +// If the same package is imported multiple times, the last appearance is +// recorded. +func FileQualifier(f *ast.File, pkg *types.Package) types.Qualifier { + // Construct mapping of import paths to their defined names. + // It is only necessary to look at renaming imports. + imports := make(map[string]string) + for _, imp := range f.Imports { + if imp.Name != nil && imp.Name.Name != "_" { + path, _ := strconv.Unquote(imp.Path.Value) + imports[path] = imp.Name.Name + } + } + + // Define qualifier to replace full package paths with names of the imports. + return func(p *types.Package) string { + if p == nil || p == pkg { + return "" + } + + if name, ok := imports[p.Path()]; ok { + if name == "." { + return "" + } else { + return name + } + } + + // If there is no local renaming, fall back to the package name. + return p.Name() + } +} diff --git a/internal/typesinternal/recv.go b/internal/typesinternal/recv.go index ba6f4f4ebd5..e54accc69a0 100644 --- a/internal/typesinternal/recv.go +++ b/internal/typesinternal/recv.go @@ -11,6 +11,8 @@ import ( // ReceiverNamed returns the named type (if any) associated with the // type of recv, which may be of the form N or *N, or aliases thereof. // It also reports whether a Pointer was present. +// +// The named result may be nil in ill-typed code. func ReceiverNamed(recv *types.Var) (isPtr bool, named *types.Named) { t := recv.Type() if ptr, ok := types.Unalias(t).(*types.Pointer); ok { diff --git a/internal/typesinternal/types.go b/internal/typesinternal/types.go index df3ea521254..a93d51f9882 100644 --- a/internal/typesinternal/types.go +++ b/internal/typesinternal/types.go @@ -82,6 +82,7 @@ func NameRelativeTo(pkg *types.Package) types.Qualifier { type NamedOrAlias interface { types.Type Obj() *types.TypeName + // TODO(hxjiang): add method TypeArgs() *types.TypeList after stop supporting go1.22. } // TypeParams is a light shim around t.TypeParams(). diff --git a/internal/typesinternal/zerovalue.go b/internal/typesinternal/zerovalue.go index 1066980649e..d272949c177 100644 --- a/internal/typesinternal/zerovalue.go +++ b/internal/typesinternal/zerovalue.go @@ -9,62 +9,97 @@ import ( "go/ast" "go/token" "go/types" - "strconv" "strings" ) -// ZeroString returns the string representation of the "zero" value of the type t. +// ZeroString returns the string representation of the zero value for any type t. +// The boolean result indicates whether the type is or contains an invalid type +// or a non-basic (constraint) interface type. +// +// Even for invalid input types, ZeroString may return a partially correct +// string representation. The caller should use the returned isValid boolean +// to determine the validity of the expression. +// +// When assigning to a wider type (such as 'any'), it's the caller's +// responsibility to handle any necessary type conversions. +// // This string can be used on the right-hand side of an assignment where the // left-hand side has that explicit type. +// References to named types are qualified by an appropriate (optional) +// qualifier function. // Exception: This does not apply to tuples. Their string representation is // informational only and cannot be used in an assignment. -// When assigning to a wider type (such as 'any'), it's the caller's -// responsibility to handle any necessary type conversions. +// // See [ZeroExpr] for a variant that returns an [ast.Expr]. -func ZeroString(t types.Type, qf types.Qualifier) string { +func ZeroString(t types.Type, qual types.Qualifier) (_ string, isValid bool) { switch t := t.(type) { case *types.Basic: switch { case t.Info()&types.IsBoolean != 0: - return "false" + return "false", true case t.Info()&types.IsNumeric != 0: - return "0" + return "0", true case t.Info()&types.IsString != 0: - return `""` + return `""`, true case t.Kind() == types.UnsafePointer: fallthrough case t.Kind() == types.UntypedNil: - return "nil" + return "nil", true + case t.Kind() == types.Invalid: + return "invalid", false default: - panic(fmt.Sprint("ZeroString for unexpected type:", t)) + panic(fmt.Sprintf("ZeroString for unexpected type %v", t)) } - case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature: - return "nil" + case *types.Pointer, *types.Slice, *types.Chan, *types.Map, *types.Signature: + return "nil", true + + case *types.Interface: + if !t.IsMethodSet() { + return "invalid", false + } + return "nil", true - case *types.Named, *types.Alias: + case *types.Named: switch under := t.Underlying().(type) { case *types.Struct, *types.Array: - return types.TypeString(t, qf) + "{}" + return types.TypeString(t, qual) + "{}", true + default: + return ZeroString(under, qual) + } + + case *types.Alias: + switch t.Underlying().(type) { + case *types.Struct, *types.Array: + return types.TypeString(t, qual) + "{}", true default: - return ZeroString(under, qf) + // A type parameter can have alias but alias type's underlying type + // can never be a type parameter. + // Use types.Unalias to preserve the info of type parameter instead + // of call Underlying() going right through and get the underlying + // type of the type parameter which is always an interface. + return ZeroString(types.Unalias(t), qual) } case *types.Array, *types.Struct: - return types.TypeString(t, qf) + "{}" + return types.TypeString(t, qual) + "{}", true case *types.TypeParam: // Assumes func new is not shadowed. - return "*new(" + types.TypeString(t, qf) + ")" + return "*new(" + types.TypeString(t, qual) + ")", true case *types.Tuple: // Tuples are not normal values. // We are currently format as "(t[0], ..., t[n])". Could be something else. + isValid := true components := make([]string, t.Len()) for i := 0; i < t.Len(); i++ { - components[i] = ZeroString(t.At(i).Type(), qf) + comp, ok := ZeroString(t.At(i).Type(), qual) + + components[i] = comp + isValid = isValid && ok } - return "(" + strings.Join(components, ", ") + ")" + return "(" + strings.Join(components, ", ") + ")", isValid case *types.Union: // Variables of these types cannot be created, so it makes @@ -76,45 +111,72 @@ func ZeroString(t types.Type, qf types.Qualifier) string { } } -// ZeroExpr returns the ast.Expr representation of the "zero" value of the type t. -// ZeroExpr is defined for types that are suitable for variables. -// It may panic for other types such as Tuple or Union. +// ZeroExpr returns the ast.Expr representation of the zero value for any type t. +// The boolean result indicates whether the type is or contains an invalid type +// or a non-basic (constraint) interface type. +// +// Even for invalid input types, ZeroExpr may return a partially correct ast.Expr +// representation. The caller should use the returned isValid boolean to determine +// the validity of the expression. +// +// This function is designed for types suitable for variables and should not be +// used with Tuple or Union types.References to named types are qualified by an +// appropriate (optional) qualifier function. +// // See [ZeroString] for a variant that returns a string. -func ZeroExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { - switch t := typ.(type) { +func ZeroExpr(t types.Type, qual types.Qualifier) (_ ast.Expr, isValid bool) { + switch t := t.(type) { case *types.Basic: switch { case t.Info()&types.IsBoolean != 0: - return &ast.Ident{Name: "false"} + return &ast.Ident{Name: "false"}, true case t.Info()&types.IsNumeric != 0: - return &ast.BasicLit{Kind: token.INT, Value: "0"} + return &ast.BasicLit{Kind: token.INT, Value: "0"}, true case t.Info()&types.IsString != 0: - return &ast.BasicLit{Kind: token.STRING, Value: `""`} + return &ast.BasicLit{Kind: token.STRING, Value: `""`}, true case t.Kind() == types.UnsafePointer: fallthrough case t.Kind() == types.UntypedNil: - return ast.NewIdent("nil") + return ast.NewIdent("nil"), true + case t.Kind() == types.Invalid: + return &ast.BasicLit{Kind: token.STRING, Value: `"invalid"`}, false default: - panic(fmt.Sprint("ZeroExpr for unexpected type:", t)) + panic(fmt.Sprintf("ZeroExpr for unexpected type %v", t)) } - case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature: - return ast.NewIdent("nil") + case *types.Pointer, *types.Slice, *types.Chan, *types.Map, *types.Signature: + return ast.NewIdent("nil"), true + + case *types.Interface: + if !t.IsMethodSet() { + return &ast.BasicLit{Kind: token.STRING, Value: `"invalid"`}, false + } + return ast.NewIdent("nil"), true - case *types.Named, *types.Alias: + case *types.Named: switch under := t.Underlying().(type) { case *types.Struct, *types.Array: return &ast.CompositeLit{ - Type: TypeExpr(f, pkg, typ), - } + Type: TypeExpr(t, qual), + }, true default: - return ZeroExpr(f, pkg, under) + return ZeroExpr(under, qual) + } + + case *types.Alias: + switch t.Underlying().(type) { + case *types.Struct, *types.Array: + return &ast.CompositeLit{ + Type: TypeExpr(t, qual), + }, true + default: + return ZeroExpr(types.Unalias(t), qual) } case *types.Array, *types.Struct: return &ast.CompositeLit{ - Type: TypeExpr(f, pkg, typ), - } + Type: TypeExpr(t, qual), + }, true case *types.TypeParam: return &ast.StarExpr{ // *new(T) @@ -125,7 +187,7 @@ func ZeroExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { ast.NewIdent(t.Obj().Name()), }, }, - } + }, true case *types.Tuple: // Unlike ZeroString, there is no ast.Expr can express tuple by @@ -157,16 +219,14 @@ func IsZeroExpr(expr ast.Expr) bool { } // TypeExpr returns syntax for the specified type. References to named types -// from packages other than pkg are qualified by an appropriate package name, as -// defined by the import environment of file. +// are qualified by an appropriate (optional) qualifier function. // It may panic for types such as Tuple or Union. -func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { - switch t := typ.(type) { +func TypeExpr(t types.Type, qual types.Qualifier) ast.Expr { + switch t := t.(type) { case *types.Basic: switch t.Kind() { case types.UnsafePointer: - // TODO(hxjiang): replace the implementation with types.Qualifier. - return &ast.SelectorExpr{X: ast.NewIdent("unsafe"), Sel: ast.NewIdent("Pointer")} + return &ast.SelectorExpr{X: ast.NewIdent(qual(types.NewPackage("unsafe", "unsafe"))), Sel: ast.NewIdent("Pointer")} default: return ast.NewIdent(t.Name()) } @@ -174,7 +234,7 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { case *types.Pointer: return &ast.UnaryExpr{ Op: token.MUL, - X: TypeExpr(f, pkg, t.Elem()), + X: TypeExpr(t.Elem(), qual), } case *types.Array: @@ -183,18 +243,18 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { Kind: token.INT, Value: fmt.Sprintf("%d", t.Len()), }, - Elt: TypeExpr(f, pkg, t.Elem()), + Elt: TypeExpr(t.Elem(), qual), } case *types.Slice: return &ast.ArrayType{ - Elt: TypeExpr(f, pkg, t.Elem()), + Elt: TypeExpr(t.Elem(), qual), } case *types.Map: return &ast.MapType{ - Key: TypeExpr(f, pkg, t.Key()), - Value: TypeExpr(f, pkg, t.Elem()), + Key: TypeExpr(t.Key(), qual), + Value: TypeExpr(t.Elem(), qual), } case *types.Chan: @@ -204,14 +264,14 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { } return &ast.ChanType{ Dir: dir, - Value: TypeExpr(f, pkg, t.Elem()), + Value: TypeExpr(t.Elem(), qual), } case *types.Signature: var params []*ast.Field for i := 0; i < t.Params().Len(); i++ { params = append(params, &ast.Field{ - Type: TypeExpr(f, pkg, t.Params().At(i).Type()), + Type: TypeExpr(t.Params().At(i).Type(), qual), Names: []*ast.Ident{ { Name: t.Params().At(i).Name(), @@ -226,7 +286,7 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { var returns []*ast.Field for i := 0; i < t.Results().Len(); i++ { returns = append(returns, &ast.Field{ - Type: TypeExpr(f, pkg, t.Results().At(i).Type()), + Type: TypeExpr(t.Results().At(i).Type(), qual), }) } return &ast.FuncType{ @@ -238,23 +298,9 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { }, } - case interface{ Obj() *types.TypeName }: // *types.{Alias,Named,TypeParam} - switch t.Obj().Pkg() { - case pkg, nil: - return ast.NewIdent(t.Obj().Name()) - } - pkgName := t.Obj().Pkg().Name() - - // TODO(hxjiang): replace the implementation with types.Qualifier. - // If the file already imports the package under another name, use that. - for _, cand := range f.Imports { - if path, _ := strconv.Unquote(cand.Path.Value); path == t.Obj().Pkg().Path() { - if cand.Name != nil && cand.Name.Name != "" { - pkgName = cand.Name.Name - } - } - } - if pkgName == "." { + case *types.TypeParam: + pkgName := qual(t.Obj().Pkg()) + if pkgName == "" || t.Obj().Pkg() == nil { return ast.NewIdent(t.Obj().Name()) } return &ast.SelectorExpr{ @@ -262,6 +308,36 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { Sel: ast.NewIdent(t.Obj().Name()), } + // types.TypeParam also implements interface NamedOrAlias. To differentiate, + // case TypeParam need to be present before case NamedOrAlias. + // TODO(hxjiang): remove this comment once TypeArgs() is added to interface + // NamedOrAlias. + case NamedOrAlias: + var expr ast.Expr = ast.NewIdent(t.Obj().Name()) + if pkgName := qual(t.Obj().Pkg()); pkgName != "." && pkgName != "" { + expr = &ast.SelectorExpr{ + X: ast.NewIdent(pkgName), + Sel: expr.(*ast.Ident), + } + } + + // TODO(hxjiang): call t.TypeArgs after adding method TypeArgs() to + // typesinternal.NamedOrAlias. + if hasTypeArgs, ok := t.(interface{ TypeArgs() *types.TypeList }); ok { + if typeArgs := hasTypeArgs.TypeArgs(); typeArgs != nil && typeArgs.Len() > 0 { + var indices []ast.Expr + for i := range typeArgs.Len() { + indices = append(indices, TypeExpr(typeArgs.At(i), qual)) + } + expr = &ast.IndexListExpr{ + X: expr, + Indices: indices, + } + } + } + + return expr + case *types.Struct: return ast.NewIdent(t.String()) @@ -269,9 +345,43 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { return ast.NewIdent(t.String()) case *types.Union: - // TODO(hxjiang): handle the union through syntax (~A | ... | ~Z). - // Remove nil check when calling typesinternal.TypeExpr. - return nil + if t.Len() == 0 { + panic("Union type should have at least one term") + } + // Same as go/ast, the return expression will put last term in the + // Y field at topmost level of BinaryExpr. + // For union of type "float32 | float64 | int64", the structure looks + // similar to: + // { + // X: { + // X: float32, + // Op: | + // Y: float64, + // } + // Op: |, + // Y: int64, + // } + var union ast.Expr + for i := range t.Len() { + term := t.Term(i) + termExpr := TypeExpr(term.Type(), qual) + if term.Tilde() { + termExpr = &ast.UnaryExpr{ + Op: token.TILDE, + X: termExpr, + } + } + if i == 0 { + union = termExpr + } else { + union = &ast.BinaryExpr{ + X: union, + Op: token.OR, + Y: termExpr, + } + } + } + return union case *types.Tuple: panic("invalid input type types.Tuple") diff --git a/internal/typesinternal/zerovalue_test.go b/internal/typesinternal/zerovalue_test.go index 6cb6ea672a5..8ec1012dfda 100644 --- a/internal/typesinternal/zerovalue_test.go +++ b/internal/typesinternal/zerovalue_test.go @@ -16,13 +16,18 @@ import ( "strings" "testing" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typesinternal" ) func TestZeroValue(t *testing.T) { + if testenv.Go1Point() == 23 { + testenv.NeedsGoExperiment(t, "aliastypeparams") + } + // This test only refernece types/functions defined within the same package. // We can safely drop the package name when encountered. - qf := types.Qualifier(func(p *types.Package) string { + qual := types.Qualifier(func(p *types.Package) string { return "" }) src := ` @@ -32,6 +37,8 @@ type foo struct{ bar string } +type aliasFoo = foo + type namedInt int type namedString string type namedBool bool @@ -43,6 +50,7 @@ type namedMap map[string]foo type namedSignature func(string) string type namedStruct struct{ bar string } type namedArray [3]foo +type namedAlias aliasFoo type aliasInt = int type aliasString = string @@ -55,8 +63,30 @@ type aliasMap = map[string]foo type aliasSignature = func(string) string type aliasStruct = struct{ bar string } type aliasArray = [3]foo +type aliasNamed = foo func _[T any]() { + type aliasTypeParam = T + + // type aliasWithTypeParam[u any] = struct { + // x u + // y T + // } + // type aliasWithTypeParams[u, q any] = struct { + // x u + // y q + // z T + // } + + type namedWithTypeParam[u any] struct { + x u + y T + } + type namedWithTypeParams[u, q any] struct{ + x u + y q + z T + } var ( _ int // 0 _ bool // false @@ -80,6 +110,7 @@ func _[T any]() { _ namedSignature // nil _ namedStruct // namedStruct{} _ namedArray // namedArray{} + _ namedAlias // namedAlias{} _ aliasInt // 0 _ aliasString // "" @@ -91,6 +122,7 @@ func _[T any]() { _ aliasSignature // nil _ aliasStruct // aliasStruct{} _ aliasArray // aliasArray{} + _ aliasNamed // aliasNamed{} _ [4]string // [4]string{} _ [5]foo // [5]foo{} @@ -98,6 +130,17 @@ func _[T any]() { _ struct{f foo} // struct{f foo}{} _ T // *new(T) + _ *T // nil + + _ aliasTypeParam // *new(T) + _ *aliasTypeParam // nil + + // TODO(hxjiang): add test for alias type param after stop supporting go1.22. + // _ aliasWithTypeParam[int] // aliasWithTypeParam[int]{} + // _ aliasWithTypeParams[int, string] // aliasWithTypeParams[int, string]{} + + _ namedWithTypeParam[int] // namedWithTypeParam[int]{} + _ namedWithTypeParams[int, string] // namedWithTypeParams[int, string]{} ) } ` @@ -122,9 +165,9 @@ func _[T any]() { t.Fatalf("the last decl of the file is not FuncDecl") } - decl, ok := fun.Body.List[0].(*ast.DeclStmt).Decl.(*ast.GenDecl) + decl, ok := fun.Body.List[len(fun.Body.List)-1].(*ast.DeclStmt).Decl.(*ast.GenDecl) if !ok { - t.Fatalf("the first statement of the function is not GenDecl") + t.Fatalf("the last statement of the function is not GenDecl") } for _, spec := range decl.Specs { @@ -135,12 +178,12 @@ func _[T any]() { want := strings.TrimSpace(s.Comment.Text()) typ := info.TypeOf(s.Type) - got := typesinternal.ZeroString(typ, qf) + got, _ := typesinternal.ZeroString(typ, qual) if got != want { t.Errorf("%s: ZeroString() = %q, want zero value %q", fset.Position(spec.Pos()), got, want) } - zeroExpr := typesinternal.ZeroExpr(f, pkg, typ) + zeroExpr, _ := typesinternal.ZeroExpr(typ, typesinternal.FileQualifier(f, pkg)) var bytes bytes.Buffer printer.Fprint(&bytes, fset, zeroExpr) got = bytes.String()